Allow user to block individual apps from resuming. am: 48ce5892e8

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Settings/+/12392268

Change-Id: I131ff7c33871d858a63824fc17ba168ba590f6e2
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3ab205c..3cbd832 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -12158,15 +12158,17 @@
     <!-- Title for media control settings [CHAR LIMIT=50]-->
     <string name="media_controls_title">Media</string>
     <!-- Summary for media control settings [CHAR LIMIT=60]-->
-    <string name="media_controls_summary">Hide player when the media session has ended</string>
+    <string name="media_controls_summary">Media player in Quick Settings</string>
     <!-- Description of toggle to enable or disable the media resumption feature in quick settings [CHAR LIMIT=NONE]-->
-    <string name="media_controls_resume_description">The player allows you to resume a session from the expanded Quick Settings panel.</string>
+    <string name="media_controls_resume_description">Show media player for an extended period to easily resume playback</string>
     <!-- Subtext for media settings when the player will be hidden [CHAR LIMIT=50] -->
     <string name="media_controls_hide_player">Hide player</string>
     <!-- Subtext for media settings when the player will be shown [CHAR LIMIT=50] -->
     <string name="media_controls_show_player">Show player</string>
     <!-- Subtext for media settings when no players can be shown [CHAR LIMIT=50] -->
     <string name="media_controls_no_players">No players available</string>
+    <!-- Subtitle for section of media control settings that shows which apps are allowed [CHAR LIMIT=50] -->
+    <string name="media_controls_apps_title">Allowed apps</string>
     <!-- Keywords for the media controls setting [CHAR LIMIT=NONE]-->
     <string name="keywords_media_controls">media</string>
 </resources>
diff --git a/res/xml/media_controls_settings.xml b/res/xml/media_controls_settings.xml
index 3f0483f..3ace6a0 100644
--- a/res/xml/media_controls_settings.xml
+++ b/res/xml/media_controls_settings.xml
@@ -28,4 +28,9 @@
         app:controller="com.android.settings.sound.MediaControlsPreferenceController"
         app:allowDividerAbove="true" />
 
+    <PreferenceCategory
+        android:key="media_controls_resumable_apps"
+        android:title="@string/media_controls_apps_title"
+        app:controller="com.android.settings.sound.ResumableMediaAppsController" />
+
 </PreferenceScreen>
diff --git a/src/com/android/settings/sound/MediaControlsPreferenceController.java b/src/com/android/settings/sound/MediaControlsPreferenceController.java
index 050cf93..219eb24 100644
--- a/src/com/android/settings/sound/MediaControlsPreferenceController.java
+++ b/src/com/android/settings/sound/MediaControlsPreferenceController.java
@@ -35,12 +35,12 @@
     @Override
     public boolean isChecked() {
         int val = Settings.Secure.getInt(mContext.getContentResolver(), MEDIA_CONTROLS_RESUME, 1);
-        return val == 0;
+        return val == 1;
     }
 
     @Override
     public boolean setChecked(boolean isChecked) {
-        int val = isChecked ? 0 : 1;
+        int val = isChecked ? 1 : 0;
         return Settings.Secure.putInt(mContext.getContentResolver(), MEDIA_CONTROLS_RESUME, val);
     }
 
diff --git a/src/com/android/settings/sound/ResumableMediaAppsController.java b/src/com/android/settings/sound/ResumableMediaAppsController.java
new file mode 100644
index 0000000..383cd41
--- /dev/null
+++ b/src/com/android/settings/sound/ResumableMediaAppsController.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2020 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.sound;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.provider.Settings;
+import android.service.media.MediaBrowserService;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceScreen;
+import androidx.preference.SwitchPreference;
+
+import com.android.settings.core.BasePreferenceController;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Section of media controls settings that contains a list of potentially resumable apps
+ */
+public class ResumableMediaAppsController extends BasePreferenceController {
+    private static final String TAG = "ResumableMediaAppsCtrl";
+
+    private PreferenceGroup mPreferenceGroup;
+    private PackageManager mPackageManager;
+    private List<ResolveInfo> mResumeInfo;
+
+    public ResumableMediaAppsController(Context context, String key) {
+        super(context, key);
+        mPackageManager = mContext.getPackageManager();
+        Intent serviceIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
+        mResumeInfo = mPackageManager.queryIntentServices(serviceIntent, 0);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        // Update list, since this will be called when the app goes to onStart / onPause
+        Intent serviceIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
+        mResumeInfo = mPackageManager.queryIntentServices(serviceIntent, 0);
+        return (mResumeInfo.size() > 0) ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreferenceGroup = screen.findPreference(getPreferenceKey());
+        Set<String> blockedApps = getBlockedMediaApps();
+        for (ResolveInfo inf : mResumeInfo) {
+            String packageName = inf.getComponentInfo().packageName;
+            MediaSwitchPreference pref = new MediaSwitchPreference(mContext, packageName);
+            CharSequence appTitle = packageName;
+            try {
+                appTitle = mPackageManager.getApplicationLabel(
+                        mPackageManager.getApplicationInfo(packageName, 0));
+                Drawable appIcon = mPackageManager.getApplicationIcon(packageName);
+                pref.setIcon(appIcon);
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.e(TAG, "Couldn't get app title", e);
+            }
+            pref.setTitle(appTitle);
+
+            pref.setOnPreferenceChangeListener((preference, status) -> {
+                MediaSwitchPreference mediaPreference = (MediaSwitchPreference) preference;
+                boolean isEnabled = (boolean) status;
+                Log.d(TAG, "preference " + mediaPreference + " changed " + isEnabled);
+
+                if (isEnabled) {
+                    blockedApps.remove(mediaPreference.getPackageName());
+                } else {
+                    blockedApps.add(mediaPreference.getPackageName());
+                }
+                setBlockedMediaApps(blockedApps);
+                return true;
+            });
+
+            pref.setChecked(!blockedApps.contains(packageName));
+            mPreferenceGroup.addPreference(pref);
+        }
+    }
+
+    class MediaSwitchPreference extends SwitchPreference {
+        private String mPackageName;
+
+        MediaSwitchPreference(Context context, String packageName) {
+            super(context);
+            mPackageName = packageName;
+        }
+
+        public String getPackageName() {
+            return mPackageName;
+        }
+    }
+
+    private Set<String> getBlockedMediaApps() {
+        String list = Settings.Secure.getString(mContext.getContentResolver(),
+                Settings.Secure.MEDIA_CONTROLS_RESUME_BLOCKED);
+        if (TextUtils.isEmpty(list)) {
+            return new ArraySet<>();
+        }
+        String[] names = list.split(":");
+        Set<String> set = new ArraySet<>(names.length);
+        Collections.addAll(set, names);
+        return set;
+    }
+
+    private void setBlockedMediaApps(Set<String> apps) {
+        if (apps == null || apps.size() == 0) {
+            Settings.Secure.putString(mContext.getContentResolver(),
+                    Settings.Secure.MEDIA_CONTROLS_RESUME_BLOCKED, "");
+            return;
+        }
+        String list = String.join(":", apps);
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Settings.Secure.MEDIA_CONTROLS_RESUME_BLOCKED, list);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/sound/MediaControlsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/sound/MediaControlsPreferenceControllerTest.java
index b8cc709..f281e25 100644
--- a/tests/robotests/src/com/android/settings/sound/MediaControlsPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/sound/MediaControlsPreferenceControllerTest.java
@@ -69,26 +69,26 @@
     }
 
     @Test
-    public void setChecked_enable_shouldTurnOff() {
+    public void setChecked_enable_shouldTurnOn() {
         Settings.Global.putInt(mContentResolver, Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1);
         Settings.Secure.putInt(mContentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1);
 
-        assertThat(mController.isChecked()).isFalse();
+        assertThat(mController.isChecked()).isTrue();
 
-        mController.setChecked(true);
+        mController.setChecked(false);
 
         assertThat(Settings.Secure.getInt(mContentResolver,
                 Settings.Secure.MEDIA_CONTROLS_RESUME, -1)).isEqualTo(0);
     }
 
     @Test
-    public void setChecked_disable_shouldTurnOn() {
+    public void setChecked_disable_shouldTurnOff() {
         Settings.Global.putInt(mContentResolver, Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1);
         Settings.Secure.putInt(mContentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0);
 
-        assertThat(mController.isChecked()).isTrue();
+        assertThat(mController.isChecked()).isFalse();
 
-        mController.setChecked(false);
+        mController.setChecked(true);
 
         assertThat(Settings.Secure.getInt(mContentResolver,
                 Settings.Secure.MEDIA_CONTROLS_RESUME, -1)).isEqualTo(1);
diff --git a/tests/robotests/src/com/android/settings/sound/ResumableMediaAppsControllerTest.java b/tests/robotests/src/com/android/settings/sound/ResumableMediaAppsControllerTest.java
new file mode 100644
index 0000000..797560a
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/sound/ResumableMediaAppsControllerTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2020 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.sound;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.provider.Settings;
+
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.testutils.ResolveInfoBuilder;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public class ResumableMediaAppsControllerTest {
+
+    private static final String KEY = "media_controls_resumable_apps";
+
+    private static final String FAKE_APP = "com.test.fakeapp1";
+
+    private Context mContext;
+    private int mOriginalQs;
+    private int mOriginalResume;
+    private String mOriginalBlocked;
+    private ContentResolver mContentResolver;
+    private ResumableMediaAppsController mController;
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private PreferenceScreen mPreferenceScreen;
+    @Mock
+    private PreferenceGroup mPreferenceGroup;
+
+    @Before
+    public void setUp() {
+        mContext = spy(RuntimeEnvironment.application);
+        mContentResolver = mContext.getContentResolver();
+        mOriginalQs = Settings.Global.getInt(mContentResolver,
+                Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1);
+        mOriginalResume = Settings.Secure.getInt(mContentResolver,
+                Settings.Secure.MEDIA_CONTROLS_RESUME, 1);
+        mOriginalBlocked = Settings.Secure.getString(mContentResolver,
+                Settings.Secure.MEDIA_CONTROLS_RESUME_BLOCKED);
+
+        // Start all tests with feature enabled, nothing blocked
+        Settings.Global.putInt(mContentResolver, Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1);
+        Settings.Secure.putInt(mContentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1);
+        Settings.Secure.putString(mContentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME_BLOCKED,
+                mOriginalBlocked);
+
+        mPreferenceScreen = mock(PreferenceScreen.class);
+        mPreferenceGroup = mock(PreferenceGroup.class);
+        mPackageManager = mock(PackageManager.class);
+
+        List<ResolveInfo> fakeInfo = new ArrayList<>();
+        fakeInfo.add(createResolveInfo(FAKE_APP));
+        when(mPackageManager.queryIntentServices(any(), anyInt())).thenReturn(fakeInfo);
+
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPreferenceScreen.findPreference(KEY)).thenReturn(mPreferenceGroup);
+
+        mController = new ResumableMediaAppsController(mContext, KEY);
+    }
+
+    @After
+    public void tearDown() {
+        Settings.Global.putInt(mContentResolver, Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
+                mOriginalQs);
+        Settings.Secure.putInt(mContentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME,
+                mOriginalResume);
+        Settings.Secure.putString(mContentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME_BLOCKED,
+                mOriginalBlocked);
+    }
+
+    @Test
+    public void getAvailability_hasEligibleApps_isAvailable() {
+        // The package manager already has an eligible app from setUp()
+        assertEquals(AVAILABLE, mController.getAvailabilityStatus());
+    }
+
+    @Test
+    public void getAvailability_noEligibleApps_isConditionallyUnavailable() {
+        Context context = mock(Context.class);
+        PackageManager packageManager = mock(PackageManager.class);
+        List<ResolveInfo> fakeInfo = new ArrayList<>();
+        when(packageManager.queryIntentServices(any(), anyInt())).thenReturn(fakeInfo);
+        when(context.getPackageManager()).thenReturn(packageManager);
+        ResumableMediaAppsController controller = new ResumableMediaAppsController(context, KEY);
+
+        assertEquals(CONDITIONALLY_UNAVAILABLE, controller.getAvailabilityStatus());
+    }
+
+    @Test
+    public void displayPreference_addsApps() {
+        mController.displayPreference(mPreferenceScreen);
+        verify(mPreferenceGroup, times(1)).addPreference(any());
+    }
+
+    @Test
+    public void unblockedApp_isChecked() {
+        ArgumentCaptor<ResumableMediaAppsController.MediaSwitchPreference> argument =
+                ArgumentCaptor.forClass(ResumableMediaAppsController.MediaSwitchPreference.class);
+        mController.displayPreference(mPreferenceScreen);
+        verify(mPreferenceGroup).addPreference(argument.capture());
+        assertTrue(argument.getValue().isChecked());
+    }
+
+    @Test
+    public void blockedApp_isNotChecked() {
+        Settings.Secure.putString(mContentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME_BLOCKED,
+                FAKE_APP);
+
+        ArgumentCaptor<ResumableMediaAppsController.MediaSwitchPreference> argument =
+                ArgumentCaptor.forClass(ResumableMediaAppsController.MediaSwitchPreference.class);
+        mController.displayPreference(mPreferenceScreen);
+        verify(mPreferenceGroup).addPreference(argument.capture());
+
+        assertFalse(argument.getValue().isChecked());
+    }
+
+    private ResolveInfo createResolveInfo(String name) {
+        ResolveInfoBuilder builder = new ResolveInfoBuilder(name);
+        builder.setActivity(name, name);
+        return builder.build();
+    }
+}