Unbind notification listener when badging is disabled

- Added SettingsObserver as wrapper around ContentObserver
  to observe Secure or System setting changes.
- NotificationListener and LauncherAppState observe changes
  to the notification dots setting and unbind and rebind
  the NotificationListener service, respectively.

Bug: 36815147
Change-Id: I2cc04ac816a8974969ad0ec759c5402e181fde24
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index cf20feb..1ffe41b 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import android.content.ComponentName;
 import android.content.ContentProviderClient;
 import android.content.Context;
 import android.content.Intent;
@@ -28,13 +29,17 @@
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dynamicui.ExtractionUtils;
+import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.util.ConfigMonitor;
 import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.SettingsObserver;
 import com.android.launcher3.util.TestingUtils;
 
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 
+import static com.android.launcher3.SettingsActivity.NOTIFICATION_BADGING;
+
 public class LauncherAppState {
 
     public static final boolean PROFILE_STARTUP = FeatureFlags.IS_DOGFOOD_BUILD;
@@ -47,7 +52,7 @@
     private final IconCache mIconCache;
     private final WidgetPreviewLoader mWidgetCache;
     private final InvariantDeviceProfile mInvariantDeviceProfile;
-
+    private final SettingsObserver mNotificationBadgingObserver;
 
     public static LauncherAppState getInstance(final Context context) {
         if (INSTANCE == null) {
@@ -117,6 +122,23 @@
         new ConfigMonitor(mContext).register();
 
         ExtractionUtils.startColorExtractionServiceIfNecessary(mContext);
+
+        if (!mContext.getResources().getBoolean(R.bool.notification_badging_enabled)) {
+            mNotificationBadgingObserver = null;
+        } else {
+            // Register an observer to rebind the notification listener when badging is re-enabled.
+            mNotificationBadgingObserver = new SettingsObserver.Secure(
+                    mContext.getContentResolver()) {
+                @Override
+                public void onSettingChanged(boolean isNotificationBadgingEnabled) {
+                    if (isNotificationBadgingEnabled) {
+                        NotificationListener.requestRebind(new ComponentName(
+                                mContext, NotificationListener.class));
+                    }
+                }
+            };
+            mNotificationBadgingObserver.register(NOTIFICATION_BADGING);
+        }
     }
 
     /**
@@ -127,6 +149,9 @@
         final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(mContext);
         launcherApps.removeOnAppsChangedCallback(mModel);
         PackageInstallerCompat.getInstance(mContext).onStop();
+        if (mNotificationBadgingObserver != null) {
+            mNotificationBadgingObserver.unregister();
+        }
     }
 
     LauncherModel setLauncher(Launcher launcher) {
diff --git a/src/com/android/launcher3/SettingsActivity.java b/src/com/android/launcher3/SettingsActivity.java
index fa7769e..9046372 100644
--- a/src/com/android/launcher3/SettingsActivity.java
+++ b/src/com/android/launcher3/SettingsActivity.java
@@ -26,17 +26,15 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
-import android.database.ContentObserver;
 import android.os.Bundle;
-import android.os.Handler;
 import android.preference.ListPreference;
 import android.preference.Preference;
 import android.preference.PreferenceFragment;
 import android.provider.Settings;
-import android.provider.Settings.System;
 
 import com.android.launcher3.graphics.IconShapeOverride;
 import com.android.launcher3.notification.NotificationListener;
+import com.android.launcher3.util.SettingsObserver;
 import com.android.launcher3.views.ButtonPreference;
 
 /**
@@ -45,8 +43,8 @@
 public class SettingsActivity extends Activity {
 
     private static final String ICON_BADGING_PREFERENCE_KEY = "pref_icon_badging";
-    // TODO: use Settings.Secure.NOTIFICATION_BADGING
-    private static final String NOTIFICATION_BADGING = "notification_badging";
+    /** Hidden field Settings.Secure.NOTIFICATION_BADGING */
+    public static final String NOTIFICATION_BADGING = "notification_badging";
     /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */
     private static final String NOTIFICATION_ENABLED_LISTENERS = "enabled_notification_listeners";
 
@@ -88,12 +86,9 @@
 
                 // Register a content observer to listen for system setting changes while
                 // this UI is active.
-                resolver.registerContentObserver(
-                        Settings.System.getUriFor(System.ACCELEROMETER_ROTATION),
-                        false, mRotationLockObserver);
+                mRotationLockObserver.register(Settings.System.ACCELEROMETER_ROTATION);
 
                 // Initialize the UI once
-                mRotationLockObserver.onChange(true);
                 rotationPref.setDefaultValue(Utilities.getAllowRotationDefaultValue(getActivity()));
             }
 
@@ -107,13 +102,7 @@
                 // Listen to system notification badge settings while this UI is active.
                 mIconBadgingObserver = new IconBadgingObserver(
                         iconBadgingPref, resolver, getFragmentManager());
-                resolver.registerContentObserver(
-                        Settings.Secure.getUriFor(NOTIFICATION_BADGING),
-                        false, mIconBadgingObserver);
-                resolver.registerContentObserver(
-                        Settings.Secure.getUriFor(NOTIFICATION_ENABLED_LISTENERS),
-                        false, mIconBadgingObserver);
-                mIconBadgingObserver.onChange(true);
+                mIconBadgingObserver.register(NOTIFICATION_BADGING, NOTIFICATION_ENABLED_LISTENERS);
             }
 
             Preference iconShapeOverride = findPreference(IconShapeOverride.KEY_PREFERENCE);
@@ -129,11 +118,11 @@
         @Override
         public void onDestroy() {
             if (mRotationLockObserver != null) {
-                getActivity().getContentResolver().unregisterContentObserver(mRotationLockObserver);
+                mRotationLockObserver.unregister();
                 mRotationLockObserver = null;
             }
             if (mIconBadgingObserver != null) {
-                getActivity().getContentResolver().unregisterContentObserver(mIconBadgingObserver);
+                mIconBadgingObserver.unregister();
                 mIconBadgingObserver = null;
             }
             super.onDestroy();
@@ -144,22 +133,18 @@
      * Content observer which listens for system auto-rotate setting changes, and enables/disables
      * the launcher rotation setting accordingly.
      */
-    private static class SystemDisplayRotationLockObserver extends ContentObserver {
+    private static class SystemDisplayRotationLockObserver extends SettingsObserver.System {
 
         private final Preference mRotationPref;
-        private final ContentResolver mResolver;
 
         public SystemDisplayRotationLockObserver(
                 Preference rotationPref, ContentResolver resolver) {
-            super(new Handler());
+            super(resolver);
             mRotationPref = rotationPref;
-            mResolver = resolver;
         }
 
         @Override
-        public void onChange(boolean selfChange) {
-            boolean enabled = Settings.System.getInt(mResolver,
-                    Settings.System.ACCELEROMETER_ROTATION, 1) == 1;
+        public void onSettingChanged(boolean enabled) {
             mRotationPref.setEnabled(enabled);
             mRotationPref.setSummary(enabled
                     ? R.string.allow_rotation_desc : R.string.allow_rotation_blocked_desc);
@@ -170,7 +155,7 @@
      * Content observer which listens for system badging setting changes,
      * and updates the launcher badging setting subtext accordingly.
      */
-    private static class IconBadgingObserver extends ContentObserver
+    private static class IconBadgingObserver extends SettingsObserver.Secure
             implements Preference.OnPreferenceClickListener {
 
         private final ButtonPreference mBadgingPref;
@@ -179,15 +164,14 @@
 
         public IconBadgingObserver(ButtonPreference badgingPref, ContentResolver resolver,
                 FragmentManager fragmentManager) {
-            super(new Handler());
+            super(resolver);
             mBadgingPref = badgingPref;
             mResolver = resolver;
             mFragmentManager = fragmentManager;
         }
 
         @Override
-        public void onChange(boolean selfChange) {
-            boolean enabled = Settings.Secure.getInt(mResolver, NOTIFICATION_BADGING, 1) == 1;
+        public void onSettingChanged(boolean enabled) {
             int summary = enabled ? R.string.icon_badging_desc_on : R.string.icon_badging_desc_off;
 
             boolean serviceEnabled = true;
diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
index 73d89aa..6a70989 100644
--- a/src/com/android/launcher3/notification/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -30,15 +30,20 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
+
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.SettingsObserver;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
+import static com.android.launcher3.SettingsActivity.NOTIFICATION_BADGING;
+
 /**
  * A {@link NotificationListenerService} that sends updates to its
  * {@link NotificationsChangedListener} when notifications are posted or canceled,
@@ -57,12 +62,14 @@
     private static NotificationListener sNotificationListenerInstance = null;
     private static NotificationsChangedListener sNotificationsChangedListener;
     private static boolean sIsConnected;
+    private static boolean sIsCreated;
 
     private final Handler mWorkerHandler;
     private final Handler mUiHandler;
-
     private final Ranking mTempRanking = new Ranking();
 
+    private SettingsObserver mNotificationBadgingObserver;
+
     private final Handler.Callback mWorkerCallback = new Handler.Callback() {
         @Override
         public boolean handleMessage(Message message) {
@@ -77,7 +84,7 @@
                     List<StatusBarNotification> activeNotifications;
                     if (sIsConnected) {
                         try {
-                            activeNotifications =  filterNotifications(getActiveNotifications());
+                            activeNotifications = filterNotifications(getActiveNotifications());
                         } catch (SecurityException ex) {
                             Log.e(TAG, "SecurityException: failed to fetch notifications");
                             activeNotifications = new ArrayList<StatusBarNotification>();
@@ -130,6 +137,28 @@
         sNotificationListenerInstance = this;
     }
 
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        sIsCreated = true;
+        mNotificationBadgingObserver = new SettingsObserver.Secure(getContentResolver()) {
+            @Override
+            public void onSettingChanged(boolean isNotificationBadgingEnabled) {
+                if (!isNotificationBadgingEnabled) {
+                    requestUnbind();
+                }
+            }
+        };
+        mNotificationBadgingObserver.register(NOTIFICATION_BADGING);
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        sIsCreated = false;
+        mNotificationBadgingObserver.unregister();
+    }
+
     public static @Nullable NotificationListener getInstanceIfConnected() {
         return sIsConnected ? sNotificationListenerInstance : null;
     }
@@ -143,6 +172,11 @@
         NotificationListener notificationListener = getInstanceIfConnected();
         if (notificationListener != null) {
             notificationListener.onNotificationFullRefresh();
+        } else if (!sIsCreated && sNotificationsChangedListener != null) {
+            // User turned off badging globally, so we unbound this service;
+            // tell the listener that there are no notifications to remove dots.
+            sNotificationsChangedListener.onNotificationFullRefresh(
+                    Collections.<StatusBarNotification>emptyList());
         }
     }
 
@@ -205,7 +239,7 @@
                 .getActiveNotifications(NotificationKeyData.extractKeysOnly(keys)
                         .toArray(new String[keys.size()]));
         return notifications == null
-            ? Collections.<StatusBarNotification>emptyList() : Arrays.asList(notifications);
+                ? Collections.<StatusBarNotification>emptyList() : Arrays.asList(notifications);
     }
 
     /**
diff --git a/src/com/android/launcher3/util/SettingsObserver.java b/src/com/android/launcher3/util/SettingsObserver.java
new file mode 100644
index 0000000..6baa242
--- /dev/null
+++ b/src/com/android/launcher3/util/SettingsObserver.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.provider.Settings;
+
+public interface SettingsObserver {
+
+    /**
+     * Registers the content observer to call {@link #onSettingChanged(boolean)} when any of the
+     * passed settings change. The value passed to onSettingChanged() is based on the key setting.
+     */
+    void register(String keySetting, String ... dependentSettings);
+    void unregister();
+    void onSettingChanged(boolean keySettingEnabled);
+
+
+    abstract class Secure extends ContentObserver implements SettingsObserver {
+        private ContentResolver mResolver;
+        private String mKeySetting;
+
+        public Secure(ContentResolver resolver) {
+            super(new Handler());
+            mResolver = resolver;
+        }
+
+        @Override
+        public void register(String keySetting, String ... dependentSettings) {
+            mKeySetting = keySetting;
+            mResolver.registerContentObserver(
+                    Settings.Secure.getUriFor(mKeySetting), false, this);
+            for (String setting : dependentSettings) {
+                mResolver.registerContentObserver(
+                        Settings.Secure.getUriFor(setting), false, this);
+            }
+            onChange(true);
+        }
+
+        @Override
+        public void unregister() {
+            mResolver.unregisterContentObserver(this);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            super.onChange(selfChange);
+            onSettingChanged(Settings.Secure.getInt(mResolver, mKeySetting, 1) == 1);
+        }
+    }
+
+    abstract class System extends ContentObserver implements SettingsObserver {
+        private ContentResolver mResolver;
+        private String mKeySetting;
+
+        public System(ContentResolver resolver) {
+            super(new Handler());
+            mResolver = resolver;
+        }
+
+        @Override
+        public void register(String keySetting, String ... dependentSettings) {
+            mKeySetting = keySetting;
+            mResolver.registerContentObserver(
+                    Settings.System.getUriFor(mKeySetting), false, this);
+            for (String setting : dependentSettings) {
+                mResolver.registerContentObserver(
+                        Settings.System.getUriFor(setting), false, this);
+            }
+            onChange(true);
+        }
+
+        @Override
+        public void unregister() {
+            mResolver.unregisterContentObserver(this);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            super.onChange(selfChange);
+            onSettingChanged(Settings.System.getInt(mResolver, mKeySetting, 1) == 1);
+        }
+    }
+}