Merge "Add bubble confirmation prompt" into qt-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
208c21d2c1
@@ -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>
|
||||
|
||||
|
@@ -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 =
|
||||
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);
|
||||
public void onPause() {
|
||||
mObserverMixin.onStop();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
|
||||
new BaseSearchIndexProvider() {
|
||||
|
||||
@Override
|
||||
protected boolean isPageSearchEnabled(Context context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -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 (mChannel != null) {
|
||||
if (Settings.Secure.getInt(mContext.getContentResolver(),
|
||||
NOTIFICATION_BUBBLES, SYSTEM_WIDE_ON) == SYSTEM_WIDE_OFF) {
|
||||
return false;
|
||||
}
|
||||
if (mChannel != null) {
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@@ -52,11 +52,11 @@ public class BubbleSummaryPreferenceController extends NotificationPreferenceCon
|
||||
if (mAppRow == null && mChannel == null) {
|
||||
return false;
|
||||
}
|
||||
if (mChannel != null) {
|
||||
if (Settings.Secure.getInt(mContext.getContentResolver(),
|
||||
NOTIFICATION_BUBBLES, SYSTEM_WIDE_ON) == SYSTEM_WIDE_OFF) {
|
||||
return false;
|
||||
}
|
||||
if (mChannel != null) {
|
||||
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);
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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 */);
|
||||
}
|
||||
}
|
@@ -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())
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
||||
|
Reference in New Issue
Block a user