Add stop casting button for output switch

This CL add a customize button to stop casting when media
is transferred to info device.

This CL include following change:
- Add new methods to PanelContent.java. Let panels to
  customize "see more button".
- MediaOutputPanel will check which device is used to
  transfer media currently. The customize button will
  shown on when transfer device is info device.
  Then user can use this button to stop casting.
- Add test case.

Bug: 147856563
Test: make -j42 RunSettingsRoboTests
Change-Id: I8b201a10339f39f1d938d99b5659a82014e5bb89
This commit is contained in:
hughchen
2020-01-17 15:19:04 +08:00
parent 14b758f1c8
commit 201b48b885
6 changed files with 245 additions and 8 deletions

View File

@@ -11716,4 +11716,6 @@
<!-- Subtext for showing the option of RTT setting. [CHAR LIMIT=NONE] --> <!-- Subtext for showing the option of RTT setting. [CHAR LIMIT=NONE] -->
<string name="rtt_settings_always_visible"></string> <string name="rtt_settings_always_visible"></string>
<!-- Button label to stop casting on media device. [CHAR LIMIT=40 -->
<string name="media_output_panel_stop_casting_button">Stop casting</string>
</resources> </resources>

View File

@@ -16,6 +16,9 @@
package com.android.settings.panel; package com.android.settings.panel;
import static androidx.lifecycle.Lifecycle.Event.ON_START;
import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
import static com.android.settings.media.MediaOutputSlice.MEDIA_PACKAGE_NAME; import static com.android.settings.media.MediaOutputSlice.MEDIA_PACKAGE_NAME;
import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI; import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI;
@@ -35,9 +38,15 @@ import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.core.graphics.drawable.IconCompat; import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.Utils; import com.android.settings.Utils;
import com.android.settingslib.media.InfoMediaDevice;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -49,13 +58,20 @@ import java.util.List;
* Displays Media output item * Displays Media output item
* </p> * </p>
*/ */
public class MediaOutputPanel implements PanelContent { public class MediaOutputPanel implements PanelContent, LocalMediaManager.DeviceCallback,
LifecycleObserver {
private static final String TAG = "MediaOutputPanel"; private static final String TAG = "MediaOutputPanel";
private final Context mContext; private final Context mContext;
private final String mPackageName; private final String mPackageName;
private PanelCustomizedButtonCallback mCallback;
private boolean mIsCustomizedButtonUsed = true;
@VisibleForTesting
LocalMediaManager mLocalMediaManager;
private MediaSessionManager mMediaSessionManager; private MediaSessionManager mMediaSessionManager;
private MediaController mMediaController; private MediaController mMediaController;
@@ -65,8 +81,9 @@ public class MediaOutputPanel implements PanelContent {
private MediaOutputPanel(Context context, String packageName) { private MediaOutputPanel(Context context, String packageName) {
mContext = context.getApplicationContext(); mContext = context.getApplicationContext();
mPackageName = packageName; mPackageName = TextUtils.isEmpty(packageName) ? "" : packageName;
if (mPackageName != null) {
if (!TextUtils.isEmpty(mPackageName)) {
mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class); mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) { for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) {
if (TextUtils.equals(controller.getPackageName(), mPackageName)) { if (TextUtils.equals(controller.getPackageName(), mPackageName)) {
@@ -75,6 +92,7 @@ public class MediaOutputPanel implements PanelContent {
} }
} }
} }
if (mMediaController == null) { if (mMediaController == null) {
Log.e(TAG, "Unable to find " + mPackageName + " media controller"); Log.e(TAG, "Unable to find " + mPackageName + " media controller");
} }
@@ -156,8 +174,69 @@ public class MediaOutputPanel implements PanelContent {
return null; return null;
} }
@Override
public boolean isCustomizedButtonUsed() {
return mIsCustomizedButtonUsed;
}
@Override
public CharSequence getCustomButtonTitle() {
return mContext.getText(R.string.media_output_panel_stop_casting_button);
}
@Override
public void onClickCustomizedButton() {
}
@Override
public void registerCallback(PanelCustomizedButtonCallback callback) {
mCallback = callback;
}
@Override @Override
public int getMetricsCategory() { public int getMetricsCategory() {
return SettingsEnums.PANEL_MEDIA_OUTPUT; return SettingsEnums.PANEL_MEDIA_OUTPUT;
} }
@Override
public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
dispatchCustomButtonStateChanged();
}
@Override
public void onDeviceListUpdate(List<MediaDevice> devices) {
dispatchCustomButtonStateChanged();
}
@Override
public void onDeviceAttributesChanged() {
dispatchCustomButtonStateChanged();
}
private void dispatchCustomButtonStateChanged() {
hideCustomButtonIfNecessary();
if (mCallback != null) {
mCallback.onCustomizedButtonStateChanged();
}
}
private void hideCustomButtonIfNecessary() {
final MediaDevice device = mLocalMediaManager.getCurrentConnectedDevice();
mIsCustomizedButtonUsed = device instanceof InfoMediaDevice;
}
@OnLifecycleEvent(ON_START)
public void onStart() {
if (mLocalMediaManager == null) {
mLocalMediaManager = new LocalMediaManager(mContext, mPackageName, null);
}
mLocalMediaManager.registerCallback(this);
mLocalMediaManager.startScan();
}
@OnLifecycleEvent(ON_STOP)
public void onStop() {
mLocalMediaManager.unregisterCallback(this);
mLocalMediaManager.stopScan();
}
} }

View File

@@ -74,4 +74,31 @@ public interface PanelContent extends Instrumentable {
default Intent getHeaderIconIntent() { default Intent getHeaderIconIntent() {
return null; return null;
} }
/**
* @return {@code true} to enable custom button to replace see more button,
* {@code false} otherwise.
*/
default boolean isCustomizedButtonUsed() {
return false;
}
/**
* @return a string for the title of the custom button.
*/
default CharSequence getCustomButtonTitle() {
return null;
}
/**
* Implement the click event for custom button.
*/
default void onClickCustomizedButton() {}
/**
* Register to start receiving callbacks for custom button events.
*
* @param callback the callback to add.
*/
default void registerCallback(PanelCustomizedButtonCallback callback) {}
} }

View File

@@ -0,0 +1,29 @@
/*
* 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.panel;
/**
* PanelCustomizedButtonCallback provides a callback interface for {@link PanelFragment} to receive
* events from {@link PanelContent}.
*/
public interface PanelCustomizedButtonCallback {
/**
* It will be called when customized button state is changed. For example, custom button
* would be hidden for specific behavior.
*/
void onCustomizedButtonStateChanged();
}

View File

@@ -41,6 +41,7 @@ import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.IconCompat; import androidx.core.graphics.drawable.IconCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@@ -53,6 +54,7 @@ import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.FeatureFactory;
import com.android.settings.panel.PanelLoggingContract.PanelClosedKeys; import com.android.settings.panel.PanelLoggingContract.PanelClosedKeys;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.utils.ThreadUtils;
import com.google.android.setupdesign.DividerItemDecoration; import com.google.android.setupdesign.DividerItemDecoration;
@@ -183,6 +185,11 @@ public class PanelFragment extends Fragment {
activity.finish(); activity.finish();
} }
mPanel.registerCallback(new LocalPanelCallback());
if (mPanel instanceof LifecycleObserver) {
getLifecycle().addObserver((LifecycleObserver) mPanel);
}
mMetricsProvider = FeatureFactory.getFactory(activity).getMetricsFeatureProvider(); mMetricsProvider = FeatureFactory.getFactory(activity).getMetricsFeatureProvider();
mPanelSlices.setLayoutManager(new LinearLayoutManager((activity))); mPanelSlices.setLayoutManager(new LinearLayoutManager((activity)));
@@ -208,8 +215,15 @@ public class PanelFragment extends Fragment {
mSeeMoreButton.setOnClickListener(getSeeMoreListener()); mSeeMoreButton.setOnClickListener(getSeeMoreListener());
mDoneButton.setOnClickListener(getCloseListener()); mDoneButton.setOnClickListener(getCloseListener());
// If getSeeMoreIntent() is null, hide the mSeeMoreButton. if (mPanel.isCustomizedButtonUsed()) {
if (mPanel.getSeeMoreIntent() == null) { final CharSequence customTitle = mPanel.getCustomButtonTitle();
if (TextUtils.isEmpty(customTitle)) {
mSeeMoreButton.setVisibility(View.GONE);
} else {
mSeeMoreButton.setText(customTitle);
}
} else if (mPanel.getSeeMoreIntent() == null) {
// If getSeeMoreIntent() is null hide the mSeeMoreButton.
mSeeMoreButton.setVisibility(View.GONE); mSeeMoreButton.setVisibility(View.GONE);
} }
@@ -371,9 +385,13 @@ public class PanelFragment extends Fragment {
View.OnClickListener getSeeMoreListener() { View.OnClickListener getSeeMoreListener() {
return (v) -> { return (v) -> {
mPanelClosedKey = PanelClosedKeys.KEY_SEE_MORE; mPanelClosedKey = PanelClosedKeys.KEY_SEE_MORE;
if (mPanel.isCustomizedButtonUsed()) {
mPanel.onClickCustomizedButton();
} else {
final FragmentActivity activity = getActivity(); final FragmentActivity activity = getActivity();
activity.startActivityForResult(mPanel.getSeeMoreIntent(), 0); activity.startActivityForResult(mPanel.getSeeMoreIntent(), 0);
activity.finish(); activity.finish();
}
}; };
} }
@@ -392,4 +410,15 @@ public class PanelFragment extends Fragment {
activity.startActivity(mPanel.getHeaderIconIntent()); activity.startActivity(mPanel.getHeaderIconIntent());
}; };
} }
class LocalPanelCallback implements PanelCustomizedButtonCallback {
@Override
public void onCustomizedButtonStateChanged() {
ThreadUtils.postOnMainThread(() -> {
mSeeMoreButton.setVisibility(
mPanel.isCustomizedButtonUsed() ? View.VISIBLE : View.GONE);
});
}
}
} }

View File

@@ -21,7 +21,9 @@ import static com.android.settings.media.MediaOutputSlice.MEDIA_PACKAGE_NAME;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.content.Context; import android.content.Context;
@@ -32,6 +34,9 @@ import android.net.Uri;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.slices.CustomSliceRegistry; import com.android.settings.slices.CustomSliceRegistry;
import com.android.settingslib.media.InfoMediaDevice;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.PhoneMediaDevice;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -58,6 +63,11 @@ public class MediaOutputPanelTest {
@Mock @Mock
private MediaMetadata mMediaMetadata; private MediaMetadata mMediaMetadata;
@Mock
private LocalMediaManager mLocalMediaManager;
@Mock
private PanelCustomizedButtonCallback mCallback;
private MediaOutputPanel mPanel; private MediaOutputPanel mPanel;
private Context mContext; private Context mContext;
private List<MediaController> mMediaControllers = new ArrayList<>(); private List<MediaController> mMediaControllers = new ArrayList<>();
@@ -67,12 +77,16 @@ public class MediaOutputPanelTest {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application); mContext = spy(RuntimeEnvironment.application);
mMediaControllers.add(mMediaController); mMediaControllers.add(mMediaController);
when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGENAME); when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGENAME);
when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers); when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers);
when(mContext.getApplicationContext()).thenReturn(mContext); when(mContext.getApplicationContext()).thenReturn(mContext);
when(mContext.getSystemService(MediaSessionManager.class)).thenReturn(mMediaSessionManager); when(mContext.getSystemService(MediaSessionManager.class)).thenReturn(mMediaSessionManager);
mPanel = MediaOutputPanel.create(mContext, TEST_PACKAGENAME); mPanel = MediaOutputPanel.create(mContext, TEST_PACKAGENAME);
mPanel.mLocalMediaManager = mLocalMediaManager;
mPanel.registerCallback(mCallback);
} }
@Test @Test
@@ -94,6 +108,63 @@ public class MediaOutputPanelTest {
assertThat(mPanel.getSeeMoreIntent()).isNull(); assertThat(mPanel.getSeeMoreIntent()).isNull();
} }
@Test
public void onStart_shouldRegisterCallback() {
mPanel.onStart();
verify(mLocalMediaManager).registerCallback(any());
verify(mLocalMediaManager).startScan();
}
@Test
public void onStop_shouldUnregisterCallback() {
mPanel.onStop();
verify(mLocalMediaManager).unregisterCallback(any());
verify(mLocalMediaManager).stopScan();
}
@Test
public void onSelectedDeviceStateChanged_shouldDispatchCustomButtonStateChanged() {
mPanel.onSelectedDeviceStateChanged(null, 0);
verify(mCallback).onCustomizedButtonStateChanged();
}
@Test
public void onDeviceListUpdate_shouldDispatchCustomButtonStateChanged() {
mPanel.onDeviceListUpdate(null);
verify(mCallback).onCustomizedButtonStateChanged();
}
@Test
public void onDeviceAttributesChanged_shouldDispatchCustomButtonStateChanged() {
mPanel.onDeviceAttributesChanged();
verify(mCallback).onCustomizedButtonStateChanged();
}
@Test
public void currentConnectDeviceIsInfoDevice_useCustomButtonIsTrue() {
final InfoMediaDevice infoMediaDevice = mock(InfoMediaDevice.class);
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(infoMediaDevice);
mPanel.onDeviceAttributesChanged();
assertThat(mPanel.isCustomizedButtonUsed()).isTrue();
}
@Test
public void currentConnectDeviceIsNotInfoDevice_useCustomButtonIsFalse() {
final PhoneMediaDevice phoneMediaDevice = mock(PhoneMediaDevice.class);
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(phoneMediaDevice);
mPanel.onDeviceAttributesChanged();
assertThat(mPanel.isCustomizedButtonUsed()).isFalse();
}
@Test @Test
public void getTitle_withMetadata_returnArtistName() { public void getTitle_withMetadata_returnArtistName() {
when(mMediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST)).thenReturn(TEST_ARTIST); when(mMediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST)).thenReturn(TEST_ARTIST);