From 1cda00b12bfffbb0f3aa0a94e8758235164c54f6 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Mon, 26 Mar 2018 17:02:58 -0400 Subject: [PATCH] Allow app lists to have a toggle And make the notifications app list toggle block/unblock notifications. Test: make -j RunSettingsRoboTests Change-Id: I7417b98fb1c66504897df515c4c84afad591764e Fixes: 74318867 --- .../AppStateNotificationBridge.java | 66 ++++++++++++++++++- .../ApplicationViewHolder.java | 35 +++++++++- .../ManageApplications.java | 34 +++++++++- .../notification/NotificationBackend.java | 21 +++++- .../AppStateNotificationBridgeTest.java | 49 +++++++++++++- .../ApplicationViewHolderTest.java | 27 ++++++++ .../ManageApplicationsTest.java | 38 +++++++++++ 7 files changed, 259 insertions(+), 11 deletions(-) diff --git a/src/com/android/settings/applications/AppStateNotificationBridge.java b/src/com/android/settings/applications/AppStateNotificationBridge.java index 238c135df75..a96a3b1ad7b 100644 --- a/src/com/android/settings/applications/AppStateNotificationBridge.java +++ b/src/com/android/settings/applications/AppStateNotificationBridge.java @@ -20,8 +20,12 @@ import android.app.usage.UsageStatsManager; import android.content.Context; import android.text.format.DateUtils; import android.util.ArrayMap; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Switch; import com.android.settings.R; +import com.android.settings.notification.NotificationBackend; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.applications.ApplicationsState.AppFilter; @@ -37,13 +41,18 @@ import java.util.Map; */ public class AppStateNotificationBridge extends AppStateBaseBridge { + private final Context mContext; private UsageStatsManager mUsageStatsManager; + private NotificationBackend mBackend; private static final int DAYS_TO_CHECK = 7; - public AppStateNotificationBridge(ApplicationsState appState, - Callback callback, UsageStatsManager usageStatsManager) { + public AppStateNotificationBridge(Context context, ApplicationsState appState, + Callback callback, UsageStatsManager usageStatsManager, + NotificationBackend backend) { super(appState, callback); + mContext = context; mUsageStatsManager = usageStatsManager; + mBackend = backend; } @Override @@ -55,6 +64,7 @@ public class AppStateNotificationBridge extends AppStateBaseBridge { for (AppEntry entry : apps) { NotificationsSentState stats = map.get(entry.info.packageName); calculateAvgSentCounts(stats); + addBlockStatus(entry, stats); entry.extraInfo = stats; } } @@ -64,6 +74,7 @@ public class AppStateNotificationBridge extends AppStateBaseBridge { Map map = getAggregatedUsageEvents(); NotificationsSentState stats = map.get(entry.info.packageName); calculateAvgSentCounts(stats); + addBlockStatus(entry, stats); entry.extraInfo = stats; } @@ -83,6 +94,14 @@ public class AppStateNotificationBridge extends AppStateBaseBridge { } } + private void addBlockStatus(AppEntry entry, NotificationsSentState stats) { + if (stats != null) { + stats.blocked = mBackend.getNotificationsBanned(entry.info.packageName, entry.info.uid); + stats.systemApp = mBackend.isSystemApp(mContext, entry.info); + stats.blockable = !stats.systemApp || (stats.systemApp && stats.blocked); + } + } + private void calculateAvgSentCounts(NotificationsSentState stats) { if (stats != null) { stats.avgSentDaily = Math.round((float) stats.sentCount / DAYS_TO_CHECK); @@ -130,6 +149,28 @@ public class AppStateNotificationBridge extends AppStateBaseBridge { return null; } + public View.OnClickListener getSwitchOnClickListener(final AppEntry entry) { + if (entry != null) { + return v -> { + ViewGroup view = (ViewGroup) v; + Switch toggle = view.findViewById(R.id.switchWidget); + if (toggle != null) { + if (!toggle.isEnabled()) { + return; + } + toggle.toggle(); + mBackend.setNotificationsEnabledForPackage( + entry.info.packageName, entry.info.uid, toggle.isChecked()); + NotificationsSentState stats = getNotificationsSentState(entry); + if (stats != null) { + stats.blocked = !toggle.isChecked(); + } + } + }; + } + return null; + } + public static final AppFilter FILTER_APP_NOTIFICATION_RECENCY = new AppFilter() { @Override public void init() { @@ -192,6 +233,24 @@ public class AppStateNotificationBridge extends AppStateBaseBridge { } }; + public static final boolean enableSwitch(AppEntry entry) { + NotificationsSentState stats = getNotificationsSentState(entry); + if (stats == null) { + return false; + } + + return stats.blockable; + } + + public static final boolean checkSwitch(AppEntry entry) { + NotificationsSentState stats = getNotificationsSentState(entry); + if (stats == null) { + return false; + } + + return !stats.blocked; + } + /** * NotificationsSentState contains how often an app sends notifications and how recently it sent * one. @@ -201,5 +260,8 @@ public class AppStateNotificationBridge extends AppStateBaseBridge { public int avgSentWeekly = 0; public long lastSent = 0; public int sentCount = 0; + public boolean blockable; + public boolean blocked; + public boolean systemApp; } } diff --git a/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java b/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java index e968b1c38ab..f7b41a6bfc4 100644 --- a/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java +++ b/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java @@ -28,6 +28,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.Switch; import android.widget.TextView; import com.android.settings.R; @@ -47,7 +48,10 @@ public class ApplicationViewHolder extends RecyclerView.ViewHolder { final TextView mSummary; @VisibleForTesting final TextView mDisabled; - + @VisibleForTesting + final ViewGroup mWidgetContainer; + @VisibleForTesting + final Switch mSwitch; ApplicationViewHolder(View itemView, boolean keepStableHeight) { super(itemView); @@ -57,11 +61,30 @@ public class ApplicationViewHolder extends RecyclerView.ViewHolder { mSummary = itemView.findViewById(android.R.id.summary); mDisabled = itemView.findViewById(R.id.appendix); mKeepStableHeight = keepStableHeight; + mSwitch = itemView.findViewById(R.id.switchWidget); + mWidgetContainer = itemView.findViewById(android.R.id.widget_frame); } static View newView(ViewGroup parent) { - return LayoutInflater.from(parent.getContext()) + return newView(parent, false); + } + + static View newView(ViewGroup parent, boolean twoTarget) { + ViewGroup view = (ViewGroup) LayoutInflater.from(parent.getContext()) .inflate(R.layout.preference_app, parent, false); + if (twoTarget) { + final ViewGroup widgetFrame = view.findViewById(android.R.id.widget_frame); + if (widgetFrame != null) { + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.preference_widget_master_switch, widgetFrame, true); + + View divider = LayoutInflater.from(parent.getContext()).inflate( + R.layout.preference_two_target_divider, view, false); + // second to last, before widget frame + view.addView(divider, view.getChildCount() - 1); + } + } + return view; } void setSummary(CharSequence summary) { @@ -141,4 +164,12 @@ public class ApplicationViewHolder extends RecyclerView.ViewHolder { setSummary(invalidSizeStr); } } + + void updateSwitch(View.OnClickListener listener, boolean enabled, boolean checked) { + if (mSwitch != null && mWidgetContainer != null) { + mWidgetContainer.setOnClickListener(listener); + mSwitch.setChecked(checked); + mSwitch.setEnabled(enabled); + } + } } diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index 6521056d800..ab8cc714550 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -109,6 +109,7 @@ import com.android.settings.dashboard.SummaryLoader; import com.android.settings.fuelgauge.HighPowerDetail; import com.android.settings.notification.AppNotificationSettings; import com.android.settings.notification.ConfigureNotificationSettings; +import com.android.settings.notification.NotificationBackend; import com.android.settings.widget.LoadingViewController; import com.android.settings.wifi.AppStateChangeWifiStateBridge; import com.android.settings.wifi.ChangeWifiStateDetails; @@ -223,6 +224,7 @@ public class ManageApplications extends InstrumentedFragment private Spinner mFilterSpinner; private FilterSpinnerAdapter mFilterAdapter; private UsageStatsManager mUsageStatsManager; + private NotificationBackend mNotificationBackend; private ResetAppsHelper mResetAppsHelper; private String mVolumeUuid; private int mStorageType; @@ -292,6 +294,7 @@ public class ManageApplications extends InstrumentedFragment mListType = LIST_TYPE_NOTIFICATION; mUsageStatsManager = (UsageStatsManager) getContext().getSystemService(Context.USAGE_STATS_SERVICE); + mNotificationBackend = new NotificationBackend(); mSortOrder = R.id.sort_order_recent_notification; screenTitle = R.string.app_notifications_title; } else { @@ -869,8 +872,9 @@ public class ManageApplications extends InstrumentedFragment mContext = manageApplications.getActivity(); mAppFilter = appFilter; if (mManageApplications.mListType == LIST_TYPE_NOTIFICATION) { - mExtraInfoBridge = new AppStateNotificationBridge(mState, this, - manageApplications.mUsageStatsManager); + mExtraInfoBridge = new AppStateNotificationBridge(mContext, mState, this, + manageApplications.mUsageStatsManager, + manageApplications.mNotificationBackend); } else if (mManageApplications.mListType == LIST_TYPE_USAGE_ACCESS) { mExtraInfoBridge = new AppStateUsageBridge(mContext, mState, this); } else if (mManageApplications.mListType == LIST_TYPE_HIGH_POWER) { @@ -988,7 +992,12 @@ public class ManageApplications extends InstrumentedFragment @Override public ApplicationViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - final View view = ApplicationViewHolder.newView(parent); + View view; + if (mManageApplications.mListType == LIST_TYPE_NOTIFICATION) { + view = ApplicationViewHolder.newView(parent, true /* twoTarget */); + } else { + view = ApplicationViewHolder.newView(parent, false /* twoTarget */); + } return new ApplicationViewHolder(view, shouldUseStableItemHeight(mManageApplications.mListType)); } @@ -1276,6 +1285,7 @@ public class ManageApplications extends InstrumentedFragment mState.ensureIcon(entry); holder.setIcon(entry.icon); updateSummary(holder, entry); + updateSwitch(holder, entry); holder.updateDisableView(entry.info); } holder.setEnabled(isEnabled(position)); @@ -1328,6 +1338,24 @@ public class ManageApplications extends InstrumentedFragment } } + private void updateSwitch(ApplicationViewHolder holder, AppEntry entry) { + switch (mManageApplications.mListType) { + case LIST_TYPE_NOTIFICATION: + holder.updateSwitch(((AppStateNotificationBridge) mExtraInfoBridge) + .getSwitchOnClickListener(entry), + AppStateNotificationBridge.enableSwitch(entry), + AppStateNotificationBridge.checkSwitch(entry)); + if (entry.extraInfo != null) { + holder.setSummary(AppStateNotificationBridge.getSummary(mContext, + (NotificationsSentState) entry.extraInfo, + (mLastSortMode == R.id.sort_order_recent_notification))); + } else { + holder.setSummary(null); + } + break; + } + } + private boolean hasExtraView() { return mExtraViewController != null && mExtraViewController.shouldShow(); diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java index d205fb4ab8e..34b6ce534cb 100644 --- a/src/com/android/settings/notification/NotificationBackend.java +++ b/src/com/android/settings/notification/NotificationBackend.java @@ -65,11 +65,15 @@ public class NotificationBackend { public AppRow loadAppRow(Context context, PackageManager pm, PackageInfo app) { final AppRow row = loadAppRow(context, pm, app.applicationInfo); + recordCanBeBlocked(context, pm, app, row); + return row; + } + + void recordCanBeBlocked(Context context, PackageManager pm, PackageInfo app, AppRow row) { row.systemApp = Utils.isSystemPackage(context.getResources(), pm, app); final String[] nonBlockablePkgs = context.getResources().getStringArray( - com.android.internal.R.array.config_nonBlockableNotificationPackages); + com.android.internal.R.array.config_nonBlockableNotificationPackages); markAppRowWithBlockables(nonBlockablePkgs, row, app.packageName); - return row; } @VisibleForTesting static void markAppRowWithBlockables(String[] nonBlockablePkgs, AppRow row, @@ -92,6 +96,19 @@ public class NotificationBackend { } } + public boolean isSystemApp(Context context, ApplicationInfo app) { + try { + PackageInfo info = context.getPackageManager().getPackageInfo( + app.packageName, PackageManager.GET_SIGNATURES); + final AppRow row = new AppRow(); + recordCanBeBlocked(context, context.getPackageManager(), info, row); + return row.systemApp; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return false; + } + public boolean getNotificationsBanned(String pkg, int uid) { try { final boolean enabled = sINM.areNotificationsEnabledForPackage(pkg, uid); diff --git a/tests/robotests/src/com/android/settings/applications/AppStateNotificationBridgeTest.java b/tests/robotests/src/com/android/settings/applications/AppStateNotificationBridgeTest.java index e46111a2999..9437a00766f 100644 --- a/tests/robotests/src/com/android/settings/applications/AppStateNotificationBridgeTest.java +++ b/tests/robotests/src/com/android/settings/applications/AppStateNotificationBridgeTest.java @@ -33,8 +33,11 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.usage.UsageEvents; @@ -44,9 +47,12 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.os.Looper; import android.os.Parcel; +import android.view.ViewGroup; +import android.widget.Switch; import com.android.settings.R; import com.android.settings.applications.AppStateNotificationBridge.NotificationsSentState; +import com.android.settings.notification.NotificationBackend; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; @@ -74,6 +80,8 @@ public class AppStateNotificationBridgeTest { private ApplicationsState mState; @Mock private UsageStatsManager mUsageStats; + @Mock + private NotificationBackend mBackend; private Context mContext; private AppStateNotificationBridge mBridge; @@ -82,10 +90,12 @@ public class AppStateNotificationBridgeTest { MockitoAnnotations.initMocks(this); when(mState.newSession(any())).thenReturn(mSession); when(mState.getBackgroundLooper()).thenReturn(mock(Looper.class)); + when(mBackend.getNotificationsBanned(anyString(), anyInt())).thenReturn(true); + when(mBackend.isSystemApp(any(), any())).thenReturn(true); mContext = RuntimeEnvironment.application.getApplicationContext(); - mBridge = new AppStateNotificationBridge(mState, - mock(AppStateBaseBridge.Callback.class), mUsageStats); + mBridge = new AppStateNotificationBridge(mContext, mState, + mock(AppStateBaseBridge.Callback.class), mUsageStats, mBackend); } private AppEntry getMockAppEntry(String pkg) { @@ -213,6 +223,9 @@ public class AppStateNotificationBridgeTest { assertThat(((NotificationsSentState) apps.get(0).extraInfo).lastSent).isEqualTo(6); assertThat(((NotificationsSentState) apps.get(0).extraInfo).avgSentDaily).isEqualTo(1); assertThat(((NotificationsSentState) apps.get(0).extraInfo).avgSentWeekly).isEqualTo(0); + assertThat(((NotificationsSentState) apps.get(0).extraInfo).blocked).isTrue(); + assertThat(((NotificationsSentState) apps.get(0).extraInfo).systemApp).isTrue(); + assertThat(((NotificationsSentState) apps.get(0).extraInfo).blockable).isTrue(); } @Test @@ -281,6 +294,9 @@ public class AppStateNotificationBridgeTest { assertThat(((NotificationsSentState) entry.extraInfo).lastSent).isEqualTo(12); assertThat(((NotificationsSentState) entry.extraInfo).avgSentDaily).isEqualTo(2); assertThat(((NotificationsSentState) entry.extraInfo).avgSentWeekly).isEqualTo(0); + assertThat(((NotificationsSentState) entry.extraInfo).blocked).isTrue(); + assertThat(((NotificationsSentState) entry.extraInfo).systemApp).isTrue(); + assertThat(((NotificationsSentState) entry.extraInfo).blockable).isTrue(); } @Test @@ -410,4 +426,33 @@ public class AppStateNotificationBridgeTest { assertThat(entries).containsExactly(veryFrequentDailyEntry, notFrequentDailyEntry, veryFrequentWeeklyEntry, notFrequentWeeklyEntry); } + + @Test + public void testSwitchOnClickListener() { + ViewGroup parent = mock(ViewGroup.class); + Switch toggle = mock(Switch.class); + when(toggle.isChecked()).thenReturn(true); + when(toggle.isEnabled()).thenReturn(true); + when(parent.findViewById(anyInt())).thenReturn(toggle); + + AppEntry entry = mock(AppEntry.class); + entry.info = new ApplicationInfo(); + entry.info.packageName = "pkg"; + entry.info.uid = 1356; + entry.extraInfo = new NotificationsSentState(); + + ViewGroup.OnClickListener listener = mBridge.getSwitchOnClickListener(entry); + listener.onClick(parent); + + verify(toggle).toggle(); + verify(mBackend).setNotificationsEnabledForPackage( + entry.info.packageName, entry.info.uid, true); + assertThat(((NotificationsSentState) entry.extraInfo).blocked).isFalse(); + } + + @Test + public void testSwitchViews_nullDoesNotCrash() { + mBridge.enableSwitch(null); + mBridge.checkSwitch(null); + } } diff --git a/tests/robotests/src/com/android/settings/applications/manageapplications/ApplicationViewHolderTest.java b/tests/robotests/src/com/android/settings/applications/manageapplications/ApplicationViewHolderTest.java index a18bf1fa43f..5b0b275e16b 100644 --- a/tests/robotests/src/com/android/settings/applications/manageapplications/ApplicationViewHolderTest.java +++ b/tests/robotests/src/com/android/settings/applications/manageapplications/ApplicationViewHolderTest.java @@ -91,4 +91,31 @@ public class ApplicationViewHolderTest { mHolder.updateSizeText(entry, invalidStr, ManageApplications.SIZE_EXTERNAL); assertThat(mHolder.mSummary.getText()).isEqualTo(invalidStr); } + + @Test + public void oneTouchTarget() { + assertThat(mHolder.mSwitch).isNull(); + assertThat(mHolder.mWidgetContainer.getChildCount()).isEqualTo(0); + // assert no exception + mHolder.updateSwitch(null, true, true); + } + + @Test + public void twoTouchTarget() { + mView = ApplicationViewHolder.newView(new FrameLayout(mContext), true); + mHolder = new ApplicationViewHolder(mView, false /* useStableHeight */); + assertThat(mHolder.mSwitch).isNotNull(); + assertThat(mHolder.mWidgetContainer.getChildCount()).isEqualTo(1); + } + + @Test + public void updateSwitch() { + mView = ApplicationViewHolder.newView(new FrameLayout(mContext), true); + mHolder = new ApplicationViewHolder(mView, false /* useStableHeight */); + mHolder.updateSwitch(v -> {}, true, true); + + assertThat(mHolder.mSwitch.isChecked()).isTrue(); + assertThat(mHolder.mSwitch.isEnabled()).isTrue(); + assertThat(mHolder.mWidgetContainer.hasOnClickListeners()).isTrue(); + } } diff --git a/tests/robotests/src/com/android/settings/applications/manageapplications/ManageApplicationsTest.java b/tests/robotests/src/com/android/settings/applications/manageapplications/ManageApplicationsTest.java index fb7b59d1efa..c75146283ef 100644 --- a/tests/robotests/src/com/android/settings/applications/manageapplications/ManageApplicationsTest.java +++ b/tests/robotests/src/com/android/settings/applications/manageapplications/ManageApplicationsTest.java @@ -22,6 +22,8 @@ import static com.android.settings.applications.manageapplications.AppFilterRegi import static com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_MAIN; import static com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_NOTIFICATION; import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; @@ -265,6 +267,42 @@ public class ManageApplicationsTest { verify(adapter).notifyDataSetChanged(); } + @Test + public void applicationsAdapter_onBindViewHolder_updateSwitch_notifications() { + ManageApplications manageApplications = mock(ManageApplications.class); + manageApplications.mListType = LIST_TYPE_NOTIFICATION; + ApplicationViewHolder holder = mock(ApplicationViewHolder.class); + ReflectionHelpers.setField(holder, "itemView", mock(View.class)); + ManageApplications.ApplicationsAdapter adapter = + new ManageApplications.ApplicationsAdapter(mState, + manageApplications, mock(AppFilterItem.class), + mock(Bundle.class)); + final ArrayList appList = new ArrayList<>(); + appList.add(mock(ApplicationsState.AppEntry.class)); + ReflectionHelpers.setField(adapter, "mEntries", appList); + + adapter.onBindViewHolder(holder, 0); + verify(holder).updateSwitch(any(), anyBoolean(), anyBoolean()); + } + + @Test + public void applicationsAdapter_onBindViewHolder_updateSwitch_notNotifications() { + ManageApplications manageApplications = mock(ManageApplications.class); + manageApplications.mListType = LIST_TYPE_MAIN; + ApplicationViewHolder holder = mock(ApplicationViewHolder.class); + ReflectionHelpers.setField(holder, "itemView", mock(View.class)); + ManageApplications.ApplicationsAdapter adapter = + new ManageApplications.ApplicationsAdapter(mState, + manageApplications, mock(AppFilterItem.class), + mock(Bundle.class)); + final ArrayList appList = new ArrayList<>(); + appList.add(mock(ApplicationsState.AppEntry.class)); + ReflectionHelpers.setField(adapter, "mEntries", appList); + + adapter.onBindViewHolder(holder, 0); + verify(holder, never()).updateSwitch(any(), anyBoolean(), anyBoolean()); + } + private void setUpOptionMenus() { when(mMenu.findItem(anyInt())).thenAnswer(invocation -> { final Object[] args = invocation.getArguments();