diff --git a/res/values/strings.xml b/res/values/strings.xml index 2ad35e0b77d..6aba3c739ca 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12161,15 +12161,17 @@ Media - Hide player when the media session has ended + Media player in Quick Settings - The player allows you to resume a session from the expanded Quick Settings panel. + Show media player for an extended period to easily resume playback Hide player Show player No players available + + Allowed apps media diff --git a/res/xml/media_controls_settings.xml b/res/xml/media_controls_settings.xml index 3f0483f028e..3ace6a0b66f 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" /> + + diff --git a/src/com/android/settings/sound/MediaControlsPreferenceController.java b/src/com/android/settings/sound/MediaControlsPreferenceController.java index 050cf9391af..219eb247895 100644 --- a/src/com/android/settings/sound/MediaControlsPreferenceController.java +++ b/src/com/android/settings/sound/MediaControlsPreferenceController.java @@ -35,12 +35,12 @@ public class MediaControlsPreferenceController extends TogglePreferenceControlle @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 00000000000..383cd419d4a --- /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 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 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 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 set = new ArraySet<>(names.length); + Collections.addAll(set, names); + return set; + } + + private void setBlockedMediaApps(Set 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 b8cc709671e..f281e25e0b7 100644 --- a/tests/robotests/src/com/android/settings/sound/MediaControlsPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/sound/MediaControlsPreferenceControllerTest.java @@ -69,27 +69,27 @@ public class MediaControlsPreferenceControllerTest { } @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(); - - mController.setChecked(true); - - assertThat(Settings.Secure.getInt(mContentResolver, - Settings.Secure.MEDIA_CONTROLS_RESUME, -1)).isEqualTo(0); - } - - @Test - public void setChecked_disable_shouldTurnOn() { - 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(); mController.setChecked(false); + assertThat(Settings.Secure.getInt(mContentResolver, + Settings.Secure.MEDIA_CONTROLS_RESUME, -1)).isEqualTo(0); + } + + @Test + 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()).isFalse(); + + 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 00000000000..797560a5841 --- /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 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 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 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 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(); + } +}