Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8eed6111 authored by Andre Le's avatar Andre Le Committed by Android (Google) Code Review
Browse files

Merge changes from topic "MediaRouteChooserContentManager" into main

* changes:
  CastDetailsView: Add unit tests for MediaRouteChooserContentManager
  CastDetailsView: Migrate functionalities to chooser content manager
parents daa6e12a 470dc53a
Loading
Loading
Loading
Loading
+208 −4
Original line number Diff line number Diff line
@@ -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();
        }
    }
}
+17 −170
Original line number Diff line number Diff line
@@ -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,127 +107,28 @@ 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();
        }
    }

    static boolean isLightTheme(Context context) {
        TypedValue value = new TypedValue();
        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();
    public void dismissView() {
        dismiss();
    }
        }
    }

    private final class MediaRouterCallback extends MediaRouter.SimpleCallback {
    @Override
        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
            refreshRoutes();
    public boolean showProgressBarWhenEmpty() {
        return mShowProgressBarWhenEmpty;
    }

        @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());
        }
    static boolean isLightTheme(Context context) {
        TypedValue value = new TypedValue();
        return context.getTheme().resolveAttribute(R.attr.isLightTheme, value, true)
                && value.data != 0;
    }
}
+182 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}