diff options
author | 2025-03-19 23:06:42 -0700 | |
---|---|---|
committer | 2025-03-19 23:06:42 -0700 | |
commit | 8eed6111d35fc29b26a4db3492d0d891b24d880a (patch) | |
tree | ad2e45e921891eb39947ec38a37f3765f3ae64b6 | |
parent | daa6e12a0b1e75c28cdb26870167cce7fc9c9267 (diff) | |
parent | 470dc53a9826d067b433d64103ec242d80b4cc8d (diff) |
Merge changes from topic "MediaRouteChooserContentManager" into main
* changes:
CastDetailsView: Add unit tests for MediaRouteChooserContentManager
CastDetailsView: Migrate functionalities to chooser content manager
3 files changed, 407 insertions, 174 deletions
diff --git a/core/java/com/android/internal/app/MediaRouteChooserContentManager.java b/core/java/com/android/internal/app/MediaRouteChooserContentManager.java index 09c6f5e6caaa..64538fdbdac1 100644 --- a/core/java/com/android/internal/app/MediaRouteChooserContentManager.java +++ b/core/java/com/android/internal/app/MediaRouteChooserContentManager.java @@ -17,21 +17,57 @@ package com.android.internal.app; import android.content.Context; +import android.media.MediaRouter; +import android.text.TextUtils; import android.view.Gravity; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.ListView; +import android.widget.TextView; import com.android.internal.R; +import java.util.Comparator; + public class MediaRouteChooserContentManager { + /** + * A delegate interface that a MediaRouteChooser UI should implement. It allows the content + * manager to inform the UI of any UI changes that need to be made in response to content + * updates. + */ + public interface Delegate { + /** + * Dismiss the UI to transition to a different workflow. + */ + void dismissView(); + + /** + * Returns true if the progress bar should be shown when the list view is empty. + */ + boolean showProgressBarWhenEmpty(); + } + Context mContext; + Delegate mDelegate; - private final boolean mShowProgressBarWhenEmpty; + private final MediaRouter mRouter; + private final MediaRouterCallback mCallback; - public MediaRouteChooserContentManager(Context context, boolean showProgressBarWhenEmpty) { + private int mRouteTypes; + private RouteAdapter mAdapter; + private boolean mAttachedToWindow; + + public MediaRouteChooserContentManager(Context context, Delegate delegate) { mContext = context; - mShowProgressBarWhenEmpty = showProgressBarWhenEmpty; + mDelegate = delegate; + + mRouter = context.getSystemService(MediaRouter.class); + mCallback = new MediaRouterCallback(); + mAdapter = new RouteAdapter(mContext); } /** @@ -41,9 +77,11 @@ public class MediaRouteChooserContentManager { public void bindViews(View containerView) { View emptyView = containerView.findViewById(android.R.id.empty); ListView listView = containerView.findViewById(R.id.media_route_list); + listView.setAdapter(mAdapter); + listView.setOnItemClickListener(mAdapter); listView.setEmptyView(emptyView); - if (!mShowProgressBarWhenEmpty) { + if (!mDelegate.showProgressBarWhenEmpty()) { containerView.findViewById(R.id.media_route_progress_bar).setVisibility(View.GONE); // Center the empty view when the progress bar is not shown. @@ -53,4 +91,170 @@ public class MediaRouteChooserContentManager { emptyView.setLayoutParams(params); } } + + /** + * Called when this UI is attached to a window.. + */ + public void onAttachedToWindow() { + mAttachedToWindow = true; + mRouter.addCallback(mRouteTypes, mCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); + refreshRoutes(); + } + + /** + * Called when this UI is detached from a window.. + */ + public void onDetachedFromWindow() { + mAttachedToWindow = false; + mRouter.removeCallback(mCallback); + } + + /** + * Gets the media route types for filtering the routes that the user can + * select using the media route chooser dialog. + * + * @return The route types. + */ + public int getRouteTypes() { + return mRouteTypes; + } + + /** + * Sets the types of routes that will be shown in the media route chooser dialog + * launched by this button. + * + * @param types The route types to match. + */ + public void setRouteTypes(int types) { + if (mRouteTypes != types) { + mRouteTypes = types; + + if (mAttachedToWindow) { + mRouter.removeCallback(mCallback); + mRouter.addCallback(types, mCallback, + MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); + } + + refreshRoutes(); + } + } + + /** + * Refreshes the list of routes that are shown in the chooser dialog. + */ + public void refreshRoutes() { + if (mAttachedToWindow) { + mAdapter.update(); + } + } + + /** + * Returns true if the route should be included in the list. + * <p> + * The default implementation returns true for enabled non-default routes that + * match the route types. Subclasses can override this method to filter routes + * differently. + * </p> + * + * @param route The route to consider, never null. + * @return True if the route should be included in the chooser dialog. + */ + public boolean onFilterRoute(MediaRouter.RouteInfo route) { + return !route.isDefault() && route.isEnabled() && route.matchesTypes(mRouteTypes); + } + + private final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo> + implements AdapterView.OnItemClickListener { + private final LayoutInflater mInflater; + + RouteAdapter(Context context) { + super(context, 0); + mInflater = LayoutInflater.from(context); + } + + public void update() { + clear(); + final int count = mRouter.getRouteCount(); + for (int i = 0; i < count; i++) { + MediaRouter.RouteInfo route = mRouter.getRouteAt(i); + if (onFilterRoute(route)) { + add(route); + } + } + sort(RouteComparator.sInstance); + notifyDataSetChanged(); + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + return getItem(position).isEnabled(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + view = mInflater.inflate(R.layout.media_route_list_item, parent, false); + } + MediaRouter.RouteInfo route = getItem(position); + TextView text1 = view.findViewById(android.R.id.text1); + TextView text2 = view.findViewById(android.R.id.text2); + text1.setText(route.getName()); + CharSequence description = route.getDescription(); + if (TextUtils.isEmpty(description)) { + text2.setVisibility(View.GONE); + text2.setText(""); + } else { + text2.setVisibility(View.VISIBLE); + text2.setText(description); + } + view.setEnabled(route.isEnabled()); + return view; + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + MediaRouter.RouteInfo route = getItem(position); + if (route.isEnabled()) { + route.select(); + mDelegate.dismissView(); + } + } + } + + private static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> { + public static final RouteComparator sInstance = new RouteComparator(); + + @Override + public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) { + return lhs.getName().toString().compareTo(rhs.getName().toString()); + } + } + + private final class MediaRouterCallback extends MediaRouter.SimpleCallback { + @Override + public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) { + refreshRoutes(); + } + + @Override + public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) { + refreshRoutes(); + } + + @Override + public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) { + refreshRoutes(); + } + + @Override + public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) { + mDelegate.dismissView(); + } + } } diff --git a/core/java/com/android/internal/app/MediaRouteChooserDialog.java b/core/java/com/android/internal/app/MediaRouteChooserDialog.java index 5030a143ea94..fc7ed89f395c 100644 --- a/core/java/com/android/internal/app/MediaRouteChooserDialog.java +++ b/core/java/com/android/internal/app/MediaRouteChooserDialog.java @@ -19,23 +19,14 @@ package com.android.internal.app; import android.app.AlertDialog; import android.content.Context; import android.media.MediaRouter; -import android.media.MediaRouter.RouteInfo; import android.os.Bundle; -import android.text.TextUtils; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; import android.widget.Button; -import android.widget.ListView; -import android.widget.TextView; import com.android.internal.R; -import java.util.Comparator; - /** * This class implements the route chooser dialog for {@link MediaRouter}. * <p> @@ -47,15 +38,11 @@ import java.util.Comparator; * * TODO: Move this back into the API, as in the support library media router. */ -public class MediaRouteChooserDialog extends AlertDialog { - private final MediaRouter mRouter; - private final MediaRouterCallback mCallback; - - private int mRouteTypes; +public class MediaRouteChooserDialog extends AlertDialog implements + MediaRouteChooserContentManager.Delegate { private View.OnClickListener mExtendedSettingsClickListener; - private RouteAdapter mAdapter; private Button mExtendedSettingsButton; - private boolean mAttachedToWindow; + private final boolean mShowProgressBarWhenEmpty; private final MediaRouteChooserContentManager mContentManager; @@ -66,19 +53,8 @@ public class MediaRouteChooserDialog extends AlertDialog { public MediaRouteChooserDialog(Context context, int theme, boolean showProgressBarWhenEmpty) { super(context, theme); - mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE); - mCallback = new MediaRouterCallback(); - mContentManager = new MediaRouteChooserContentManager(context, showProgressBarWhenEmpty); - } - - /** - * Gets the media route types for filtering the routes that the user can - * select using the media route chooser dialog. - * - * @return The route types. - */ - public int getRouteTypes() { - return mRouteTypes; + mShowProgressBarWhenEmpty = showProgressBarWhenEmpty; + mContentManager = new MediaRouteChooserContentManager(context, this); } /** @@ -88,17 +64,7 @@ public class MediaRouteChooserDialog extends AlertDialog { * @param types The route types to match. */ public void setRouteTypes(int types) { - if (mRouteTypes != types) { - mRouteTypes = types; - - if (mAttachedToWindow) { - mRouter.removeCallback(mCallback); - mRouter.addCallback(types, mCallback, - MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); - } - - refreshRoutes(); - } + mContentManager.setRouteTypes(types); } public void setExtendedSettingsClickListener(View.OnClickListener listener) { @@ -108,21 +74,6 @@ public class MediaRouteChooserDialog extends AlertDialog { } } - /** - * Returns true if the route should be included in the list. - * <p> - * The default implementation returns true for enabled non-default routes that - * match the route types. Subclasses can override this method to filter routes - * differently. - * </p> - * - * @param route The route to consider, never null. - * @return True if the route should be included in the chooser dialog. - */ - public boolean onFilterRoute(MediaRouter.RouteInfo route) { - return !route.isDefault() && route.isEnabled() && route.matchesTypes(mRouteTypes); - } - @Override protected void onCreate(Bundle savedInstanceState) { // Note: setView must be called before super.onCreate(). @@ -130,7 +81,7 @@ public class MediaRouteChooserDialog extends AlertDialog { R.layout.media_route_chooser_dialog, null); setView(containerView); - setTitle(mRouteTypes == MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY + setTitle(mContentManager.getRouteTypes() == MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY ? R.string.media_route_chooser_title_for_remote_display : R.string.media_route_chooser_title); @@ -139,11 +90,6 @@ public class MediaRouteChooserDialog extends AlertDialog { super.onCreate(savedInstanceState); - mAdapter = new RouteAdapter(getContext()); - ListView listView = findViewById(R.id.media_route_list); - listView.setAdapter(mAdapter); - listView.setOnItemClickListener(mAdapter); - mExtendedSettingsButton = findViewById(R.id.media_route_extended_settings_button); updateExtendedSettingsButton(); @@ -161,27 +107,23 @@ public class MediaRouteChooserDialog extends AlertDialog { @Override public void onAttachedToWindow() { super.onAttachedToWindow(); - - mAttachedToWindow = true; - mRouter.addCallback(mRouteTypes, mCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); - refreshRoutes(); + mContentManager.onAttachedToWindow(); } @Override public void onDetachedFromWindow() { - mAttachedToWindow = false; - mRouter.removeCallback(mCallback); - + mContentManager.onDetachedFromWindow(); super.onDetachedFromWindow(); } - /** - * Refreshes the list of routes that are shown in the chooser dialog. - */ - public void refreshRoutes() { - if (mAttachedToWindow) { - mAdapter.update(); - } + @Override + public void dismissView() { + dismiss(); + } + + @Override + public boolean showProgressBarWhenEmpty() { + return mShowProgressBarWhenEmpty; } static boolean isLightTheme(Context context) { @@ -189,99 +131,4 @@ public class MediaRouteChooserDialog extends AlertDialog { return context.getTheme().resolveAttribute(R.attr.isLightTheme, value, true) && value.data != 0; } - - private final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo> - implements ListView.OnItemClickListener { - private final LayoutInflater mInflater; - - public RouteAdapter(Context context) { - super(context, 0); - mInflater = LayoutInflater.from(context); - } - - public void update() { - clear(); - final int count = mRouter.getRouteCount(); - for (int i = 0; i < count; i++) { - MediaRouter.RouteInfo route = mRouter.getRouteAt(i); - if (onFilterRoute(route)) { - add(route); - } - } - sort(RouteComparator.sInstance); - notifyDataSetChanged(); - } - - @Override - public boolean areAllItemsEnabled() { - return false; - } - - @Override - public boolean isEnabled(int position) { - return getItem(position).isEnabled(); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = convertView; - if (view == null) { - view = mInflater.inflate(R.layout.media_route_list_item, parent, false); - } - MediaRouter.RouteInfo route = getItem(position); - TextView text1 = view.findViewById(android.R.id.text1); - TextView text2 = view.findViewById(android.R.id.text2); - text1.setText(route.getName()); - CharSequence description = route.getDescription(); - if (TextUtils.isEmpty(description)) { - text2.setVisibility(View.GONE); - text2.setText(""); - } else { - text2.setVisibility(View.VISIBLE); - text2.setText(description); - } - view.setEnabled(route.isEnabled()); - return view; - } - - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - MediaRouter.RouteInfo route = getItem(position); - if (route.isEnabled()) { - route.select(); - dismiss(); - } - } - } - - private final class MediaRouterCallback extends MediaRouter.SimpleCallback { - @Override - public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) { - refreshRoutes(); - } - - @Override - public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) { - refreshRoutes(); - } - - @Override - public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) { - refreshRoutes(); - } - - @Override - public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { - dismiss(); - } - } - - private static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> { - public static final RouteComparator sInstance = new RouteComparator(); - - @Override - public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) { - return lhs.getName().toString().compareTo(rhs.getName().toString()); - } - } } diff --git a/core/tests/coretests/src/com/android/internal/app/MediaRouteChooserContentManagerTest.kt b/core/tests/coretests/src/com/android/internal/app/MediaRouteChooserContentManagerTest.kt new file mode 100644 index 000000000000..bbed6e0c3618 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/MediaRouteChooserContentManagerTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2025 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.internal.app + +import android.content.Context +import android.media.MediaRouter +import android.testing.TestableLooper.RunWithLooper +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.R +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +@SmallTest +@RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidJUnit4::class) +class MediaRouteChooserContentManagerTest { + private val context: Context = getInstrumentation().context + + @Test + fun bindViews_showProgressBarWhenEmptyTrue_progressBarVisible() { + val delegate = mock<MediaRouteChooserContentManager.Delegate> { + on { showProgressBarWhenEmpty() } doReturn true + } + val contentManager = MediaRouteChooserContentManager(context, delegate) + val containerView = inflateMediaRouteChooserDialog() + contentManager.bindViews(containerView) + + assertThat(containerView.findViewById<View>(R.id.media_route_progress_bar).visibility) + .isEqualTo(View.VISIBLE) + } + + @Test + fun bindViews_showProgressBarWhenEmptyFalse_progressBarNotVisible() { + val delegate = mock<MediaRouteChooserContentManager.Delegate> { + on { showProgressBarWhenEmpty() } doReturn false + } + val contentManager = MediaRouteChooserContentManager(context, delegate) + val containerView = inflateMediaRouteChooserDialog() + contentManager.bindViews(containerView) + val emptyView = containerView.findViewById<View>(android.R.id.empty) + val emptyViewLayout = emptyView.layoutParams as? LinearLayout.LayoutParams + + assertThat(containerView.findViewById<View>(R.id.media_route_progress_bar).visibility) + .isEqualTo(View.GONE) + assertThat(emptyView.visibility).isEqualTo(View.VISIBLE) + assertThat(emptyViewLayout?.gravity).isEqualTo(Gravity.CENTER) + } + + @Test + fun onFilterRoute_routeDefault_returnsFalse() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val contentManager = MediaRouteChooserContentManager(context, delegate) + val route: MediaRouter.RouteInfo = mock<MediaRouter.RouteInfo> { + on { isDefault } doReturn true + } + + assertThat(contentManager.onFilterRoute(route)).isEqualTo(false) + } + + @Test + fun onFilterRoute_routeNotEnabled_returnsFalse() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val contentManager = MediaRouteChooserContentManager(context, delegate) + val route: MediaRouter.RouteInfo = mock<MediaRouter.RouteInfo> { + on { isEnabled } doReturn false + } + + assertThat(contentManager.onFilterRoute(route)).isEqualTo(false) + } + + @Test + fun onFilterRoute_routeNotMatch_returnsFalse() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val contentManager = MediaRouteChooserContentManager(context, delegate) + val route: MediaRouter.RouteInfo = mock<MediaRouter.RouteInfo> { + on { matchesTypes(anyInt()) } doReturn false + } + + assertThat(contentManager.onFilterRoute(route)).isEqualTo(false) + } + + @Test + fun onFilterRoute_returnsTrue() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val contentManager = MediaRouteChooserContentManager(context, delegate) + val route: MediaRouter.RouteInfo = mock<MediaRouter.RouteInfo> { + on { isDefault } doReturn false + on { isEnabled } doReturn true + on { matchesTypes(anyInt()) } doReturn true + } + + assertThat(contentManager.onFilterRoute(route)).isEqualTo(true) + } + + @Test + fun onAttachedToWindow() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val mediaRouter: MediaRouter = mock() + val layoutInflater: LayoutInflater = mock() + val context: Context = mock<Context> { + on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE + on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter + on { getSystemService(Context.LAYOUT_INFLATER_SERVICE) } doReturn layoutInflater + } + val contentManager = MediaRouteChooserContentManager(context, delegate) + contentManager.routeTypes = MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY + + contentManager.onAttachedToWindow() + + verify(mediaRouter).addCallback(eq(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY), any(), + eq(MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN)) + } + + @Test + fun onDetachedFromWindow() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val layoutInflater: LayoutInflater = mock() + val mediaRouter: MediaRouter = mock() + val context: Context = mock<Context> { + on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE + on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter + on { getSystemService(Context.LAYOUT_INFLATER_SERVICE) } doReturn layoutInflater + } + val contentManager = MediaRouteChooserContentManager(context, delegate) + + contentManager.onDetachedFromWindow() + + verify(mediaRouter).removeCallback(any()) + } + + @Test + fun setRouteTypes() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val mediaRouter: MediaRouter = mock() + val layoutInflater: LayoutInflater = mock() + val context: Context = mock<Context> { + on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE + on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter + on { getSystemService(Context.LAYOUT_INFLATER_SERVICE) } doReturn layoutInflater + } + val contentManager = MediaRouteChooserContentManager(context, delegate) + contentManager.onAttachedToWindow() + + contentManager.routeTypes = MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY + + assertThat(contentManager.routeTypes).isEqualTo(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) + verify(mediaRouter).addCallback(eq(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY), any(), + eq(MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN)) + } + + private fun inflateMediaRouteChooserDialog(): View { + return LayoutInflater.from(context) + .inflate(R.layout.media_route_chooser_dialog, null, false) + } +} |