├── .gitignore ├── LICENSE ├── README.md ├── anr-watchdog ├── .gitignore ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── github │ └── anrwatchdog │ ├── ANRError.java │ └── ANRWatchDog.java ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── testapp ├── .gitignore ├── build.gradle └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── github │ └── anrtestapp │ ├── ANRWatchdogTestApplication.java │ └── MainActivity.java └── res ├── layout └── activity_main.xml ├── mipmap-hdpi └── ic_launcher.png ├── mipmap-mdpi └── ic_launcher.png ├── mipmap-xhdpi └── ic_launcher.png ├── mipmap-xxhdpi └── ic_launcher.png ├── mipmap-xxxhdpi └── ic_launcher.png └── values └── strings.xml /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # ECLIPSE 3 | .classpath 4 | .project 5 | .settings/ 6 | 7 | # INTELLIJ 8 | .idea/ 9 | *.ipr 10 | *.iml 11 | 12 | # SYSTEM FILES 13 | *~ 14 | .DS_STORE 15 | 16 | # GRADLE 17 | .gradle 18 | /build/ 19 | gradle.properties 20 | local.properties 21 | .gradletasknamecache 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Salomon BRYS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Maven Central](https://img.shields.io/maven-central/v/com.github.anrwatchdog/anrwatchdog.svg)](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.github.anrwatchdog%22) 2 | [![MIT License](https://img.shields.io/github/license/salomonbrys/ANR-WatchDog.svg)](https://github.com/SalomonBrys/ANR-WatchDog/blob/master/LICENSE) 3 | [![GitHub issues](https://img.shields.io/github/issues/SalomonBrys/ANR-WatchDog.svg)](https://github.com/SalomonBrys/ANR-WatchDog/issues) 4 | [![Donate](https://img.shields.io/badge/Backing-Donate-orange.svg)](https://donorbox.org/donation-salomonbrys/) 5 | 6 | 7 | ANR-WatchDog 8 | ============ 9 | 10 | A simple watchdog that detects Android ANRs (Application Not Responding). 11 | 12 | 13 | Table of contents 14 | ----------------- 15 | 16 | * [ANR-WatchDog](#anr-watchdog) 17 | * [Table of contents](#table-of-contents) 18 | * [Why it exists](#why-it-exists) 19 | * [What it does](#what-it-does) 20 | * [Can it work with crash reporters?](#can-it-work-with-crash-reporters) 21 | * [How it works](#how-it-works) 22 | * [Usage](#usage) 23 | * [Install](#install) 24 | * [With Gradle / Android Studio](#with-gradle--android-studio) 25 | * [With Eclipse](#with-eclipse) 26 | * [Reading the ANRError exception report](#reading-the-anrerror-exception-report) 27 | * [Configuration](#configuration) 28 | * [Timeout (minimum hanging time for an ANR)](#timeout-minimum-hanging-time-for-an-anr) 29 | * [Debugger](#debugger) 30 | * [On ANR callback](#on-anr-callback) 31 | * [Filtering reports](#filtering-reports) 32 | * [Watchdog thread](#watchdog-thread) 33 | * [Donate](#donate) 34 | 35 | 36 | Why it exists 37 | ------------- 38 | 39 | There is currently no way for an android application to catch and report ANR errors. 40 | If your application is not in the play store (either because you are still developing it or because you are distributing it differently), the only way to investigate an ANR is to pull the file /data/anr/traces.txt. 41 | Additionally, we found that using the Play Store was not as effective as being able to choose our own bug tracking service. 42 | 43 | There is an [issue entry](https://code.google.com/p/android/issues/detail?id=35380) in the android bug tracker describing this lack, feel free to star it ;) 44 | 45 | 46 | What it does 47 | ------------ 48 | 49 | It sets up a "watchdog" timer that will detect when the UI thread stops responding. When it does, it raises an error with all threads stack traces (main first). 50 | 51 | 52 | Can it work with crash reporters? 53 | --------------------------------- 54 | 55 | Yes! I'm glad you asked: That's the reason why it was developed in the first place! 56 | As this throws an error, a crash handler can intercept it and handle it the way it needs. 57 | 58 | Known working crash reporters include: 59 | 60 | * [ACRA](https://github.com/ACRA/acra) 61 | * [Crashlytics](https://get.fabric.io/crashlytics) ([Only with `setReportMainThreadOnly()`](https://github.com/SalomonBrys/ANR-WatchDog/issues/29)) 62 | * [HockeyApp](https://hockeyapp.net/) 63 | * [Bugsnag](https://www.bugsnag.com/) 64 | 65 | And there is no reason why it should not work with *[insert your favourite crash reporting system here]*. 66 | 67 | 68 | How it works 69 | ------------ 70 | 71 | The watchdog is a simple thread that does the following in a loop: 72 | 73 | 1. Schedules a runnable to be run on the UI thread as soon as possible. 74 | 2. Wait for 5 seconds. (5 seconds is the default, but it can be configured). 75 | 3. See if the runnable has been run. If it has, go back to 1. 76 | 4. If the runnable has not been run, which means that the UI thread has been blocked for at least 5 seconds, it raises an error with all running threads stack traces. 77 | 78 | 79 | Usage 80 | ===== 81 | 82 | Install 83 | ------- 84 | 85 | ### With Gradle / Android Studio 86 | 87 | 1. In the `app/build.gradle` file, add: 88 | 89 | ``` 90 | implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0' 91 | ``` 92 | 93 | 2. In your application class, in `onCreate`, add: 94 | 95 | ```java 96 | new ANRWatchDog().start(); 97 | ``` 98 | 99 | 100 | ### With Eclipse 101 | 102 | 1. [Download the latest jar](https://search.maven.org/remote_content?g=com.github.anrwatchdog&a=anrwatchdog&v=LATEST) 103 | 104 | 2. Put the jar in the `libs/` directory of your project 105 | 106 | 107 | Reading the ANRError exception report 108 | ------------------------------------- 109 | 110 | The `ANRError` stack trace is a bit particular, it has the stack traces of all the threads running in your application. So, in the report, **each `caused by` section is not the cause of the precedent exception**, but the stack trace of a different thread. 111 | 112 | Here is a dead lock example: 113 | 114 | ``` 115 | FATAL EXCEPTION: |ANR-WatchDog| 116 | Process: anrwatchdog.github.com.testapp, PID: 26737 117 | com.github.anrwatchdog.ANRError: Application Not Responding 118 | Caused by: com.github.anrwatchdog.ANRError$_$_Thread: main (state = WAITING) 119 | at testapp.MainActivity$1.run(MainActivity.java:46) 120 | at android.os.Handler.handleCallback(Handler.java:739) 121 | at android.os.Handler.dispatchMessage(Handler.java:95) 122 | at android.os.Looper.loop(Looper.java:135) 123 | at android.app.ActivityThread.main(ActivityThread.java:5221) 124 | Caused by: com.github.anrwatchdog.ANRError$_$_Thread: APP: Locker (state = TIMED_WAITING) 125 | at java.lang.Thread.sleep(Native Method) 126 | at java.lang.Thread.sleep(Thread.java:1031) 127 | at java.lang.Thread.sleep(Thread.java:985) 128 | at testapp.MainActivity.SleepAMinute(MainActivity.java:18) 129 | at testapp.MainActivity.access$100(MainActivity.java:12) 130 | at testapp.MainActivity$LockerThread.run(MainActivity.java:36) 131 | ``` 132 | 133 | From this report, we can see that the stack traces of two threads. The first (the "main" thread) is stuck at `MainActivity.java:46` while the second thread (named "App: Locker") is locked in a Sleep at `MainActivity.java:18`. 134 | From there, if we looked at those two lines, we would surely understand the cause of the dead lock! 135 | 136 | Note that some crash reporting library (such as Crashlytics) report all thread stack traces at the time of an uncaught exception. In that case, having all threads in the same exception can be cumbersome. In such cases, simply use `setReportMainThreadOnly()`. 137 | 138 | 139 | Configuration 140 | ------------- 141 | 142 | ### Timeout (minimum hanging time for an ANR) 143 | 144 | To set a different timeout (5000 millis is the default): 145 | 146 | ```java 147 | if (BuildConfig.DEBUG == false) { 148 | new ANRWatchDog(10000 /*timeout*/).start(); 149 | } 150 | ``` 151 | 152 | 153 | ### Debugger 154 | 155 | By default, the watchdog will ignore ANRs if the debugger is attached or if the app is waiting for the debugger to attach. This is because it detects execution pauses and breakpoints as ANRs. 156 | To disable this and throw an `ANRError` even if the debugger is connected, you can add `setIgnoreDebugger(true)`: 157 | 158 | ```java 159 | new ANRWatchDog().setIgnoreDebugger(true).start(); 160 | ``` 161 | 162 | 163 | ### On ANR callback 164 | 165 | If you would prefer not to crash the application when an ANR is detected, you can enable a callback instead: 166 | 167 | ```java 168 | new ANRWatchDog().setANRListener(new ANRWatchDog.ANRListener() { 169 | @Override 170 | public void onAppNotResponding(ANRError error) { 171 | // Handle the error. For example, log it to HockeyApp: 172 | ExceptionHandler.saveException(error, new CrashManager()); 173 | } 174 | }).start(); 175 | ``` 176 | 177 | **This is very important when delivering your app in production.** 178 | When in the hand of the final user, it's *probably better* not to crash after 5 seconds, but simply report the ANR to whatever reporting system you use. 179 | Maybe, after some more seconds, the app will "de-freeze". 180 | 181 | 182 | ### Filtering reports 183 | 184 | If you would like to have only your own threads to be reported in the ANRError, and not all threads (including system threads such as the `FinalizerDaemon` thread), you can set a prefix: only the threads whose name starts with this prefix will be reported. 185 | 186 | ```java 187 | new ANRWatchDog().setReportThreadNamePrefix("APP:").start(); 188 | ``` 189 | 190 | Then, when you start a thread, don't forget to set its name to something that starts with this prefix (if you want it to be reported): 191 | 192 | ```java 193 | public class MyAmazingThread extends Thread { 194 | @Override 195 | public void run() { 196 | setName("APP: Amazing!"); 197 | /* ... do amazing things ... */ 198 | } 199 | } 200 | ``` 201 | 202 | If you want to have only the main thread stack trace and not all the other threads, you can: 203 | 204 | ```java 205 | new ANRWatchDog().setReportMainThreadOnly().start(); 206 | ``` 207 | 208 | 209 | ### ANR Interceptor 210 | 211 | Sometimes, you want to know that the application has froze for a certain duration, but not report the ANR error just yet. 212 | You can define an interceptor that will be called before reporting an error. 213 | The role of the interceptor is to define whether or not, given the given freeze duration, an ANR error should be raised or postponed. 214 | 215 | ```java 216 | new ANRWatchDog(2000).setANRInterceptor(new ANRWatchDog.ANRInterceptor() { 217 | @Override 218 | public long intercept(long duration) { 219 | long ret = 5000 - duration; 220 | if (ret > 0) { 221 | Log.w(TAG, "Intercepted ANR that is too short (" + duration + " ms), postponing for " + ret + " ms."); 222 | } 223 | return ret; 224 | } 225 | }) 226 | ``` 227 | 228 | In this example, the ANRWatchDog starts with a timeout of 2000 ms, but the interceptor will postpone the error until at least 5000 ms of freeze has been reached. 229 | 230 | 231 | ### Watchdog thread 232 | 233 | ANRWatchDog is a thread, so you can interrupt it at any time. 234 | 235 | If you are programming with Android's multi process capability (like starting an activity in a new process), remember that you will need an ANRWatchDog thread per process. 236 | 237 | 238 | Donate 239 | ====== 240 | 241 | ANR-Watchdog is free to use for both non-profit and commercial use and always will be. 242 | 243 | If you wish to show some support or appreciation to my work, you are free to **[donate](https://donorbox.org/donation-salomonbrys)**! 244 | 245 | This would be (of course) greatly appreciated but is by no means necessary to receive help or support, which I'll be happy to provide for free :) 246 | -------------------------------------------------------------------------------- /anr-watchdog/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /anr-watchdog/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | apply plugin: 'com.android.library' 3 | apply plugin: 'maven' 4 | apply plugin: 'signing' 5 | 6 | group = "com.github.anrwatchdog" 7 | version = "1.4.0" 8 | 9 | sourceCompatibility = 1.6 10 | 11 | task javadoc(type: Javadoc) { 12 | failOnError false 13 | source = android.sourceSets.main.java.sourceFiles 14 | } 15 | 16 | task javadocJar(type: Jar, dependsOn: javadoc) { 17 | classifier 'javadoc' 18 | from javadoc.destinationDir 19 | } 20 | 21 | task sourcesJar(type: Jar) { 22 | from android.sourceSets.main.java.srcDirs 23 | classifier = 'sources' 24 | } 25 | 26 | artifacts { 27 | archives javadocJar, sourcesJar 28 | } 29 | 30 | signing { 31 | sign configurations.archives 32 | } 33 | 34 | configurations { 35 | provided 36 | compile.extendsFrom provided 37 | } 38 | 39 | android { 40 | compileSdkVersion 29 41 | defaultConfig { 42 | minSdkVersion 14 43 | targetSdkVersion 28 44 | } 45 | } 46 | 47 | dependencies { 48 | implementation 'androidx.annotation:annotation:1.1.0' 49 | } 50 | 51 | uploadArchives { 52 | repositories { 53 | mavenDeployer { 54 | beforeDeployment { deployment -> signing.signPom(deployment) } 55 | 56 | repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { 57 | authentication(userName: ossrhUsername, password: ossrhPassword) 58 | } 59 | 60 | snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { 61 | authentication(userName: ossrhUsername, password: ossrhPassword) 62 | } 63 | 64 | pom.project { 65 | name 'ANR-Watchdog' 66 | packaging 'jar' 67 | artifactId 'anrwatchdog' 68 | description 'A simple watchdog that detects Android ANR (Application Not Responding) error and throws a meaningful exception' 69 | url 'https://github.com/SalomonBrys/ANR-WatchDog' 70 | 71 | licenses { 72 | license { 73 | name 'MIT' 74 | url 'http://opensource.org/licenses/MIT' 75 | } 76 | } 77 | 78 | developers { 79 | developer { 80 | id 'salomonbrys' 81 | name 'Salomon BRYS' 82 | email 'salomon.brys@gmail.com' 83 | } 84 | } 85 | 86 | scm { 87 | url 'https://github.com/SalomonBrys/ANR-WatchDog' 88 | connection 'scm:git:https://github.com/SalomonBrys/ANR-WatchDog.git' 89 | developerConnection 'scm:git:git@github.com:SalomonBrys/ANR-WatchDog.git' 90 | tag 'HEAD' 91 | } 92 | 93 | issueManagement { 94 | system 'GitHub Issues' 95 | url 'https://github.com/SalomonBrys/ANR-WatchDog/issues' 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /anr-watchdog/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /anr-watchdog/src/main/java/com/github/anrwatchdog/ANRError.java: -------------------------------------------------------------------------------- 1 | package com.github.anrwatchdog; 2 | 3 | import android.os.Looper; 4 | 5 | import java.io.Serializable; 6 | import java.util.Comparator; 7 | import java.util.Map; 8 | import java.util.TreeMap; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.annotation.Nullable; 12 | 13 | /** 14 | * Error thrown by {@link com.github.anrwatchdog.ANRWatchDog} when an ANR is detected. 15 | * Contains the stack trace of the frozen UI thread. 16 | *

17 | * It is important to notice that, in an ANRError, all the "Caused by" are not really the cause 18 | * of the exception. Each "Caused by" is the stack trace of a running thread. Note that the main 19 | * thread always comes first. 20 | */ 21 | public class ANRError extends Error { 22 | 23 | private static class $ implements Serializable { 24 | private final String _name; 25 | private final StackTraceElement[] _stackTrace; 26 | 27 | private class _Thread extends Throwable { 28 | private _Thread(_Thread other) { 29 | super(_name, other); 30 | } 31 | 32 | @Override 33 | @NonNull 34 | public Throwable fillInStackTrace() { 35 | setStackTrace(_stackTrace); 36 | return this; 37 | } 38 | } 39 | 40 | private $(String name, StackTraceElement[] stackTrace) { 41 | _name = name; 42 | _stackTrace = stackTrace; 43 | } 44 | } 45 | 46 | private static final long serialVersionUID = 1L; 47 | 48 | /** 49 | * The minimum duration, in ms, for which the main thread has been blocked. May be more. 50 | */ 51 | @SuppressWarnings("WeakerAccess") 52 | public final long duration; 53 | 54 | private ANRError($._Thread st, long duration) { 55 | super("Application Not Responding for at least " + duration + " ms.", st); 56 | this.duration = duration; 57 | } 58 | 59 | @Override 60 | @NonNull 61 | public Throwable fillInStackTrace() { 62 | setStackTrace(new StackTraceElement[] {}); 63 | return this; 64 | } 65 | 66 | @NonNull 67 | static ANRError New(long duration, @Nullable String prefix, boolean logThreadsWithoutStackTrace) { 68 | final Thread mainThread = Looper.getMainLooper().getThread(); 69 | 70 | final Map stackTraces = new TreeMap(new Comparator() { 71 | @Override 72 | public int compare(Thread lhs, Thread rhs) { 73 | if (lhs == rhs) 74 | return 0; 75 | if (lhs == mainThread) 76 | return 1; 77 | if (rhs == mainThread) 78 | return -1; 79 | return rhs.getName().compareTo(lhs.getName()); 80 | } 81 | }); 82 | 83 | for (Map.Entry entry : Thread.getAllStackTraces().entrySet()) 84 | if ( 85 | entry.getKey() == mainThread 86 | || ( 87 | entry.getKey().getName().startsWith(prefix) 88 | && ( 89 | logThreadsWithoutStackTrace 90 | || 91 | entry.getValue().length > 0 92 | ) 93 | ) 94 | ) 95 | stackTraces.put(entry.getKey(), entry.getValue()); 96 | 97 | // Sometimes main is not returned in getAllStackTraces() - ensure that we list it 98 | if (!stackTraces.containsKey(mainThread)) { 99 | stackTraces.put(mainThread, mainThread.getStackTrace()); 100 | } 101 | 102 | $._Thread tst = null; 103 | for (Map.Entry entry : stackTraces.entrySet()) 104 | tst = new $(getThreadTitle(entry.getKey()), entry.getValue()).new _Thread(tst); 105 | 106 | return new ANRError(tst, duration); 107 | } 108 | 109 | @NonNull 110 | static ANRError NewMainOnly(long duration) { 111 | final Thread mainThread = Looper.getMainLooper().getThread(); 112 | final StackTraceElement[] mainStackTrace = mainThread.getStackTrace(); 113 | 114 | return new ANRError(new $(getThreadTitle(mainThread), mainStackTrace).new _Thread(null), duration); 115 | } 116 | 117 | private static String getThreadTitle(Thread thread) { 118 | return thread.getName() + " (state = " + thread.getState() + ")"; 119 | } 120 | } -------------------------------------------------------------------------------- /anr-watchdog/src/main/java/com/github/anrwatchdog/ANRWatchDog.java: -------------------------------------------------------------------------------- 1 | package com.github.anrwatchdog; 2 | 3 | /* 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2016 Salomon BRYS 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the "Software"), to deal in 10 | * the Software without restriction, including without limitation the rights to 11 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 12 | * the Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | import android.os.Debug; 27 | import android.os.Handler; 28 | import android.os.Looper; 29 | import android.util.Log; 30 | 31 | import androidx.annotation.NonNull; 32 | import androidx.annotation.Nullable; 33 | 34 | /** 35 | * A watchdog timer thread that detects when the UI thread has frozen. 36 | */ 37 | @SuppressWarnings("UnusedReturnValue") 38 | public class ANRWatchDog extends Thread { 39 | 40 | public interface ANRListener { 41 | /** 42 | * Called when an ANR is detected. 43 | * 44 | * @param error The error describing the ANR. 45 | */ 46 | void onAppNotResponding(@NonNull ANRError error); 47 | } 48 | 49 | public interface ANRInterceptor { 50 | /** 51 | * Called when main thread has froze more time than defined by the timeout. 52 | * 53 | * @param duration The minimum time (in ms) the main thread has been frozen (may be more). 54 | * @return 0 or negative if the ANR should be reported immediately. A positive number of ms to postpone the reporting. 55 | */ 56 | long intercept(long duration); 57 | } 58 | 59 | public interface InterruptionListener { 60 | void onInterrupted(@NonNull InterruptedException exception); 61 | } 62 | 63 | private static final int DEFAULT_ANR_TIMEOUT = 5000; 64 | 65 | private static final ANRListener DEFAULT_ANR_LISTENER = new ANRListener() { 66 | @Override public void onAppNotResponding(@NonNull ANRError error) { 67 | throw error; 68 | } 69 | }; 70 | 71 | private static final ANRInterceptor DEFAULT_ANR_INTERCEPTOR = new ANRInterceptor() { 72 | @Override public long intercept(long duration) { 73 | return 0; 74 | } 75 | }; 76 | 77 | private static final InterruptionListener DEFAULT_INTERRUPTION_LISTENER = new InterruptionListener() { 78 | @Override public void onInterrupted(@NonNull InterruptedException exception) { 79 | Log.w("ANRWatchdog", "Interrupted: " + exception.getMessage()); 80 | } 81 | }; 82 | 83 | private ANRListener _anrListener = DEFAULT_ANR_LISTENER; 84 | private ANRInterceptor _anrInterceptor = DEFAULT_ANR_INTERCEPTOR; 85 | private InterruptionListener _interruptionListener = DEFAULT_INTERRUPTION_LISTENER; 86 | 87 | private final Handler _uiHandler = new Handler(Looper.getMainLooper()); 88 | private final int _timeoutInterval; 89 | 90 | private String _namePrefix = ""; 91 | private boolean _logThreadsWithoutStackTrace = false; 92 | private boolean _ignoreDebugger = false; 93 | 94 | private volatile long _tick = 0; 95 | private volatile boolean _reported = false; 96 | 97 | private final Runnable _ticker = new Runnable() { 98 | @Override public void run() { 99 | _tick = 0; 100 | _reported = false; 101 | } 102 | }; 103 | 104 | /** 105 | * Constructs a watchdog that checks the ui thread every {@value #DEFAULT_ANR_TIMEOUT} milliseconds 106 | */ 107 | public ANRWatchDog() { 108 | this(DEFAULT_ANR_TIMEOUT); 109 | } 110 | 111 | /** 112 | * Constructs a watchdog that checks the ui thread every given interval 113 | * 114 | * @param timeoutInterval The interval, in milliseconds, between to checks of the UI thread. 115 | * It is therefore the maximum time the UI may freeze before being reported as ANR. 116 | */ 117 | public ANRWatchDog(int timeoutInterval) { 118 | super(); 119 | _timeoutInterval = timeoutInterval; 120 | } 121 | 122 | /** 123 | * @return The interval the WatchDog 124 | */ 125 | public int getTimeoutInterval() { 126 | return _timeoutInterval; 127 | } 128 | 129 | /** 130 | * Sets an interface for when an ANR is detected. 131 | * If not set, the default behavior is to throw an error and crash the application. 132 | * 133 | * @param listener The new listener or null 134 | * @return itself for chaining. 135 | */ 136 | @NonNull 137 | public ANRWatchDog setANRListener(@Nullable ANRListener listener) { 138 | if (listener == null) { 139 | _anrListener = DEFAULT_ANR_LISTENER; 140 | } else { 141 | _anrListener = listener; 142 | } 143 | return this; 144 | } 145 | 146 | /** 147 | * Sets an interface to intercept ANRs before they are reported. 148 | * If set, you can define if, given the current duration of the detected ANR and external context, it is necessary to report the ANR. 149 | * 150 | * @param interceptor The new interceptor or null 151 | * @return itself for chaining. 152 | */ 153 | @NonNull 154 | public ANRWatchDog setANRInterceptor(@Nullable ANRInterceptor interceptor) { 155 | if (interceptor == null) { 156 | _anrInterceptor = DEFAULT_ANR_INTERCEPTOR; 157 | } else { 158 | _anrInterceptor = interceptor; 159 | } 160 | return this; 161 | } 162 | 163 | /** 164 | * Sets an interface for when the watchdog thread is interrupted. 165 | * If not set, the default behavior is to just log the interruption message. 166 | * 167 | * @param listener The new listener or null. 168 | * @return itself for chaining. 169 | */ 170 | @NonNull 171 | public ANRWatchDog setInterruptionListener(@Nullable InterruptionListener listener) { 172 | if (listener == null) { 173 | _interruptionListener = DEFAULT_INTERRUPTION_LISTENER; 174 | } else { 175 | _interruptionListener = listener; 176 | } 177 | return this; 178 | } 179 | 180 | /** 181 | * Set the prefix that a thread's name must have for the thread to be reported. 182 | * Note that the main thread is always reported. 183 | * Default "". 184 | * 185 | * @param prefix The thread name's prefix for a thread to be reported. 186 | * @return itself for chaining. 187 | */ 188 | @NonNull 189 | public ANRWatchDog setReportThreadNamePrefix(@Nullable String prefix) { 190 | if (prefix == null) { 191 | prefix = ""; 192 | } 193 | _namePrefix = prefix; 194 | return this; 195 | } 196 | 197 | /** 198 | * Set that only the main thread will be reported. 199 | * 200 | * @return itself for chaining. 201 | */ 202 | @NonNull 203 | public ANRWatchDog setReportMainThreadOnly() { 204 | _namePrefix = null; 205 | return this; 206 | } 207 | 208 | /** 209 | * Set that all threads will be reported (default behaviour). 210 | * 211 | * @return itself for chaining. 212 | */ 213 | @NonNull 214 | public ANRWatchDog setReportAllThreads() { 215 | _namePrefix = ""; 216 | return this; 217 | } 218 | 219 | /** 220 | * Set that all running threads will be reported, 221 | * even those from which no stack trace could be extracted. 222 | * Default false. 223 | * 224 | * @param logThreadsWithoutStackTrace Whether or not all running threads should be reported 225 | * @return itself for chaining. 226 | */ 227 | @NonNull 228 | public ANRWatchDog setLogThreadsWithoutStackTrace(boolean logThreadsWithoutStackTrace) { 229 | _logThreadsWithoutStackTrace = logThreadsWithoutStackTrace; 230 | return this; 231 | } 232 | 233 | /** 234 | * Set whether to ignore the debugger when detecting ANRs. 235 | * When ignoring the debugger, ANRWatchdog will detect ANRs even if the debugger is connected. 236 | * By default, it does not, to avoid interpreting debugging pauses as ANRs. 237 | * Default false. 238 | * 239 | * @param ignoreDebugger Whether to ignore the debugger. 240 | * @return itself for chaining. 241 | */ 242 | @NonNull 243 | public ANRWatchDog setIgnoreDebugger(boolean ignoreDebugger) { 244 | _ignoreDebugger = ignoreDebugger; 245 | return this; 246 | } 247 | 248 | @SuppressWarnings("NonAtomicOperationOnVolatileField") 249 | @Override 250 | public void run() { 251 | setName("|ANR-WatchDog|"); 252 | 253 | long interval = _timeoutInterval; 254 | while (!isInterrupted()) { 255 | boolean needPost = _tick == 0; 256 | _tick += interval; 257 | if (needPost) { 258 | _uiHandler.post(_ticker); 259 | } 260 | 261 | try { 262 | Thread.sleep(interval); 263 | } catch (InterruptedException e) { 264 | _interruptionListener.onInterrupted(e); 265 | return ; 266 | } 267 | 268 | // If the main thread has not handled _ticker, it is blocked. ANR. 269 | if (_tick != 0 && !_reported) { 270 | //noinspection ConstantConditions 271 | if (!_ignoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) { 272 | Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))"); 273 | _reported = true; 274 | continue ; 275 | } 276 | 277 | interval = _anrInterceptor.intercept(_tick); 278 | if (interval > 0) { 279 | continue; 280 | } 281 | 282 | final ANRError error; 283 | if (_namePrefix != null) { 284 | error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace); 285 | } else { 286 | error = ANRError.NewMainOnly(_tick); 287 | } 288 | _anrListener.onAppNotResponding(error); 289 | interval = _timeoutInterval; 290 | _reported = true; 291 | } 292 | } 293 | } 294 | 295 | } 296 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | mavenCentral() 7 | google() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.3.2' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SalomonBrys/ANR-WatchDog/4d7b4bb288d34a604be80c3c1b13ec85c0c5b06a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 06 12:43:28 CET 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':anr-watchdog', ':testapp' 2 | -------------------------------------------------------------------------------- /testapp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /testapp/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | 6 | defaultConfig { 7 | applicationId "anrwatchdog.github.com.testapp" 8 | minSdkVersion 14 9 | targetSdkVersion 28 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | } 14 | 15 | dependencies { 16 | implementation project(':anr-watchdog') 17 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 18 | implementation 'androidx.annotation:annotation:1.1.0' 19 | } 20 | -------------------------------------------------------------------------------- /testapp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /testapp/src/main/java/com/github/anrtestapp/ANRWatchdogTestApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.anrtestapp; 2 | 3 | import android.app.Application; 4 | import android.util.Log; 5 | 6 | import com.github.anrwatchdog.ANRError; 7 | import com.github.anrwatchdog.ANRWatchDog; 8 | 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.IOException; 11 | import java.io.ObjectOutputStream; 12 | 13 | import androidx.annotation.NonNull; 14 | 15 | public class ANRWatchdogTestApplication extends Application { 16 | 17 | ANRWatchDog anrWatchDog = new ANRWatchDog(2000); 18 | 19 | int duration = 4; 20 | 21 | final ANRWatchDog.ANRListener silentListener = new ANRWatchDog.ANRListener() { 22 | @Override 23 | public void onAppNotResponding(@NonNull ANRError error) { 24 | Log.e("ANR-Watchdog-Demo", "", error); 25 | } 26 | }; 27 | 28 | @Override 29 | public void onCreate() { 30 | super.onCreate(); 31 | 32 | anrWatchDog 33 | .setANRListener(new ANRWatchDog.ANRListener() { 34 | @Override 35 | public void onAppNotResponding(@NonNull ANRError error) { 36 | Log.e("ANR-Watchdog-Demo", "Detected Application Not Responding!"); 37 | 38 | // Some tools like ACRA are serializing the exception, so we must make sure the exception serializes correctly 39 | try { 40 | new ObjectOutputStream(new ByteArrayOutputStream()).writeObject(error); 41 | } 42 | catch (IOException ex) { 43 | throw new RuntimeException(ex); 44 | } 45 | 46 | Log.i("ANR-Watchdog-Demo", "Error was successfully serialized"); 47 | 48 | throw error; 49 | } 50 | }) 51 | .setANRInterceptor(new ANRWatchDog.ANRInterceptor() { 52 | @Override 53 | public long intercept(long duration) { 54 | long ret = ANRWatchdogTestApplication.this.duration * 1000 - duration; 55 | if (ret > 0) 56 | Log.w("ANR-Watchdog-Demo", "Intercepted ANR that is too short (" + duration + " ms), postponing for " + ret + " ms."); 57 | return ret; 58 | } 59 | }) 60 | ; 61 | 62 | anrWatchDog.start(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /testapp/src/main/java/com/github/anrtestapp/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.anrtestapp; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.os.Bundle; 6 | import android.os.Handler; 7 | import android.util.Log; 8 | import android.view.View; 9 | import android.widget.Button; 10 | 11 | 12 | public class MainActivity extends Activity { 13 | 14 | private final Object _mutex = new Object(); 15 | 16 | private static void Sleep() { 17 | try { 18 | Thread.sleep(8 * 1000); 19 | } 20 | catch (InterruptedException e) { 21 | e.printStackTrace(); 22 | } 23 | } 24 | 25 | private static void InfiniteLoop() { 26 | int i = 0; 27 | //noinspection InfiniteLoopStatement 28 | while (true) { 29 | i++; 30 | } 31 | } 32 | 33 | public class LockerThread extends Thread { 34 | 35 | LockerThread() { 36 | setName("APP: Locker"); 37 | } 38 | 39 | @Override 40 | public void run() { 41 | synchronized (_mutex) { 42 | //noinspection InfiniteLoopStatement 43 | while (true) 44 | Sleep(); 45 | } 46 | } 47 | } 48 | 49 | private void _deadLock() { 50 | new LockerThread().start(); 51 | 52 | new Handler().postDelayed(new Runnable() { 53 | @Override public void run() { 54 | synchronized (_mutex) { 55 | Log.e("ANR-Failed", "There should be a dead lock before this message"); 56 | } 57 | } 58 | }, 1000); 59 | } 60 | 61 | private int mode = 0; 62 | private boolean crash = true; 63 | 64 | @SuppressLint("SetTextI18n") 65 | @Override 66 | protected void onCreate(Bundle savedInstanceState) { 67 | super.onCreate(savedInstanceState); 68 | setContentView(R.layout.activity_main); 69 | 70 | final ANRWatchdogTestApplication application = (ANRWatchdogTestApplication) getApplication(); 71 | 72 | final Button minAnrDurationButton = (Button) findViewById(R.id.minAnrDuration); 73 | minAnrDurationButton.setText(application.duration + " seconds"); 74 | minAnrDurationButton.setOnClickListener(new View.OnClickListener() { 75 | @Override 76 | public void onClick(View v) { 77 | application.duration = application.duration % 6 + 2; 78 | minAnrDurationButton.setText(application.duration + " seconds"); 79 | } 80 | }); 81 | 82 | final Button reportModeButton = (Button) findViewById(R.id.reportMode); 83 | reportModeButton.setText("All threads"); 84 | reportModeButton.setOnClickListener(new View.OnClickListener() { 85 | @Override 86 | public void onClick(View v) { 87 | mode = (mode + 1) % 3; 88 | switch (mode) { 89 | case 0: 90 | reportModeButton.setText("All threads"); 91 | application.anrWatchDog.setReportAllThreads(); 92 | break ; 93 | case 1: 94 | reportModeButton.setText("Main thread only"); 95 | application.anrWatchDog.setReportMainThreadOnly(); 96 | break ; 97 | case 2: 98 | reportModeButton.setText("Filtered"); 99 | application.anrWatchDog.setReportThreadNamePrefix("APP:"); 100 | break ; 101 | } 102 | } 103 | }); 104 | 105 | final Button behaviourButton = (Button) findViewById(R.id.behaviour); 106 | behaviourButton.setText("Crash"); 107 | behaviourButton.setOnClickListener(new View.OnClickListener() { 108 | @Override 109 | public void onClick(View v) { 110 | crash = !crash; 111 | if (crash) { 112 | behaviourButton.setText("Crash"); 113 | application.anrWatchDog.setANRListener(null); 114 | } else { 115 | behaviourButton.setText("Silent"); 116 | application.anrWatchDog.setANRListener(application.silentListener); 117 | } 118 | } 119 | }); 120 | 121 | findViewById(R.id.threadSleep).setOnClickListener(new View.OnClickListener() { 122 | @Override public void onClick(View v) { 123 | Sleep(); 124 | } 125 | }); 126 | 127 | findViewById(R.id.infiniteLoop).setOnClickListener(new View.OnClickListener() { 128 | @Override public void onClick(View v) { 129 | InfiniteLoop(); 130 | } 131 | }); 132 | 133 | findViewById(R.id.deadlock).setOnClickListener(new View.OnClickListener() { 134 | @Override public void onClick(View v) { 135 | _deadLock(); 136 | } 137 | }); 138 | 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /testapp/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 20 | 21 | 22 | 37 | 38 |