diff --git a/res/values/strings.xml b/res/values/strings.xml index e27fbe3e49c..82e50cb5a14 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -11716,4 +11716,6 @@ + + Stop casting diff --git a/src/com/android/settings/panel/MediaOutputPanel.java b/src/com/android/settings/panel/MediaOutputPanel.java index d6030cc4096..7b69fe382b7 100644 --- a/src/com/android/settings/panel/MediaOutputPanel.java +++ b/src/com/android/settings/panel/MediaOutputPanel.java @@ -16,6 +16,9 @@ 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.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI; @@ -35,9 +38,15 @@ import android.text.TextUtils; import android.util.Log; 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.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.List; @@ -49,13 +58,20 @@ import java.util.List; * Displays Media output item *

*/ -public class MediaOutputPanel implements PanelContent { +public class MediaOutputPanel implements PanelContent, LocalMediaManager.DeviceCallback, + LifecycleObserver { private static final String TAG = "MediaOutputPanel"; private final Context mContext; private final String mPackageName; + private PanelCustomizedButtonCallback mCallback; + private boolean mIsCustomizedButtonUsed = true; + + @VisibleForTesting + LocalMediaManager mLocalMediaManager; + private MediaSessionManager mMediaSessionManager; private MediaController mMediaController; @@ -65,8 +81,9 @@ public class MediaOutputPanel implements PanelContent { private MediaOutputPanel(Context context, String packageName) { mContext = context.getApplicationContext(); - mPackageName = packageName; - if (mPackageName != null) { + mPackageName = TextUtils.isEmpty(packageName) ? "" : packageName; + + if (!TextUtils.isEmpty(mPackageName)) { mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class); for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) { if (TextUtils.equals(controller.getPackageName(), mPackageName)) { @@ -75,6 +92,7 @@ public class MediaOutputPanel implements PanelContent { } } } + if (mMediaController == null) { Log.e(TAG, "Unable to find " + mPackageName + " media controller"); } @@ -156,8 +174,69 @@ public class MediaOutputPanel implements PanelContent { 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 public int getMetricsCategory() { return SettingsEnums.PANEL_MEDIA_OUTPUT; } + + @Override + public void onSelectedDeviceStateChanged(MediaDevice device, int state) { + dispatchCustomButtonStateChanged(); + } + + @Override + public void onDeviceListUpdate(List 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(); + } } diff --git a/src/com/android/settings/panel/PanelContent.java b/src/com/android/settings/panel/PanelContent.java index 60b9ed7ab18..352a304e24d 100644 --- a/src/com/android/settings/panel/PanelContent.java +++ b/src/com/android/settings/panel/PanelContent.java @@ -74,4 +74,31 @@ public interface PanelContent extends Instrumentable { default Intent getHeaderIconIntent() { 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) {} } diff --git a/src/com/android/settings/panel/PanelCustomizedButtonCallback.java b/src/com/android/settings/panel/PanelCustomizedButtonCallback.java new file mode 100644 index 00000000000..8b4290ff1d3 --- /dev/null +++ b/src/com/android/settings/panel/PanelCustomizedButtonCallback.java @@ -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(); +} diff --git a/src/com/android/settings/panel/PanelFragment.java b/src/com/android/settings/panel/PanelFragment.java index 0f467582443..31e2ac6ee55 100644 --- a/src/com/android/settings/panel/PanelFragment.java +++ b/src/com/android/settings/panel/PanelFragment.java @@ -41,6 +41,7 @@ import androidx.annotation.Nullable; import androidx.core.graphics.drawable.IconCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LiveData; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -53,6 +54,7 @@ import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; import com.android.settings.panel.PanelLoggingContract.PanelClosedKeys; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.utils.ThreadUtils; import com.google.android.setupdesign.DividerItemDecoration; @@ -183,6 +185,11 @@ public class PanelFragment extends Fragment { activity.finish(); } + mPanel.registerCallback(new LocalPanelCallback()); + if (mPanel instanceof LifecycleObserver) { + getLifecycle().addObserver((LifecycleObserver) mPanel); + } + mMetricsProvider = FeatureFactory.getFactory(activity).getMetricsFeatureProvider(); mPanelSlices.setLayoutManager(new LinearLayoutManager((activity))); @@ -208,8 +215,15 @@ public class PanelFragment extends Fragment { mSeeMoreButton.setOnClickListener(getSeeMoreListener()); mDoneButton.setOnClickListener(getCloseListener()); - // If getSeeMoreIntent() is null, hide the mSeeMoreButton. - if (mPanel.getSeeMoreIntent() == null) { + if (mPanel.isCustomizedButtonUsed()) { + 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); } @@ -371,9 +385,13 @@ public class PanelFragment extends Fragment { View.OnClickListener getSeeMoreListener() { return (v) -> { mPanelClosedKey = PanelClosedKeys.KEY_SEE_MORE; - final FragmentActivity activity = getActivity(); - activity.startActivityForResult(mPanel.getSeeMoreIntent(), 0); - activity.finish(); + if (mPanel.isCustomizedButtonUsed()) { + mPanel.onClickCustomizedButton(); + } else { + final FragmentActivity activity = getActivity(); + activity.startActivityForResult(mPanel.getSeeMoreIntent(), 0); + activity.finish(); + } }; } @@ -392,4 +410,15 @@ public class PanelFragment extends Fragment { activity.startActivity(mPanel.getHeaderIconIntent()); }; } + + class LocalPanelCallback implements PanelCustomizedButtonCallback { + + @Override + public void onCustomizedButtonStateChanged() { + ThreadUtils.postOnMainThread(() -> { + mSeeMoreButton.setVisibility( + mPanel.isCustomizedButtonUsed() ? View.VISIBLE : View.GONE); + }); + } + } } diff --git a/tests/robotests/src/com/android/settings/panel/MediaOutputPanelTest.java b/tests/robotests/src/com/android/settings/panel/MediaOutputPanelTest.java index fb7f0ae62b9..a4c94ca760d 100644 --- a/tests/robotests/src/com/android/settings/panel/MediaOutputPanelTest.java +++ b/tests/robotests/src/com/android/settings/panel/MediaOutputPanelTest.java @@ -21,7 +21,9 @@ import static com.android.settings.media.MediaOutputSlice.MEDIA_PACKAGE_NAME; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; @@ -32,6 +34,9 @@ import android.net.Uri; import com.android.settings.R; 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.Test; @@ -58,6 +63,11 @@ public class MediaOutputPanelTest { @Mock private MediaMetadata mMediaMetadata; + @Mock + private LocalMediaManager mLocalMediaManager; + @Mock + private PanelCustomizedButtonCallback mCallback; + private MediaOutputPanel mPanel; private Context mContext; private List mMediaControllers = new ArrayList<>(); @@ -67,12 +77,16 @@ public class MediaOutputPanelTest { MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); + mMediaControllers.add(mMediaController); when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGENAME); when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers); when(mContext.getApplicationContext()).thenReturn(mContext); when(mContext.getSystemService(MediaSessionManager.class)).thenReturn(mMediaSessionManager); + mPanel = MediaOutputPanel.create(mContext, TEST_PACKAGENAME); + mPanel.mLocalMediaManager = mLocalMediaManager; + mPanel.registerCallback(mCallback); } @Test @@ -94,6 +108,63 @@ public class MediaOutputPanelTest { 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 public void getTitle_withMetadata_returnArtistName() { when(mMediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST)).thenReturn(TEST_ARTIST);