Merge "Add bubble confirmation prompt" into qt-dev

This commit is contained in:
TreeHugger Robot
2019-04-05 15:26:24 +00:00
committed by Android (Google) Code Review
9 changed files with 279 additions and 30 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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 */);
}
}

View File

@@ -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())

View File

@@ -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();
}
}

View File

@@ -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);