From b545599933acc61ee29f21b707289d551e1cfb7d Mon Sep 17 00:00:00 2001 From: timhypeng Date: Fri, 7 Feb 2020 08:59:00 +0800 Subject: [PATCH] Implement remote media preference in Sound Setting -Add test cases Bug: 147395289 Test: make -j42 RunSettingsRoboTests Change-Id: I7a3a2312e5f777cc22afb7a33df52afcc7707dc7 --- .../RemoteVolumeGroupController.java | 183 ++++++++++++++++++ .../RemoteVolumeSeekBarPreference.java | 11 ++ .../RemoteVolumeGroupControllerTest.java | 180 +++++++++++++++++ 3 files changed, 374 insertions(+) create mode 100644 src/com/android/settings/notification/RemoteVolumeGroupController.java create mode 100644 tests/robotests/src/com/android/settings/notification/RemoteVolumeGroupControllerTest.java diff --git a/src/com/android/settings/notification/RemoteVolumeGroupController.java b/src/com/android/settings/notification/RemoteVolumeGroupController.java new file mode 100644 index 00000000000..c36035aa20e --- /dev/null +++ b/src/com/android/settings/notification/RemoteVolumeGroupController.java @@ -0,0 +1,183 @@ +/* + * 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.notification; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.MediaDevice; +import com.android.settingslib.media.MediaOutputSliceConstants; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * A group preference controller to add/remove/update preference + * {@link com.android.settings.notification.RemoteVolumeSeekBarPreference} + **/ +public class RemoteVolumeGroupController extends BasePreferenceController implements + Preference.OnPreferenceChangeListener, LifecycleObserver, LocalMediaManager.DeviceCallback { + + private static final String KEY_REMOTE_VOLUME_GROUP = "remote_media_group"; + private static final String TAG = "RemoteVolumePrefCtr"; + @VisibleForTesting + static final String SWITCHER_PREFIX = "OUTPUT_SWITCHER"; + + private PreferenceCategory mPreferenceCategory; + private List mActiveRemoteMediaDevices = new ArrayList<>(); + + @VisibleForTesting + LocalMediaManager mLocalMediaManager; + + public RemoteVolumeGroupController(Context context, String preferenceKey) { + super(context, preferenceKey); + if (mLocalMediaManager == null) { + mLocalMediaManager = new LocalMediaManager(mContext, null, null); + mLocalMediaManager.registerCallback(this); + mLocalMediaManager.startScan(); + } + } + + @Override + public int getAvailabilityStatus() { + if (mActiveRemoteMediaDevices.isEmpty()) { + return CONDITIONALLY_UNAVAILABLE; + } + return AVAILABLE_UNSEARCHABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreferenceCategory = screen.findPreference(getPreferenceKey()); + mActiveRemoteMediaDevices.clear(); + mActiveRemoteMediaDevices.addAll(mLocalMediaManager.getActiveMediaDevice( + MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE)); + refreshPreference(); + } + + /** + * onDestroy() + * {@link androidx.lifecycle.OnLifecycleEvent} + **/ + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + public void onDestroy() { + mLocalMediaManager.unregisterCallback(this); + mLocalMediaManager.stopScan(); + } + + private void refreshPreference() { + mPreferenceCategory.removeAll(); + if (!isAvailable()) { + mPreferenceCategory.setVisible(false); + return; + } + final CharSequence outputTitle = mContext.getText(R.string.media_output_title); + final CharSequence castVolume = mContext.getText(R.string.remote_media_volume_option_title); + mPreferenceCategory.setVisible(true); + int i = 0; + for (MediaDevice device : mActiveRemoteMediaDevices) { + if (mPreferenceCategory.findPreference(device.getId()) != null) { + continue; + } + // Add slider + final RemoteVolumeSeekBarPreference seekBarPreference = + new RemoteVolumeSeekBarPreference(mContext); + seekBarPreference.setKey(device.getId()); + seekBarPreference.setTitle(castVolume + " (" + device.getClientAppLabel() + ")"); + seekBarPreference.setMax(device.getMaxVolume()); + seekBarPreference.setProgress(device.getCurrentVolume()); + seekBarPreference.setMin(0); + seekBarPreference.setOnPreferenceChangeListener(this); + seekBarPreference.setIcon(R.drawable.ic_volume_remote); + mPreferenceCategory.addPreference(seekBarPreference); + // Add output indicator + final Preference preference = new Preference(mContext); + preference.setKey(SWITCHER_PREFIX + device.getId()); + preference.setTitle(outputTitle); + preference.setSummary(device.getName()); + mPreferenceCategory.addPreference(preference); + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final MediaDevice device = mLocalMediaManager.getMediaDeviceById(preference.getKey()); + if (device == null) { + Log.e(TAG, "Unable to find " + preference.getKey() + " to set volume"); + return false; + } + ThreadUtils.postOnBackgroundThread(() -> { + device.requestSetVolume((int) newValue); + }); + return true; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!preference.getKey().startsWith(SWITCHER_PREFIX)) { + return false; + } + final String key = preference.getKey().substring(SWITCHER_PREFIX.length()); + final MediaDevice device = mLocalMediaManager.getMediaDeviceById(key); + if (device == null) { + return false; + } + final Intent intent = new Intent() + .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME, + device.getClientPackageName()); + mContext.startActivity(intent); + return true; + } + + @Override + public String getPreferenceKey() { + return KEY_REMOTE_VOLUME_GROUP; + } + + @Override + public void onDeviceListUpdate(List devices) { + if (mPreferenceCategory == null) { + // Preference group is not ready. + return; + } + mActiveRemoteMediaDevices.clear(); + mActiveRemoteMediaDevices.addAll(mLocalMediaManager.getActiveMediaDevice( + MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE)); + refreshPreference(); + } + + @Override + public void onSelectedDeviceStateChanged(MediaDevice device, int state) { + + } +} diff --git a/src/com/android/settings/notification/RemoteVolumeSeekBarPreference.java b/src/com/android/settings/notification/RemoteVolumeSeekBarPreference.java index e99af6a42cb..8714351d53e 100644 --- a/src/com/android/settings/notification/RemoteVolumeSeekBarPreference.java +++ b/src/com/android/settings/notification/RemoteVolumeSeekBarPreference.java @@ -18,6 +18,7 @@ package com.android.settings.notification; import android.content.Context; import android.util.AttributeSet; +import android.widget.SeekBar; /** * A slider preference that controls remote volume, which doesn't go through @@ -50,7 +51,17 @@ public class RemoteVolumeSeekBarPreference extends VolumeSeekBarPreference { @Override protected void init() { if (mSeekBar == null) return; + setContinuousUpdates(true); updateIconView(); updateSuppressionText(); + notifyHierarchyChanged(); + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + super.onProgressChanged(seekBar, progress, fromUser); + if (fromUser) { + notifyChanged(); + } } } diff --git a/tests/robotests/src/com/android/settings/notification/RemoteVolumeGroupControllerTest.java b/tests/robotests/src/com/android/settings/notification/RemoteVolumeGroupControllerTest.java new file mode 100644 index 00000000000..d81f30fc6b0 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/RemoteVolumeGroupControllerTest.java @@ -0,0 +1,180 @@ +/* + * 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.notification; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.widget.SeekBarPreference; +import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.MediaDevice; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class}) +public class RemoteVolumeGroupControllerTest { + + private static final String KEY_REMOTE_VOLUME_GROUP = "remote_media_group"; + private static final String TEST_PACKAGE_LABEL = "music"; + private static final String TEST_DEVICE_1_ID = "test_device_1_id"; + private static final String TEST_DEVICE_1_NAME = "test_device_1_name"; + private static final int CURRENT_VOLUME = 30; + private static final int MAX_VOLUME = 100; + + @Mock + private LocalMediaManager mLocalMediaManager; + @Mock + private MediaDevice mDevice; + @Mock + private PreferenceScreen mScreen; + @Mock + private PreferenceManager mPreferenceManager; + @Mock + private SharedPreferences mSharedPreferences; + + private final List mDevices = new ArrayList<>(); + + private Context mContext; + private RemoteVolumeGroupController mController; + private PreferenceCategory mPreferenceCategory; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mController = new RemoteVolumeGroupController(mContext, KEY_REMOTE_VOLUME_GROUP); + mController.mLocalMediaManager = mLocalMediaManager; + mPreferenceCategory = spy(new PreferenceCategory(mContext)); + mPreferenceCategory.setKey(mController.getPreferenceKey()); + + when(mPreferenceCategory.getPreferenceManager()).thenReturn(mPreferenceManager); + when(mPreferenceManager.getSharedPreferences()).thenReturn(mSharedPreferences); + when(mLocalMediaManager.getActiveMediaDevice( + MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE)).thenReturn(mDevices); + when(mDevice.getId()).thenReturn(TEST_DEVICE_1_ID); + when(mDevice.getName()).thenReturn(TEST_DEVICE_1_NAME); + when(mDevice.getMaxVolume()).thenReturn(MAX_VOLUME); + when(mDevice.getCurrentVolume()).thenReturn(CURRENT_VOLUME); + when(mDevice.getClientAppLabel()).thenReturn(TEST_PACKAGE_LABEL); + when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn( + mPreferenceCategory); + } + + @Test + public void getAvailabilityStatus_withActiveDevice_returnAvailableUnsearchable() { + mDevices.add(mDevice); + mController.displayPreference(mScreen); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE_UNSEARCHABLE); + } + + @Test + public void getAvailabilityStatus_noActiveDevice_returnConditionallyUnavailable() { + mController.displayPreference(mScreen); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void displayPreference_noActiveDevice_checkPreferenceCount() { + mController.displayPreference(mScreen); + + assertThat(mPreferenceCategory.getPreferenceCount()).isEqualTo(0); + } + + @Test + public void displayPreference_withActiveDevice_checkPreferenceCount() { + mDevices.add(mDevice); + mController.displayPreference(mScreen); + + assertThat(mPreferenceCategory.getPreferenceCount()).isEqualTo(2); + } + + @Test + public void displayPreference_withActiveDevice_checkSeekBarTitle() { + mDevices.add(mDevice); + mController.displayPreference(mScreen); + final Preference preference = mPreferenceCategory.findPreference(TEST_DEVICE_1_ID); + + assertThat(preference.getTitle()).isEqualTo(mContext.getText( + R.string.remote_media_volume_option_title) + " (" + TEST_PACKAGE_LABEL + ")"); + } + + @Test + public void displayPreference_withActiveDevice_checkSeekBarMaxVolume() { + mDevices.add(mDevice); + mController.displayPreference(mScreen); + final SeekBarPreference preference = mPreferenceCategory.findPreference(TEST_DEVICE_1_ID); + + assertThat(preference.getMax()).isEqualTo(MAX_VOLUME); + } + + @Test + public void displayPreference_withActiveDevice_checkSeekBarCurrentVolume() { + mDevices.add(mDevice); + mController.displayPreference(mScreen); + final SeekBarPreference preference = mPreferenceCategory.findPreference(TEST_DEVICE_1_ID); + + assertThat(preference.getProgress()).isEqualTo(CURRENT_VOLUME); + } + + @Test + public void displayPreference_withActiveDevice_checkSwitcherPreferenceTitle() { + mDevices.add(mDevice); + mController.displayPreference(mScreen); + final Preference preference = mPreferenceCategory.findPreference( + RemoteVolumeGroupController.SWITCHER_PREFIX + TEST_DEVICE_1_ID); + + assertThat(preference.getTitle()).isEqualTo(mContext.getText(R.string.media_output_title)); + } + + @Test + public void displayPreference_withActiveDevice_checkSwitcherPreferenceSummary() { + mDevices.add(mDevice); + mController.displayPreference(mScreen); + final Preference preference = mPreferenceCategory.findPreference( + RemoteVolumeGroupController.SWITCHER_PREFIX + TEST_DEVICE_1_ID); + + assertThat(preference.getSummary()).isEqualTo(TEST_DEVICE_1_NAME); + } +}