From 9ffc0a70b06a98e4c850171e4f2141d2d9ba5633 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Fri, 22 Mar 2019 16:20:23 -0700 Subject: [PATCH] refactor: ANR implementation now uses a HandlerThread and uptimeMillis, and only reports ANRs in the Using a HandlerThread with postDelayed and uptimeMillis will pause ANR detection when the system enters a deep sleep. As the ANR dialog is only shown when the user dispatches a touch event, we use whether the app is in the foreground or not as a proxy to determine whether we should count an ANR. --- .../android/BlockedThreadDetector.java | 78 +++++++++++-------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/sdk/src/main/java/com/bugsnag/android/BlockedThreadDetector.java b/sdk/src/main/java/com/bugsnag/android/BlockedThreadDetector.java index 137def31ee..1405963269 100644 --- a/sdk/src/main/java/com/bugsnag/android/BlockedThreadDetector.java +++ b/sdk/src/main/java/com/bugsnag/android/BlockedThreadDetector.java @@ -1,6 +1,9 @@ package com.bugsnag.android; +import android.app.ActivityManager; +import android.os.Build; import android.os.Handler; +import android.os.HandlerThread; import android.os.Looper; import android.os.SystemClock; @@ -27,7 +30,9 @@ interface Delegate { final Looper looper; final long checkIntervalMs; final long blockedThresholdMs; - final Handler handler; + final Handler uiHandler; + final Handler watchdogHandler; + private final HandlerThread watchdogHandlerThread; final Delegate delegate; volatile long lastUpdateMs; @@ -51,55 +56,66 @@ interface Delegate { this.checkIntervalMs = checkIntervalMs; this.looper = looper; this.delegate = delegate; - this.handler = new Handler(looper); - } + this.uiHandler = new Handler(looper); - void updateLivenessTimestamp() { - lastUpdateMs = SystemClock.elapsedRealtime(); + watchdogHandlerThread = new HandlerThread("bugsnag-anr-watchdog"); + watchdogHandlerThread.start(); + watchdogHandler = new Handler(watchdogHandlerThread.getLooper()); } void start() { updateLivenessTimestamp(); - handler.post(livenessCheck); - watcherThread.start(); + uiHandler.post(livenessCheck); + watchdogHandler.postDelayed(watchdogCheck, calculateNextCheckIn()); + } + + void updateLivenessTimestamp() { + lastUpdateMs = SystemClock.uptimeMillis(); } final Runnable livenessCheck = new Runnable() { @Override public void run() { updateLivenessTimestamp(); - handler.postDelayed(this, checkIntervalMs); + uiHandler.postDelayed(this, checkIntervalMs); } }; - final Thread watcherThread = new Thread() { + final Runnable watchdogCheck = new Runnable() { @Override public void run() { - while (!isInterrupted()) { - // when we would next consider the app blocked if no timestamp updates take place - long now = SystemClock.elapsedRealtime(); - long nextCheckIn = Math.max(lastUpdateMs + blockedThresholdMs - now, 0); - - try { - Thread.sleep(nextCheckIn); // throttle checks to the configured threshold - } catch (InterruptedException exc) { - interrupt(); - } - checkIfThreadBlocked(); - } + checkIfThreadBlocked(); + watchdogHandler.postDelayed(this, calculateNextCheckIn()); } + }; - private void checkIfThreadBlocked() { - long delta = SystemClock.elapsedRealtime() - lastUpdateMs; + long calculateNextCheckIn() { + long currentUptimeMs = SystemClock.uptimeMillis(); + return Math.max(lastUpdateMs + blockedThresholdMs - currentUptimeMs, 0); + } + + void checkIfThreadBlocked() { + long delta = SystemClock.uptimeMillis() - lastUpdateMs; + boolean inForeground = isInForeground(); - if (delta > blockedThresholdMs) { - if (!isAlreadyBlocked) { - delegate.onThreadBlocked(looper.getThread()); - } - isAlreadyBlocked = true; // prevents duplicate reports for the same ANR - } else { - isAlreadyBlocked = false; + if (inForeground && delta > blockedThresholdMs) { + if (!isAlreadyBlocked) { + delegate.onThreadBlocked(looper.getThread()); } + isAlreadyBlocked = true; // prevents duplicate reports for the same ANR + } else { + isAlreadyBlocked = false; } - }; + } + + private boolean isInForeground() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + ActivityManager.RunningAppProcessInfo info + = new ActivityManager.RunningAppProcessInfo(); + ActivityManager.getMyMemoryState(info); + return info.importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; + } else { + return true; + } + } }