Bug 1550291 - Android Q: Use the overlay permission or a foreground notification to start the crash handler; r=VladBaicu
authorPetru Lingurar <petru.lingurar@softvision.ro>
Mon, 10 Jun 2019 15:52:58 +0000
changeset 478103 53931ee8bb199c8daf59ff554c1aee1e1afc3828
parent 478102 d7f44087b9a52623abf61f73f4b9a82d97945756
child 478104 e69430c126d73a919b73ed81002d690dea888358
push id113405
push usernerli@mozilla.com
push dateTue, 11 Jun 2019 03:22:35 +0000
treeherdermozilla-inbound@e3bbbcf873c2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersVladBaicu
bugs1550291
milestone69.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1550291 - Android Q: Use the overlay permission or a foreground notification to start the crash handler; r=VladBaicu Android Q doesn't allow starting Activities from background. https://developer.android.com/preview/privacy/background-activity-starts We can either piggy-back the SYSTEM_ALERT_WINDOW permission given by the user for the Tab Queue functionality or use a foreground notification from where users could start `CrashReporterActivity`. Differential Revision: https://phabricator.services.mozilla.com/D33029
mobile/android/app/src/main/res/values/ids.xml
mobile/android/base/java/org/mozilla/gecko/CrashHandlerService.java
mobile/android/base/java/org/mozilla/gecko/CrashReporterActivity.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/strings.xml.in
--- a/mobile/android/app/src/main/res/values/ids.xml
+++ b/mobile/android/app/src/main/res/values/ids.xml
@@ -18,9 +18,10 @@
     <item type="id" name="pref_header_search"/>
     <item type="id" name="updateServicePermissionNotification" />
     <item type="id" name="foregroundNotification" />
     <item type="id" name="stumblerNotification" />
     <item type="id" name="actionbar"/>
     <item type="id" name="action_button"/>
     <item type="id" name="page_progress"/>
     <item type="id" name="mediaControlNotification"/>
+    <item type="id" name="crashReporterNotification"/>
 </resources>
--- a/mobile/android/base/java/org/mozilla/gecko/CrashHandlerService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/CrashHandlerService.java
@@ -1,23 +1,92 @@
 package org.mozilla.gecko;
 
-import android.app.IntentService;
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
 import android.content.Intent;
-import android.support.annotation.Nullable;
+import android.os.IBinder;
+import android.provider.Settings;
+
+import org.mozilla.gecko.notifications.NotificationHelper;
 
+public class CrashHandlerService extends Service {
+    private static final String ACTION_STOP = "action_stop";
+    // Build.VERSION_CODES.Q placeholder. While Android Q is in Beta it shares API 28 with Android P.
+    private static final int ANDROID_Q = 29;
 
-public class CrashHandlerService extends IntentService {
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (ACTION_STOP.equals(intent.getAction())) {
+            dismissNotification();
+        } else {
+            intent.setClass(this, CrashReporterActivity.class);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 
-    public CrashHandlerService() {
-        super("Crash Handler");
+            if (AppConstants.Versions.feature29Plus) {
+                startCrashHandling(intent);
+            } else {
+                startActivity(intent);
+
+                // Avoid ANR due to background limitations on Oreo+
+                System.exit(0);
+            }
+        }
+
+        return Service.START_NOT_STICKY;
     }
 
     @Override
-    protected void onHandleIntent(@Nullable Intent intent) {
-        intent.setClass(this, CrashReporterActivity.class);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        startActivity(intent);
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    /**
+     * Call this for any necessary cleanup like removing the foreground notification shown on Android Q+.
+     */
+    public static void reportingStarted(final Context context) {
+        if (AppConstants.Versions.feature29Plus) {
+            final Intent intent = new Intent(context, CrashHandlerService.class);
+            intent.setAction(ACTION_STOP);
+            context.startService(intent);
+        }
+    }
+
+    @TargetApi(ANDROID_Q)
+    private Notification getActivityNotification(final Context context, final Intent activityIntent) {
+        final Intent dismissNotificationIntent = new Intent(ACTION_STOP, null, this, this.getClass());
+        final PendingIntent dismissNotification = PendingIntent.getService(this, 0, dismissNotificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+        final PendingIntent startReporterActivity = PendingIntent.getActivity(this, 0, activityIntent, 0);
+        final String notificationChannelId = NotificationHelper.getInstance(context)
+                .getNotificationChannel(NotificationHelper.Channel.CRASH_HANDLER).getId();
 
-        // Avoid ANR due to background limitations on Oreo+
-        System.exit(0);
+        return new Notification.Builder(this, notificationChannelId)
+                .setSmallIcon(R.drawable.ic_status_logo)
+                .setContentTitle(getString(R.string.crash_notification_title))
+                .setContentText(getString(R.string.crash_notification_message))
+                .setDefaults(Notification.DEFAULT_ALL)
+                .setContentIntent(startReporterActivity)
+                .addAction(0, getString(R.string.crash_notification_negative_button_text), dismissNotification)
+                .build();
+    }
+
+    @TargetApi(ANDROID_Q)
+    private void dismissNotification() {
+        stopForeground(Service.STOP_FOREGROUND_REMOVE);
+    }
+
+    @TargetApi(ANDROID_Q)
+    private void startCrashHandling(final Intent activityIntent) {
+        // Piggy-back the SYSTEM_ALERT_WINDOW permission given by the user for the Tab Queue functionality.
+        // Otherwise fallback to display a foreground notification, this being the only way we can
+        // start an activity from background.
+        // https://developer.android.com/preview/privacy/background-activity-starts#conditions-allow-activity-starts
+        if (Settings.canDrawOverlays(this)) {
+            startActivity(activityIntent);
+        } else {
+            final Notification notification = getActivityNotification(this, activityIntent);
+            startForeground(R.id.mediaControlNotification, notification);
+        }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/CrashReporterActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/CrashReporterActivity.java
@@ -134,16 +134,18 @@ public class CrashReporterActivity exten
             Log.e(LOGTAG, "exception while closing progress dialog: ", e);
         }
         super.finish();
     }
 
     @Override
     @SuppressLint("WrongThread") // We don't have a worker thread for the TelemetryDispatcher
     public void onCreate(Bundle savedInstanceState) {
+        informReporterStarted();
+
         super.onCreate(savedInstanceState);
         // mHandler is created here so runnables can be run on the main thread
         mHandler = new Handler();
         setContentView(R.layout.crash_reporter);
         mProgressDialog = new ProgressDialog(this);
         mProgressDialog.setMessage(getString(R.string.sending_crash_report));
 
         mMinidumpSucceeded = getIntent().getBooleanExtra(PASSED_MINI_DUMP_SUCCESS_KEY, false);
@@ -591,9 +593,16 @@ public class CrashReporterActivity exten
         } catch (Exception e) {
             Log.e(LOGTAG, "error while trying to restart", e);
         }
     }
 
     private String unescape(String string) {
         return string.replaceAll("\\\\\\\\", "\\").replaceAll("\\\\n", "\n").replaceAll("\\\\t", "\t");
     }
+
+    /**
+     * Inform other parts of the app that user started the crash reporting process.
+     */
+    private void informReporterStarted() {
+        CrashHandlerService.reportingStarted(this);
+    }
 }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -63,16 +63,21 @@
 <!ENTITY  crash_sorry "We\'re sorry">
 <!ENTITY  crash_comment "Add a comment (comments are publicly visible)">
 <!ENTITY  crash_allow_contact2 "Allow &vendorShortName; to contact me about this report">
 <!ENTITY  crash_email "Your email">
 <!ENTITY  crash_closing_alert "Exit without sending a crash report?">
 <!ENTITY  sending_crash_report "Sending crash report\u2026">
 <!ENTITY  crash_close_label "Close">
 <!ENTITY  crash_restart_label "Restart &brandShortName;">
+<!-- Localization note (crash_notification_title, crash_notification_message, crash_notification_negative_button_text)
+     Text displayed in a system notification to allow starting the Crash Reporter (Android Q and later).-->
+<!ENTITY  crash_notification_title "&brandShortName; crashed">
+<!ENTITY  crash_notification_message "Tap to report to &vendorShortName;">
+<!ENTITY  crash_notification_negative_button_text "Ignore">
 
 <!ENTITY url_bar_default_text2 "Search or enter address">
 
 <!-- Localization note: this text will be displayed in the Search Widget -->
 <!ENTITY search_widget_default_text "Search the web">
 <!ENTITY search_widget_default_cropped_text "Search">
 <!ENTITY search_widget_logo_description "Search the web with &brandShortName;">
 
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -66,16 +66,19 @@
   <string name="crash_sorry">&crash_sorry;</string>
   <string name="crash_comment">&crash_comment;</string>
   <string name="crash_allow_contact2">&crash_allow_contact2;</string>
   <string name="crash_email">&crash_email;</string>
   <string name="crash_closing_alert">&crash_closing_alert;</string>
   <string name="sending_crash_report">&sending_crash_report;</string>
   <string name="crash_close_label">&crash_close_label;</string>
   <string name="crash_restart_label">&crash_restart_label;</string>
+  <string name="crash_notification_title">&crash_notification_title;</string>
+  <string name="crash_notification_message">&crash_notification_message;</string>
+  <string name="crash_notification_negative_button_text">&crash_notification_negative_button_text;</string>
 
   <string name="url_bar_default_text">&url_bar_default_text2;</string>
   <string name="url_bar_qrcode_text">&url_bar_qrcode_text2;</string>
   <string name="url_bar_mic_text">&url_bar_mic_text2;</string>
 
   <string name="search_widget_default_text">&search_widget_default_text;</string>
   <string name="search_widget_default_cropped_text">&search_widget_default_cropped_text;</string>
   <string name="search_widget_logo_description">&search_widget_logo_description;</string>