diff --git a/res/values/strings.xml b/res/values/strings.xml index ce156b5a072..4e1b32eeed4 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7788,9 +7788,9 @@ Turn on bubbles - Before you can turn on bubbles for this app, you need to turn on bubbles for your device + 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. - Go to Bubbles + Turn on for device Cancel diff --git a/src/com/android/settings/notification/AppBubbleNotificationSettings.java b/src/com/android/settings/notification/AppBubbleNotificationSettings.java index 17909c0266e..f55c262e3e9 100644 --- a/src/com/android/settings/notification/AppBubbleNotificationSettings.java +++ b/src/com/android/settings/notification/AppBubbleNotificationSettings.java @@ -18,22 +18,23 @@ package com.android.settings.notification; 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() { @@ -64,6 +65,11 @@ public class AppBubbleNotificationSettings extends NotificationSettingsBase { return controllers; } + @Override + public void onGlobalBubblePermissionChanged() { + updatePreferenceStates(); + } + @Override public void onResume() { super.onResume(); @@ -79,19 +85,23 @@ public class AppBubbleNotificationSettings extends NotificationSettingsBase { 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 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 5dab37445b8..e5a1a6244e9 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.R; 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 @@ public class BubblePreferenceController extends NotificationPreferenceController 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 @@ public class BubblePreferenceController extends NotificationPreferenceController 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 @@ public class BubblePreferenceController extends NotificationPreferenceController 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 @@ public class BubblePreferenceController extends NotificationPreferenceController 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 708bbcded21..5f58f6757bf 100644 --- a/src/com/android/settings/notification/BubbleSummaryPreferenceController.java +++ b/src/com/android/settings/notification/BubbleSummaryPreferenceController.java @@ -52,11 +52,11 @@ public class BubbleSummaryPreferenceController extends NotificationPreferenceCon 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 @@ public class BubbleSummaryPreferenceController extends NotificationPreferenceCon 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 00000000000..5086fb044a5 --- /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 00000000000..398931dc518 --- /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 0c091b437a3..d94211384d7 100644 --- a/src/com/android/settings/notification/HeaderPreferenceController.java +++ b/src/com/android/settings/notification/HeaderPreferenceController.java @@ -68,9 +68,13 @@ public class HeaderPreferenceController extends NotificationPreferenceController 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 6d13798bd27..54bbd0874c7 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.any; 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.RobolectricTestRunner; 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 @@ public class BubblePreferenceControllerTest { private UserManager mUm; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private PreferenceScreen mScreen; + @Mock + private FragmentManager mFragmentManager; private BubblePreferenceController mController; @@ -78,7 +83,8 @@ public class BubblePreferenceControllerTest { 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 @@ public class BubblePreferenceControllerTest { } @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); @@ -242,6 +257,19 @@ public class BubblePreferenceControllerTest { assertTrue(pref.getSummary().toString().contains(appRow.label)); } + @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(); @@ -313,4 +341,23 @@ public class BubblePreferenceControllerTest { 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 5158e82e3b4..0a0addcbab4 100644 --- a/tests/robotests/src/com/android/settings/notification/BubbleSummaryPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/BubbleSummaryPreferenceControllerTest.java @@ -109,6 +109,15 @@ public class BubbleSummaryPreferenceControllerTest { assertTrue(mController.isAvailable()); } + @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(); @@ -141,6 +150,10 @@ public class BubbleSummaryPreferenceControllerTest { 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);