Merge "Add bubble confirmation prompt" into qt-dev
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ce156b5..4e1b32e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -7788,9 +7788,9 @@
     <!-- Title of the dialog shown when the user has disabled bubbles at the feature level but tries to enable it for an app. [CHAR LIMIT=NONE] -->
     <string name="bubbles_feature_disabled_dialog_title">Turn on bubbles</string>
     <!-- Description of the dialog shown when the user has disabled bubbles at the feature level but tries to enable it for an app. [CHAR LIMIT=NONE] -->
-    <string name="bubbles_feature_disabled_dialog_text">Before you can turn on bubbles for this app, you need to turn on bubbles for your device</string>
+    <string name="bubbles_feature_disabled_dialog_text">To turn on bubbles for this app, first you need to turn them on for your device. This affects other apps in which you previously turned on bubbles.</string>
     <!-- Button of the dialog shown when the user has disabled bubbles at the feature level but tries to enable it for an app. [CHAR LIMIT=60]-->
-    <string name="bubbles_feature_disabled_button_go_to_bubbles">Go to Bubbles</string>
+    <string name="bubbles_feature_disabled_button_approve">Turn on for device</string>
     <!-- Button to cancel out of the dialog shown when the user has disabled bubbles at the feature level but tries to enable it for an app. [CHAR LIMIT=60] -->
     <string name="bubbles_feature_disabled_button_cancel">Cancel</string>
 
diff --git a/src/com/android/settings/notification/AppBubbleNotificationSettings.java b/src/com/android/settings/notification/AppBubbleNotificationSettings.java
index 17909c0..f55c262 100644
--- a/src/com/android/settings/notification/AppBubbleNotificationSettings.java
+++ b/src/com/android/settings/notification/AppBubbleNotificationSettings.java
@@ -18,22 +18,23 @@
 
 import android.app.settings.SettingsEnums;
 import android.content.Context;
-import android.provider.SearchIndexableResource;
 import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.settings.R;
 import com.android.settings.search.BaseSearchIndexProvider;
+import com.android.settings.search.Indexable;
 import com.android.settingslib.core.AbstractPreferenceController;
 import com.android.settingslib.search.SearchIndexable;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 
 @SearchIndexable
-public class AppBubbleNotificationSettings extends NotificationSettingsBase {
+public class AppBubbleNotificationSettings extends NotificationSettingsBase implements
+        GlobalBubblePermissionObserverMixin.Listener {
     private static final String TAG = "AppBubNotiSettings";
+    private GlobalBubblePermissionObserverMixin mObserverMixin;
 
     @Override
     public int getMetricsCategory() {
@@ -65,6 +66,11 @@
     }
 
     @Override
+    public void onGlobalBubblePermissionChanged() {
+        updatePreferenceStates();
+    }
+
+    @Override
     public void onResume() {
         super.onResume();
 
@@ -79,19 +85,23 @@
             controller.displayPreference(getPreferenceScreen());
         }
         updatePreferenceStates();
+
+        mObserverMixin = new GlobalBubblePermissionObserverMixin(getContext(), this);
+        mObserverMixin.onStart();
     }
 
-    /**
-     * For Search.
-     */
-    public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
+    @Override
+    public void onPause() {
+        mObserverMixin.onStop();
+        super.onPause();
+    }
+
+    public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
             new BaseSearchIndexProvider() {
+
                 @Override
-                public List<SearchIndexableResource> getXmlResourcesToIndex(
-                        Context context, boolean enabled) {
-                    final SearchIndexableResource sir = new SearchIndexableResource(context);
-                    sir.xmlResId = R.xml.app_bubble_notification_settings;
-                    return Arrays.asList(sir);
+                protected boolean isPageSearchEnabled(Context context) {
+                    return false;
                 }
 
                 @Override
diff --git a/src/com/android/settings/notification/BubblePreferenceController.java b/src/com/android/settings/notification/BubblePreferenceController.java
index 5dab374..e5a1a62 100644
--- a/src/com/android/settings/notification/BubblePreferenceController.java
+++ b/src/com/android/settings/notification/BubblePreferenceController.java
@@ -25,6 +25,7 @@
 import com.android.settings.core.PreferenceControllerMixin;
 import com.android.settingslib.RestrictedSwitchPreference;
 
+import androidx.fragment.app.FragmentManager;
 import androidx.preference.Preference;
 
 public class BubblePreferenceController extends NotificationPreferenceController
@@ -35,10 +36,18 @@
     private static final int SYSTEM_WIDE_ON = 1;
     private static final int SYSTEM_WIDE_OFF = 0;
 
+    private FragmentManager mFragmentManager;
+
     public BubblePreferenceController(Context context, NotificationBackend backend) {
         super(context, backend);
     }
 
+    public BubblePreferenceController(Context context, FragmentManager fragmentManager,
+            NotificationBackend backend) {
+        super(context, backend);
+        mFragmentManager = fragmentManager;
+    }
+
     @Override
     public String getPreferenceKey() {
         return KEY;
@@ -52,11 +61,11 @@
         if (mAppRow == null && mChannel == null) {
             return false;
         }
-        if (Settings.Secure.getInt(mContext.getContentResolver(),
-                NOTIFICATION_BUBBLES, SYSTEM_WIDE_ON) == SYSTEM_WIDE_OFF) {
-            return false;
-        }
         if (mChannel != null) {
+            if (Settings.Secure.getInt(mContext.getContentResolver(),
+                    NOTIFICATION_BUBBLES, SYSTEM_WIDE_ON) == SYSTEM_WIDE_OFF) {
+                return false;
+            }
             if (isDefaultChannel()) {
                 return true;
             } else {
@@ -74,7 +83,9 @@
                 pref.setChecked(mChannel.canBubble());
                 pref.setEnabled(isChannelConfigurable() && !pref.isDisabledByAdmin());
             } else {
-                pref.setChecked(mAppRow.allowBubbles);
+                pref.setChecked(mAppRow.allowBubbles
+                        && Settings.Secure.getInt(mContext.getContentResolver(),
+                        NOTIFICATION_BUBBLES, SYSTEM_WIDE_ON) == SYSTEM_WIDE_ON);
                 pref.setSummary(mContext.getString(
                         R.string.bubbles_app_toggle_summary, mAppRow.label));
             }
@@ -87,11 +98,44 @@
         if (mChannel != null) {
             mChannel.setAllowBubbles(value);
             saveChannel();
-        } else if (mAppRow != null){
-            mAppRow.allowBubbles = value;
-            mBackend.setAllowBubbles(mAppRow.pkg, mAppRow.uid, value);
+            return true;
+        } else if (mAppRow != null) {
+            RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference;
+            // if the global setting is off, toggling app level permission requires extra
+            // confirmation
+            if (Settings.Secure.getInt(mContext.getContentResolver(),
+                    NOTIFICATION_BUBBLES, SYSTEM_WIDE_ON) == SYSTEM_WIDE_OFF
+                    && !pref.isChecked()) {
+                new BubbleWarningDialogFragment()
+                        .setPkgInfo(mAppRow.pkg, mAppRow.uid)
+                        .show(mFragmentManager, "dialog");
+                return false;
+            } else {
+                mAppRow.allowBubbles = value;
+                mBackend.setAllowBubbles(mAppRow.pkg, mAppRow.uid, value);
+            }
         }
         return true;
     }
 
+    // Used in app level prompt that confirms the user is ok with turning on bubbles
+    // globally. If they aren't, undo what
+    public static void revertBubblesApproval(Context mContext, String pkg, int uid) {
+        NotificationBackend backend = new NotificationBackend();
+        backend.setAllowBubbles(pkg, uid, false);
+        // changing the global settings will cause the observer on the host page to reload
+        // correct preference state
+        Settings.Secure.putInt(mContext.getContentResolver(),
+                NOTIFICATION_BUBBLES, SYSTEM_WIDE_OFF);
+    }
+
+    // Apply global bubbles approval
+    public static void applyBubblesApproval(Context mContext, String pkg, int uid) {
+        NotificationBackend backend = new NotificationBackend();
+        backend.setAllowBubbles(pkg, uid, true);
+        // changing the global settings will cause the observer on the host page to reload
+        // correct preference state
+        Settings.Secure.putInt(mContext.getContentResolver(),
+                NOTIFICATION_BUBBLES, SYSTEM_WIDE_ON);
+    }
 }
diff --git a/src/com/android/settings/notification/BubbleSummaryPreferenceController.java b/src/com/android/settings/notification/BubbleSummaryPreferenceController.java
index 708bbcd..5f58f67 100644
--- a/src/com/android/settings/notification/BubbleSummaryPreferenceController.java
+++ b/src/com/android/settings/notification/BubbleSummaryPreferenceController.java
@@ -52,11 +52,11 @@
         if (mAppRow == null && mChannel == null) {
             return false;
         }
-        if (Settings.Secure.getInt(mContext.getContentResolver(),
-                NOTIFICATION_BUBBLES, SYSTEM_WIDE_ON) == SYSTEM_WIDE_OFF) {
-            return false;
-        }
         if (mChannel != null) {
+            if (Settings.Secure.getInt(mContext.getContentResolver(),
+                    NOTIFICATION_BUBBLES, SYSTEM_WIDE_ON) == SYSTEM_WIDE_OFF) {
+                return false;
+            }
             if (isDefaultChannel()) {
                 return true;
             } else {
@@ -91,7 +91,9 @@
             if (mChannel != null) {
                 canBubble |= mChannel.canBubble();
             } else {
-               canBubble |= mAppRow.allowBubbles;
+               canBubble |= mAppRow.allowBubbles
+                       && (Settings.Secure.getInt(mContext.getContentResolver(),
+                       NOTIFICATION_BUBBLES, SYSTEM_WIDE_ON) == SYSTEM_WIDE_ON);
             }
         }
         return mContext.getString(canBubble ? R.string.switch_on_text : R.string.switch_off_text);
diff --git a/src/com/android/settings/notification/BubbleWarningDialogFragment.java b/src/com/android/settings/notification/BubbleWarningDialogFragment.java
new file mode 100644
index 0000000..5086fb0
--- /dev/null
+++ b/src/com/android/settings/notification/BubbleWarningDialogFragment.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 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.settings.notification;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.os.Bundle;
+
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+public class BubbleWarningDialogFragment extends InstrumentedDialogFragment {
+    static final String KEY_PKG = "p";
+    static final String KEY_UID = "u";
+
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.DIALOG_APP_BUBBLE_SETTINGS;
+    }
+
+    public BubbleWarningDialogFragment setPkgInfo(String pkg, int uid) {
+        Bundle args = new Bundle();
+        args.putString(KEY_PKG, pkg);
+        args.putInt(KEY_UID, uid);
+        setArguments(args);
+        return this;
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        final Bundle args = getArguments();
+        final String pkg = args.getString(KEY_PKG);
+        final int uid = args.getInt(KEY_UID);
+
+        final String title =
+                getResources().getString(R.string.bubbles_feature_disabled_dialog_title);
+        final String summary = getResources()
+                .getString(R.string.bubbles_feature_disabled_dialog_text);
+        return new AlertDialog.Builder(getContext())
+                .setMessage(summary)
+                .setTitle(title)
+                .setCancelable(true)
+                .setPositiveButton(R.string.bubbles_feature_disabled_button_approve,
+                        (dialog, id) ->
+                                BubblePreferenceController.applyBubblesApproval(
+                                        getContext(), pkg, uid))
+                .setNegativeButton(R.string.bubbles_feature_disabled_button_cancel,
+                        (dialog, id) ->
+                                BubblePreferenceController.revertBubblesApproval(
+                                        getContext(), pkg, uid))
+                .create();
+    }
+}
diff --git a/src/com/android/settings/notification/GlobalBubblePermissionObserverMixin.java b/src/com/android/settings/notification/GlobalBubblePermissionObserverMixin.java
new file mode 100644
index 0000000..398931d
--- /dev/null
+++ b/src/com/android/settings/notification/GlobalBubblePermissionObserverMixin.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2019 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.settings.notification;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+
+public class GlobalBubblePermissionObserverMixin extends ContentObserver {
+
+    public interface Listener {
+        void onGlobalBubblePermissionChanged();
+    }
+
+    private final Context mContext;
+    private final Listener mListener;
+
+    public GlobalBubblePermissionObserverMixin(Context context, Listener listener) {
+        super(new Handler(Looper.getMainLooper()));
+        mContext = context;
+        mListener = listener;
+    }
+
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        if (mListener != null) {
+            mListener.onGlobalBubblePermissionChanged();
+        }
+    }
+
+    public void onStart() {
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(
+                        Settings.Secure.NOTIFICATION_BUBBLES),
+                false /* notifyForDescendants */,
+                this /* observer */);
+    }
+
+    public void onStop() {
+        mContext.getContentResolver().unregisterContentObserver(this /* observer */);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/notification/HeaderPreferenceController.java b/src/com/android/settings/notification/HeaderPreferenceController.java
index 0c091b4..d942113 100644
--- a/src/com/android/settings/notification/HeaderPreferenceController.java
+++ b/src/com/android/settings/notification/HeaderPreferenceController.java
@@ -68,9 +68,13 @@
                 activity = mFragment.getActivity();
             }
 
+            if (activity == null) {
+                return;
+            }
+
             LayoutPreference pref = (LayoutPreference) preference;
             mHeaderController = EntityHeaderController.newInstance(
-                    mFragment.getActivity(), mFragment, pref.findViewById(R.id.entity_header));
+                    activity, mFragment, pref.findViewById(R.id.entity_header));
             pref = mHeaderController.setIcon(mAppRow.icon)
                     .setLabel(getLabel())
                     .setSummary(getSummary())
diff --git a/tests/robotests/src/com/android/settings/notification/BubblePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/BubblePreferenceControllerTest.java
index 6d13798..54bbd08 100644
--- a/tests/robotests/src/com/android/settings/notification/BubblePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/BubblePreferenceControllerTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -53,6 +54,8 @@
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.shadows.ShadowApplication;
 
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceScreen;
 
@@ -68,6 +71,8 @@
     private UserManager mUm;
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private PreferenceScreen mScreen;
+    @Mock
+    private FragmentManager mFragmentManager;
 
     private BubblePreferenceController mController;
 
@@ -78,7 +83,8 @@
         shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm);
         shadowApplication.setSystemService(Context.USER_SERVICE, mUm);
         mContext = RuntimeEnvironment.application;
-        mController = spy(new BubblePreferenceController(mContext, mBackend));
+        when(mFragmentManager.beginTransaction()).thenReturn(mock(FragmentTransaction.class));
+        mController = spy(new BubblePreferenceController(mContext, mFragmentManager, mBackend));
     }
 
     @Test
@@ -117,7 +123,16 @@
     }
 
     @Test
-    public void testIsAvailable_notIfOffGlobally() {
+    public void testIsAvailable_ifOffGlobally_app() {
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        mController.onResume(appRow, null, null, null);
+        Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 0);
+
+        assertTrue(mController.isAvailable());
+    }
+
+    @Test
+    public void testIsAvailable_notIfOffGlobally_channel() {
         NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
         NotificationChannel channel = mock(NotificationChannel.class);
         when(channel.getImportance()).thenReturn(IMPORTANCE_HIGH);
@@ -243,6 +258,19 @@
     }
 
     @Test
+    public void testUpdateState_app_offGlobally() {
+        Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 0);
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.label = "App!";
+        appRow.allowBubbles = true;
+        mController.onResume(appRow, null, null, null);
+
+        RestrictedSwitchPreference pref = new RestrictedSwitchPreference(mContext);
+        mController.updateState(pref);
+        assertFalse(pref.isChecked());
+    }
+
+    @Test
     public void testOnPreferenceChange_on_channel() {
         NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
         appRow.allowBubbles = true;
@@ -313,4 +341,23 @@
         assertFalse(appRow.allowBubbles);
         verify(mBackend, times(1)).setAllowBubbles(any(), anyInt(), eq(false));
     }
+
+    @Test
+    public void testOnPreferenceChange_on_app_offGlobally() {
+        Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 0);
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.allowBubbles = false;
+        mController.onResume(appRow, null, null, null);
+
+        RestrictedSwitchPreference pref = new RestrictedSwitchPreference(mContext);
+        when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(pref);
+        mController.displayPreference(mScreen);
+        mController.updateState(pref);
+
+        mController.onPreferenceChange(pref, true);
+
+        assertFalse(appRow.allowBubbles);
+        verify(mBackend, never()).setAllowBubbles(any(), anyInt(), eq(true));
+        verify(mFragmentManager, times(1)).beginTransaction();
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/notification/BubbleSummaryPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/BubbleSummaryPreferenceControllerTest.java
index 5158e82..0a0addc 100644
--- a/tests/robotests/src/com/android/settings/notification/BubbleSummaryPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/BubbleSummaryPreferenceControllerTest.java
@@ -110,6 +110,15 @@
     }
 
     @Test
+    public void testIsAvailable_app_globalOff() {
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        mController.onResume(appRow, null, null, null);
+        Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 0);
+
+        assertTrue(mController.isAvailable());
+    }
+
+    @Test
     public void testIsAvailable_defaultChannel() {
         NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
         appRow.allowBubbles = true;
@@ -141,6 +150,10 @@
 
         assertEquals("On", mController.getSummary());
 
+        Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 0);
+        assertEquals("Off", mController.getSummary());
+
+        Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 1);
         appRow.allowBubbles = false;
         mController.onResume(appRow, null, null, null);