diff --git a/src/com/android/settings/homepage/contextualcards/slices/ContextualNotificationChannelSlice.java b/src/com/android/settings/homepage/contextualcards/slices/ContextualNotificationChannelSlice.java index 2025a06a71a..17cae777b75 100644 --- a/src/com/android/settings/homepage/contextualcards/slices/ContextualNotificationChannelSlice.java +++ b/src/com/android/settings/homepage/contextualcards/slices/ContextualNotificationChannelSlice.java @@ -16,14 +16,23 @@ package com.android.settings.homepage.contextualcards.slices; +import static android.content.Context.MODE_PRIVATE; + import android.content.Context; import android.net.Uri; +import android.util.ArraySet; import com.android.settings.R; import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.SliceBackgroundWorker; + +import java.util.Set; public class ContextualNotificationChannelSlice extends NotificationChannelSlice { + public static final String PREFS = "notification_channel_slice_prefs"; + public static final String PREF_KEY_INTERACTED_PACKAGES = "interacted_packages"; + public ContextualNotificationChannelSlice(Context context) { super(context); } @@ -37,4 +46,18 @@ public class ContextualNotificationChannelSlice extends NotificationChannelSlice protected CharSequence getSubTitle(String packageName, int uid) { return mContext.getText(R.string.recently_installed_app); } + + @Override + protected boolean isUserInteracted(String packageName) { + // Check the package has been interacted on current slice or not. + final Set interactedPackages = + mContext.getSharedPreferences(PREFS, MODE_PRIVATE) + .getStringSet(PREF_KEY_INTERACTED_PACKAGES, new ArraySet<>()); + return interactedPackages.contains(packageName); + } + + @Override + public Class getBackgroundWorkerClass() { + return NotificationChannelWorker.class; + } } diff --git a/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java b/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java index 0550e7b30eb..e5cee37abcf 100644 --- a/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java +++ b/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java @@ -218,6 +218,17 @@ public class NotificationChannelSlice implements CustomSliceable { .toIntent(); } + /** + * Check the package has been interacted by user or not. + * Will use to filter package in {@link #getRecentlyInstalledPackages()}. + * + * @param packageName The app package name. + * @return true if the package was interacted, false otherwise. + */ + protected boolean isUserInteracted(String packageName) { + return false; + } + @VisibleForTesting IconCompat getApplicationIcon(String packageName) { final Drawable drawable; @@ -328,8 +339,9 @@ public class NotificationChannelSlice implements CustomSliceable { final List installedPackages = mContext.getPackageManager().getInstalledPackages(0); for (PackageInfo packageInfo : installedPackages) { - // Not include system app. - if (packageInfo.applicationInfo.isSystemApp()) { + // Not include system app and interacted app. + if (packageInfo.applicationInfo.isSystemApp() + || isUserInteracted(packageInfo.packageName)) { continue; } diff --git a/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelWorker.java b/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelWorker.java new file mode 100644 index 00000000000..f1d0d593cfa --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelWorker.java @@ -0,0 +1,77 @@ +/* + * 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.homepage.contextualcards.slices; + +import static android.content.Context.MODE_PRIVATE; + +import static com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice.PREFS; +import static com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice.PREF_KEY_INTERACTED_PACKAGES; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.net.Uri; +import android.util.ArraySet; + +import com.android.settings.slices.SliceBackgroundWorker; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class NotificationChannelWorker extends SliceBackgroundWorker { + + public NotificationChannelWorker(Context context, Uri uri) { + super(context, uri); + } + + @Override + protected void onSlicePinned() { + } + + @Override + protected void onSliceUnpinned() { + removeUninstalledPackages(); + } + + @Override + public void close() throws IOException { + } + + private void removeUninstalledPackages() { + final SharedPreferences prefs = getContext().getSharedPreferences(PREFS, MODE_PRIVATE); + final Set interactedPackages = + prefs.getStringSet(PREF_KEY_INTERACTED_PACKAGES, new ArraySet()); + if (interactedPackages.isEmpty()) { + return; + } + + final List installedPackageInfos = + getContext().getPackageManager().getInstalledPackages(0); + final List installedPackages = installedPackageInfos.stream() + .map(packageInfo -> packageInfo.packageName) + .collect(Collectors.toList()); + final Set newInteractedPackages = new ArraySet<>(); + for (String packageName : interactedPackages) { + if (installedPackages.contains(packageName)) { + newInteractedPackages.add(packageName); + } + } + prefs.edit().putStringSet(PREF_KEY_INTERACTED_PACKAGES, newInteractedPackages).apply(); + } +} diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/ContextualNotificationChannelSliceTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/ContextualNotificationChannelSliceTest.java index f2b87be53ed..8541a30c9a9 100644 --- a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/ContextualNotificationChannelSliceTest.java +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/ContextualNotificationChannelSliceTest.java @@ -16,30 +16,49 @@ package com.android.settings.homepage.contextualcards.slices; +import static android.content.Context.MODE_PRIVATE; + +import static com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice.PREFS; +import static com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice.PREF_KEY_INTERACTED_PACKAGES; + import static com.google.common.truth.Truth.assertThat; import android.content.Context; +import android.content.SharedPreferences; import android.net.Uri; +import android.util.ArraySet; import com.android.settings.R; import com.android.settings.slices.CustomSliceRegistry; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import java.util.Set; + @RunWith(RobolectricTestRunner.class) public class ContextualNotificationChannelSliceTest { + private static final String PACKAGE_NAME = "package_name"; + private Context mContext; private ContextualNotificationChannelSlice mNotificationChannelSlice; + private SharedPreferences mSharedPreferences; @Before public void setUp() { mContext = RuntimeEnvironment.application; mNotificationChannelSlice = new ContextualNotificationChannelSlice(mContext); + mSharedPreferences = mContext.getSharedPreferences(PREFS, MODE_PRIVATE); + } + + @After + public void tearDown() { + removeInteractedPackageFromSharedPreference(); } @Test @@ -55,4 +74,34 @@ public class ContextualNotificationChannelSliceTest { assertThat(subTitle).isEqualTo(mContext.getText(R.string.recently_installed_app)); } + + @Test + public void isUserInteracted_hasInteractedPackage_shouldBeTrue() { + addInteractedPackageToSharedPreference(); + + final boolean isInteracted = mNotificationChannelSlice.isUserInteracted(PACKAGE_NAME); + + assertThat(isInteracted).isTrue(); + } + + @Test + public void isUserInteracted_noInteractedPackage_shouldBeFalse() { + final boolean isInteracted = mNotificationChannelSlice.isUserInteracted(PACKAGE_NAME); + + assertThat(isInteracted).isFalse(); + } + + private void addInteractedPackageToSharedPreference() { + final Set interactedPackages = new ArraySet<>(); + interactedPackages.add(PACKAGE_NAME); + + mSharedPreferences.edit().putStringSet(PREF_KEY_INTERACTED_PACKAGES, + interactedPackages).apply(); + } + + private void removeInteractedPackageFromSharedPreference() { + if (mSharedPreferences.contains(PREF_KEY_INTERACTED_PACKAGES)) { + mSharedPreferences.edit().remove(PREF_KEY_INTERACTED_PACKAGES).apply(); + } + } } diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSliceTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSliceTest.java index 12513f67e1a..81f5797c659 100644 --- a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSliceTest.java +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSliceTest.java @@ -26,7 +26,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.app.NotificationChannel; @@ -299,6 +298,21 @@ public class NotificationChannelSliceTest { assertThat(metadata.getTitle()).isEqualTo(mContext.getString(R.string.no_suggested_app)); } + @Test + @Config(shadows = ShadowRestrictedLockUtilsInternal.class) + public void getSlice_isInteractedPackage_shouldHaveNoSuggestedAppTitle() { + addMockPackageToPackageManager(true /* isRecentlyInstalled */, + ApplicationInfo.FLAG_INSTALLED); + mockNotificationBackend(CHANNEL_COUNT, NOTIFICATION_COUNT, false /* banned */, + false /* isChannelBlocked */); + doReturn(true).when(mNotificationChannelSlice).isUserInteracted(any(String.class)); + + final Slice slice = mNotificationChannelSlice.getSlice(); + + final SliceMetadata metadata = SliceMetadata.from(mContext, slice); + assertThat(metadata.getTitle()).isEqualTo(mContext.getString(R.string.no_suggested_app)); + } + private void addMockPackageToPackageManager(boolean isRecentlyInstalled, int flags) { final ApplicationInfo applicationInfo = new ApplicationInfo(); applicationInfo.name = APP_LABEL; diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelWorkerTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelWorkerTest.java new file mode 100644 index 00000000000..6ac8b708262 --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelWorkerTest.java @@ -0,0 +1,112 @@ +/* + * 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.homepage.contextualcards.slices; + +import static android.content.Context.MODE_PRIVATE; + +import static com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice.PREFS; +import static com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice.PREF_KEY_INTERACTED_PACKAGES; + +import static com.google.common.truth.Truth.assertThat; + +import static org.robolectric.Shadows.shadowOf; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.net.Uri; +import android.util.ArraySet; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowPackageManager; + +import java.util.Set; + +@RunWith(RobolectricTestRunner.class) +public class NotificationChannelWorkerTest { + private static final Uri URI = Uri.parse("content://com.android.settings.slices/test"); + private static final String PACKAGE_NAME = "com.test.notification.channel.slice"; + + private Context mContext; + private NotificationChannelWorker mNotificationChannelWorker; + private ShadowPackageManager mPackageManager; + private SharedPreferences mSharedPreferences; + + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mNotificationChannelWorker = new NotificationChannelWorker(mContext, URI); + + // Shadow PackageManager to add mock package. + mPackageManager = shadowOf(mContext.getPackageManager()); + + mSharedPreferences = mContext.getSharedPreferences(PREFS, MODE_PRIVATE); + addInteractedPackageToSharedPreference(); + } + + @After + public void tearDown() { + mPackageManager.removePackage(PACKAGE_NAME); + removeInteractedPackageFromSharedPreference(); + } + + @Test + public void onSliceUnpinned_interactedPackageIsUninstalled_shouldRemovePackage() { + mNotificationChannelWorker.onSliceUnpinned(); + + final Set interactedPackages = mSharedPreferences.getStringSet( + PREF_KEY_INTERACTED_PACKAGES, new ArraySet<>()); + assertThat(interactedPackages.contains(PACKAGE_NAME)).isFalse(); + } + + @Test + public void onSliceUnpinned_interactedPackageIsInstalled_shouldKeepPackage() { + mockInteractedPackageAsInstalled(); + + mNotificationChannelWorker.onSliceUnpinned(); + + final Set interactedPackages = mSharedPreferences.getStringSet( + PREF_KEY_INTERACTED_PACKAGES, new ArraySet<>()); + assertThat(interactedPackages.contains(PACKAGE_NAME)).isTrue(); + } + + private void mockInteractedPackageAsInstalled() { + final PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = PACKAGE_NAME; + mPackageManager.addPackage(packageInfo); + } + + private void addInteractedPackageToSharedPreference() { + final Set interactedPackages = new ArraySet<>(); + interactedPackages.add(PACKAGE_NAME); + + mSharedPreferences.edit().putStringSet(PREF_KEY_INTERACTED_PACKAGES, + interactedPackages).apply(); + } + + private void removeInteractedPackageFromSharedPreference() { + if (mSharedPreferences.contains(PREF_KEY_INTERACTED_PACKAGES)) { + mSharedPreferences.edit().remove(PREF_KEY_INTERACTED_PACKAGES).apply(); + } + } +}