diff options
22 files changed, 1406 insertions, 355 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index ea2ae0c9e15f..4734e8a75de6 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -23189,9 +23189,10 @@ package android.media {    public abstract static class MediaRouter2.RouteCallback {      ctor public MediaRouter2.RouteCallback(); -    method public void onRoutesAdded(@NonNull java.util.List<android.media.MediaRoute2Info>); -    method public void onRoutesChanged(@NonNull java.util.List<android.media.MediaRoute2Info>); -    method public void onRoutesRemoved(@NonNull java.util.List<android.media.MediaRoute2Info>); +    method @Deprecated public void onRoutesAdded(@NonNull java.util.List<android.media.MediaRoute2Info>); +    method @Deprecated public void onRoutesChanged(@NonNull java.util.List<android.media.MediaRoute2Info>); +    method @Deprecated public void onRoutesRemoved(@NonNull java.util.List<android.media.MediaRoute2Info>); +    method public void onRoutesUpdated(@NonNull java.util.List<android.media.MediaRoute2Info>);    }    public class MediaRouter2.RoutingController { diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index b233e5453c05..b21c5b35e24b 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -226,6 +226,8 @@ public class Editor {      final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);      boolean mAllowUndo = true; +    private int mLastToolType = MotionEvent.TOOL_TYPE_UNKNOWN; +      private final MetricsLogger mMetricsLogger = new MetricsLogger();      // Cursor Controllers. @@ -1732,6 +1734,9 @@ public class Editor {      @VisibleForTesting      public void onTouchEvent(MotionEvent event) {          final boolean filterOutEvent = shouldFilterOutTouchEvent(event); + +        mLastToolType = event.getToolType(event.getActionIndex()); +          mLastButtonState = event.getButtonState();          if (filterOutEvent) {              if (event.getActionMasked() == MotionEvent.ACTION_UP) { @@ -1784,7 +1789,7 @@ public class Editor {      }      private void showFloatingToolbar() { -        if (mTextActionMode != null) { +        if (mTextActionMode != null && showUIForFingerInput()) {              // Delay "show" so it doesn't interfere with click confirmations              // or double-clicks that could "dismiss" the floating toolbar.              int delay = ViewConfiguration.getDoubleTapTimeout(); @@ -1864,7 +1869,8 @@ public class Editor {              final CursorController cursorController = mTextView.hasSelection()                      ? getSelectionController() : getInsertionController();              if (cursorController != null && !cursorController.isActive() -                    && !cursorController.isCursorBeingModified()) { +                    && !cursorController.isCursorBeingModified() +                    && showUIForFingerInput()) {                  cursorController.show();              }          } @@ -2515,6 +2521,10 @@ public class Editor {              return false;          } +        if (!showUIForFingerInput()) { +            return false; +        } +          ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode);          mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);          registerOnBackInvokedCallback(); @@ -2667,7 +2677,7 @@ public class Editor {                      mTextView.postDelayed(mShowSuggestionRunnable,                              ViewConfiguration.getDoubleTapTimeout());                  } else if (hasInsertionController()) { -                    if (shouldInsertCursor) { +                    if (shouldInsertCursor && showUIForFingerInput()) {                          getInsertionController().show();                      } else {                          getInsertionController().hide(); @@ -5397,7 +5407,8 @@ public class Editor {              final PointF showPosInView = new PointF();              final boolean shouldShow = checkForTransforms() /*check not rotated and compute scale*/                      && !tooLargeTextForMagnifier() -                    && obtainMagnifierShowCoordinates(event, showPosInView); +                    && obtainMagnifierShowCoordinates(event, showPosInView) +                    && showUIForFingerInput();              if (shouldShow) {                  // Make the cursor visible and stop blinking.                  mRenderCursorRegardlessTiming = true; @@ -6343,6 +6354,15 @@ public class Editor {          }      } +    /** +     * Returns true when need to show UIs, e.g. floating toolbar, etc, for finger based interaction. +     * +     * @return true if UIs need to show for finger interaciton. false if UIs are not necessary. +     */ +    public boolean showUIForFingerInput() { +        return mLastToolType != MotionEvent.TOOL_TYPE_MOUSE; +    } +      /** Controller for the insertion cursor. */      @VisibleForTesting      public class InsertionPointCursorController implements CursorController { diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java index a0ec48bc6beb..54a415cfa01b 100644 --- a/core/java/android/widget/SelectionActionModeHelper.java +++ b/core/java/android/widget/SelectionActionModeHelper.java @@ -301,7 +301,11 @@ public final class SelectionActionModeHelper {              final SelectionModifierCursorController controller = mEditor.getSelectionController();              if (controller != null                      && (mTextView.isTextSelectable() || mTextView.isTextEditable())) { -                controller.show(); +                if (mEditor.showUIForFingerInput()) { +                    controller.show(); +                } else { +                    controller.hide(); +                }              }              if (result != null) {                  switch (actionMode) { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt new file mode 100644 index 000000000000..c23cdb610671 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2022 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.wm.shell.flicker.splitscreen + +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.Presubmit +import android.view.WindowManagerPolicyConstants +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.wm.shell.flicker.appWindowBecomesVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test switch back to split pair from recent. + * + * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromRecent` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class SwitchBackToSplitFromRecent(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + +    // TODO(b/231399940): Remove this once we can use recent shortcut to enter split. +    @Before +    open fun before() { +        Assume.assumeTrue(tapl.isTablet) +    } + +    override val transition: FlickerBuilder.() -> Unit +        get() = { +            super.transition(this) +            setup { +                eachRun { +                    primaryApp.launchViaIntent(wmHelper) +                    // TODO(b/231399940): Use recent shortcut to enter split. +                    tapl.launchedAppState.taskbar +                        .openAllApps() +                        .getAppIcon(secondaryApp.appName) +                        .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) +                    SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + +                    tapl.goHome() +                    wmHelper.StateSyncBuilder() +                        .withAppTransitionIdle() +                        .withHomeActivityVisible() +                        .waitForAndVerify() +                } +            } +            transitions { +                tapl.workspace.switchToOverview() +                    .currentTask +                    .open() +                SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) +            } +        } + +    @Presubmit +    @Test +    fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible() + +    @Presubmit +    @Test +    fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp) + +    @Presubmit +    @Test +    fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp) + +    @Presubmit +    @Test +    fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( +        primaryApp, splitLeftTop = false) + +    @Presubmit +    @Test +    fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( +        secondaryApp, splitLeftTop = true) + +    @Presubmit +    @Test +    fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp) + +    @Presubmit +    @Test +    fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) + +    /** {@inheritDoc} */ +    @Postsubmit +    @Test +    override fun entireScreenCovered() = +        super.entireScreenCovered() + +    /** {@inheritDoc} */ +    @Postsubmit +    @Test +    override fun navBarLayerIsVisibleAtStartAndEnd() = +        super.navBarLayerIsVisibleAtStartAndEnd() + +    /** {@inheritDoc} */ +    @Postsubmit +    @Test +    override fun navBarLayerPositionAtStartAndEnd() = +        super.navBarLayerPositionAtStartAndEnd() + +    /** {@inheritDoc} */ +    @Postsubmit +    @Test +    override fun navBarWindowIsAlwaysVisible() = +        super.navBarWindowIsAlwaysVisible() + +    /** {@inheritDoc} */ +    @Postsubmit +    @Test +    override fun statusBarLayerIsVisibleAtStartAndEnd() = +        super.statusBarLayerIsVisibleAtStartAndEnd() + +    /** {@inheritDoc} */ +    @Postsubmit +    @Test +    override fun statusBarLayerPositionAtStartAndEnd() = +        super.statusBarLayerPositionAtStartAndEnd() + +    /** {@inheritDoc} */ +    @Postsubmit +    @Test +    override fun statusBarWindowIsAlwaysVisible() = +        super.statusBarWindowIsAlwaysVisible() + +    /** {@inheritDoc} */ +    @Postsubmit +    @Test +    override fun taskBarLayerIsVisibleAtStartAndEnd() = +        super.taskBarLayerIsVisibleAtStartAndEnd() + +    /** {@inheritDoc} */ +    @Postsubmit +    @Test +    override fun taskBarWindowIsAlwaysVisible() = +        super.taskBarWindowIsAlwaysVisible() + +    /** {@inheritDoc} */ +    @Postsubmit +    @Test +    override fun visibleLayersShownMoreThanOneConsecutiveEntry() = +        super.visibleLayersShownMoreThanOneConsecutiveEntry() + +    /** {@inheritDoc} */ +    @Postsubmit +    @Test +    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = +        super.visibleWindowsShownMoreThanOneConsecutiveEntry() + +    companion object { +        @Parameterized.Parameters(name = "{0}") +        @JvmStatic +        fun getParams(): List<FlickerTestParameter> { +            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( +                repetitions = SplitScreenHelper.TEST_REPETITIONS, +                // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. +                supportedNavigationModes = +                    listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)) +        } +    } +} diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl index fe15f0e67b1d..29bfd1acae17 100644 --- a/media/java/android/media/IMediaRouter2.aidl +++ b/media/java/android/media/IMediaRouter2.aidl @@ -26,9 +26,7 @@ import android.os.Bundle;  oneway interface IMediaRouter2 {      void notifyRouterRegistered(in List<MediaRoute2Info> currentRoutes,              in RoutingSessionInfo currentSystemSessionInfo); -    void notifyRoutesAdded(in List<MediaRoute2Info> routes); -    void notifyRoutesRemoved(in List<MediaRoute2Info> routes); -    void notifyRoutesChanged(in List<MediaRoute2Info> routes); +    void notifyRoutesUpdated(in List<MediaRoute2Info> routes);      void notifySessionCreated(int requestId, in @nullable RoutingSessionInfo sessionInfo);      void notifySessionInfoChanged(in RoutingSessionInfo sessionInfo);      void notifySessionReleased(in RoutingSessionInfo sessionInfo); diff --git a/media/java/android/media/IMediaRouter2Manager.aidl b/media/java/android/media/IMediaRouter2Manager.aidl index 71dc2a781ba9..9f3c3ff89032 100644 --- a/media/java/android/media/IMediaRouter2Manager.aidl +++ b/media/java/android/media/IMediaRouter2Manager.aidl @@ -30,8 +30,6 @@ oneway interface IMediaRouter2Manager {      void notifySessionReleased(in RoutingSessionInfo session);      void notifyDiscoveryPreferenceChanged(String packageName,              in RouteDiscoveryPreference discoveryPreference); -    void notifyRoutesAdded(in List<MediaRoute2Info> routes); -    void notifyRoutesRemoved(in List<MediaRoute2Info> routes); -    void notifyRoutesChanged(in List<MediaRoute2Info> routes); +    void notifyRoutesUpdated(in List<MediaRoute2Info> routes);      void notifyRequestFailed(int requestId, int reason);  } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index a7a21e7a2013..26cb9f8e9ee1 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -132,7 +132,7 @@ public final class MediaRouter2 {      /**       * Stores an auxiliary copy of {@link #mFilteredRoutes} at the time of the last route callback       * dispatch. This is only used to determine what callback a route should be assigned to (added, -     * removed, changed) in {@link #dispatchFilteredRoutesChangedLocked(List)}. +     * removed, changed) in {@link #dispatchFilteredRoutesUpdatedOnHandler(List)}.       */      private volatile ArrayMap<String, MediaRoute2Info> mPreviousRoutes = new ArrayMap<>(); @@ -820,7 +820,7 @@ public final class MediaRouter2 {          }      } -    void dispatchFilteredRoutesChangedLocked(List<MediaRoute2Info> newRoutes) { +    void dispatchFilteredRoutesUpdatedOnHandler(List<MediaRoute2Info> newRoutes) {          List<MediaRoute2Info> addedRoutes = new ArrayList<>();          List<MediaRoute2Info> removedRoutes = new ArrayList<>();          List<MediaRoute2Info> changedRoutes = new ArrayList<>(); @@ -863,29 +863,16 @@ public final class MediaRouter2 {          if (!changedRoutes.isEmpty()) {              notifyRoutesChanged(changedRoutes);          } -    } -    void addRoutesOnHandler(List<MediaRoute2Info> routes) { -        synchronized (mLock) { -            for (MediaRoute2Info route : routes) { -                mRoutes.put(route.getId(), route); -            } -            updateFilteredRoutesLocked(); +        // Note: We don't notify clients of changes in route ordering. +        if (!addedRoutes.isEmpty() || !removedRoutes.isEmpty() || !changedRoutes.isEmpty()) { +            notifyRoutesUpdated(newRoutes);          }      } -    void removeRoutesOnHandler(List<MediaRoute2Info> routes) { -        synchronized (mLock) { -            for (MediaRoute2Info route : routes) { -                mRoutes.remove(route.getId()); -            } -            updateFilteredRoutesLocked(); -        } -    } - -    void changeRoutesOnHandler(List<MediaRoute2Info> routes) { -        List<MediaRoute2Info> changedRoutes = new ArrayList<>(); +    void updateRoutesOnHandler(List<MediaRoute2Info> routes) {          synchronized (mLock) { +            mRoutes.clear();              for (MediaRoute2Info route : routes) {                  mRoutes.put(route.getId(), route);              } @@ -900,8 +887,10 @@ public final class MediaRouter2 {                  Collections.unmodifiableList(                          filterRoutesWithCompositePreferenceLocked(List.copyOf(mRoutes.values())));          mHandler.sendMessage( -                obtainMessage(MediaRouter2::dispatchFilteredRoutesChangedLocked, -                        this, mFilteredRoutes)); +                obtainMessage( +                        MediaRouter2::dispatchFilteredRoutesUpdatedOnHandler, +                        this, +                        mFilteredRoutes));      }      /** @@ -1211,6 +1200,14 @@ public final class MediaRouter2 {          }      } +    private void notifyRoutesUpdated(List<MediaRoute2Info> routes) { +        for (RouteCallbackRecord record : mRouteCallbackRecords) { +            List<MediaRoute2Info> filteredRoutes = +                    filterRoutesWithIndividualPreference(routes, record.mPreference); +            record.mExecutor.execute(() -> record.mRouteCallback.onRoutesUpdated(filteredRoutes)); +        } +    } +      private void notifyPreferredFeaturesChanged(List<String> features) {          for (RouteCallbackRecord record : mRouteCallbackRecords) {              record.mExecutor.execute( @@ -1246,29 +1243,44 @@ public final class MediaRouter2 {      /** Callback for receiving events about media route discovery. */      public abstract static class RouteCallback {          /** -         * Called when routes are added. Whenever you registers a callback, this will be invoked -         * with known routes. +         * Called when routes are added. Whenever you register a callback, this will be invoked with +         * known routes.           *           * @param routes the list of routes that have been added. It's never empty. +         * @deprecated Use {@link #onRoutesUpdated(List)} instead.           */ +        @Deprecated          public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) {}          /**           * Called when routes are removed.           *           * @param routes the list of routes that have been removed. It's never empty. +         * @deprecated Use {@link #onRoutesUpdated(List)} instead.           */ +        @Deprecated          public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {}          /** -         * Called when routes are changed. For example, it is called when the route's name or volume -         * have been changed. +         * Called when the properties of one or more existing routes are changed. For example, it is +         * called when a route's name or volume have changed.           *           * @param routes the list of routes that have been changed. It's never empty. +         * @deprecated Use {@link #onRoutesUpdated(List)} instead.           */ +        @Deprecated          public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {}          /** +         * Called when the route list is updated, which can happen when routes are added, removed, +         * or modified. It will also be called when a route callback is registered. +         * +         * @param routes the updated list of routes filtered by the callback's individual discovery +         *     preferences. +         */ +        public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {} + +        /**           * Called when the client app's preferred features are changed. When this is called, it is           * recommended to {@link #getRoutes()} to get the routes that are currently available to the           * app. @@ -1985,21 +1997,9 @@ public final class MediaRouter2 {          }          @Override -        public void notifyRoutesAdded(List<MediaRoute2Info> routes) { -            mHandler.sendMessage( -                    obtainMessage(MediaRouter2::addRoutesOnHandler, MediaRouter2.this, routes)); -        } - -        @Override -        public void notifyRoutesRemoved(List<MediaRoute2Info> routes) { -            mHandler.sendMessage( -                    obtainMessage(MediaRouter2::removeRoutesOnHandler, MediaRouter2.this, routes)); -        } - -        @Override -        public void notifyRoutesChanged(List<MediaRoute2Info> routes) { +        public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {              mHandler.sendMessage( -                    obtainMessage(MediaRouter2::changeRoutesOnHandler, MediaRouter2.this, routes)); +                    obtainMessage(MediaRouter2::updateRoutesOnHandler, MediaRouter2.this, routes));          }          @Override @@ -2047,17 +2047,7 @@ public final class MediaRouter2 {      class ManagerCallback implements MediaRouter2Manager.Callback {          @Override -        public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) { -            updateAllRoutesFromManager(); -        } - -        @Override -        public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) { -            updateAllRoutesFromManager(); -        } - -        @Override -        public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) { +        public void onRoutesUpdated() {              updateAllRoutesFromManager();          } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 44c0b54546be..8afc7d999d2e 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -546,37 +546,15 @@ public final class MediaRouter2Manager {          }      } -    void addRoutesOnHandler(List<MediaRoute2Info> routes) { +    void updateRoutesOnHandler(@NonNull List<MediaRoute2Info> routes) {          synchronized (mRoutesLock) { +            mRoutes.clear();              for (MediaRoute2Info route : routes) {                  mRoutes.put(route.getId(), route);              }          } -        if (routes.size() > 0) { -            notifyRoutesAdded(routes); -        } -    } -    void removeRoutesOnHandler(List<MediaRoute2Info> routes) { -        synchronized (mRoutesLock) { -            for (MediaRoute2Info route : routes) { -                mRoutes.remove(route.getId()); -            } -        } -        if (routes.size() > 0) { -            notifyRoutesRemoved(routes); -        } -    } - -    void changeRoutesOnHandler(List<MediaRoute2Info> routes) { -        synchronized (mRoutesLock) { -            for (MediaRoute2Info route : routes) { -                mRoutes.put(route.getId(), route); -            } -        } -        if (routes.size() > 0) { -            notifyRoutesChanged(routes); -        } +        notifyRoutesUpdated();      }      void createSessionOnHandler(int requestId, RoutingSessionInfo sessionInfo) { @@ -650,24 +628,9 @@ public final class MediaRouter2Manager {          notifySessionUpdated(sessionInfo);      } -    private void notifyRoutesAdded(List<MediaRoute2Info> routes) { -        for (CallbackRecord record: mCallbackRecords) { -            record.mExecutor.execute( -                    () -> record.mCallback.onRoutesAdded(routes)); -        } -    } - -    private void notifyRoutesRemoved(List<MediaRoute2Info> routes) { +    private void notifyRoutesUpdated() {          for (CallbackRecord record: mCallbackRecords) { -            record.mExecutor.execute( -                    () -> record.mCallback.onRoutesRemoved(routes)); -        } -    } - -    private void notifyRoutesChanged(List<MediaRoute2Info> routes) { -        for (CallbackRecord record: mCallbackRecords) { -            record.mExecutor.execute( -                    () -> record.mCallback.onRoutesChanged(routes)); +            record.mExecutor.execute(() -> record.mCallback.onRoutesUpdated());          }      } @@ -963,23 +926,12 @@ public final class MediaRouter2Manager {       * Interface for receiving events about media routing changes.       */      public interface Callback { -        /** -         * Called when routes are added. -         * @param routes the list of routes that have been added. It's never empty. -         */ -        default void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) {}          /** -         * Called when routes are removed. -         * @param routes the list of routes that have been removed. It's never empty. +         * Called when the routes list changes. This includes adding, modifying, or removing +         * individual routes.           */ -        default void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {} - -        /** -         * Called when routes are changed. -         * @param routes the list of routes that have been changed. It's never empty. -         */ -        default void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {} +        default void onRoutesUpdated() {}          /**           * Called when a session is changed. @@ -1115,21 +1067,12 @@ public final class MediaRouter2Manager {          }          @Override -        public void notifyRoutesAdded(List<MediaRoute2Info> routes) { -            mHandler.sendMessage(obtainMessage(MediaRouter2Manager::addRoutesOnHandler, -                    MediaRouter2Manager.this, routes)); -        } - -        @Override -        public void notifyRoutesRemoved(List<MediaRoute2Info> routes) { -            mHandler.sendMessage(obtainMessage(MediaRouter2Manager::removeRoutesOnHandler, -                    MediaRouter2Manager.this, routes)); -        } - -        @Override -        public void notifyRoutesChanged(List<MediaRoute2Info> routes) { -            mHandler.sendMessage(obtainMessage(MediaRouter2Manager::changeRoutesOnHandler, -                    MediaRouter2Manager.this, routes)); +        public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { +            mHandler.sendMessage( +                    obtainMessage( +                            MediaRouter2Manager::updateRoutesOnHandler, +                            MediaRouter2Manager.this, +                            routes));          }      }  } diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java index 4086dec99218..37c836762da0 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java @@ -32,7 +32,6 @@ import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_I  import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID_FIXED_VOLUME;  import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID_SPECIAL_FEATURE;  import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID_VARIABLE_VOLUME; -import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_NAME2;  import static com.android.mediaroutertest.StubMediaRoute2ProviderService.VOLUME_MAX;  import static org.junit.Assert.assertEquals; @@ -56,10 +55,10 @@ import android.media.RoutingSessionInfo;  import android.os.Bundle;  import android.text.TextUtils; -import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4;  import androidx.test.filters.LargeTest;  import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry;  import com.android.compatibility.common.util.PollingCheck; @@ -69,6 +68,7 @@ import org.junit.Test;  import org.junit.runner.RunWith;  import java.util.ArrayList; +import java.util.Collections;  import java.util.HashMap;  import java.util.List;  import java.util.Map; @@ -115,7 +115,7 @@ public class MediaRouter2ManagerTest {      @Before      public void setUp() throws Exception { -        mContext = InstrumentationRegistry.getTargetContext(); +        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();          mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();          mUiAutomation.adoptShellPermissionIdentity(Manifest.permission.MEDIA_CONTENT_CONTROL,                  Manifest.permission.MODIFY_AUDIO_ROUTING); @@ -170,51 +170,95 @@ public class MediaRouter2ManagerTest {      }      @Test -    public void testOnRoutesRemovedAndAdded() throws Exception { -        RouteCallback routeCallback = new RouteCallback() {}; -        mRouteCallbacks.add(routeCallback); -        mRouter2.registerRouteCallback(mExecutor, routeCallback, -                new RouteDiscoveryPreference.Builder(FEATURES_ALL, true).build()); +    public void testOnRoutesUpdated() throws Exception { +        final String routeId0 = "routeId0"; +        final String routeName0 = "routeName0"; +        final String routeId1 = "routeId1"; +        final String routeName1 = "routeName1"; +        final List<String> features = Collections.singletonList("customFeature"); -        Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL); +        final int newConnectionState = MediaRoute2Info.CONNECTION_STATE_CONNECTED; + +        final List<MediaRoute2Info> routes = new ArrayList<>(); +        routes.add(new MediaRoute2Info.Builder(routeId0, routeName0).addFeatures(features).build()); +        routes.add(new MediaRoute2Info.Builder(routeId1, routeName1).addFeatures(features).build()); -        CountDownLatch removedLatch = new CountDownLatch(1);          CountDownLatch addedLatch = new CountDownLatch(1); +        CountDownLatch changedLatch = new CountDownLatch(1); +        CountDownLatch removedLatch = new CountDownLatch(1); -        addManagerCallback(new MediaRouter2Manager.Callback() { -            @Override -            public void onRoutesRemoved(List<MediaRoute2Info> routes) { -                assertTrue(routes.size() > 0); -                for (MediaRoute2Info route : routes) { -                    if (route.getOriginalId().equals(ROUTE_ID2) -                            && route.getName().equals(ROUTE_NAME2)) { -                        removedLatch.countDown(); +        addManagerCallback( +                new MediaRouter2Manager.Callback() { +                    @Override +                    public void onRoutesUpdated() { +                        if (addedLatch.getCount() == 1 +                                && checkRoutesMatch(mManager.getAllRoutes(), routes)) { +                            addedLatch.countDown(); +                        } else if (changedLatch.getCount() == 1 +                                && checkRoutesMatch( +                                        mManager.getAllRoutes(), routes.subList(1, 2))) { +                            changedLatch.countDown(); +                        } else if (removedLatch.getCount() == 1 +                                && checkRoutesRemoved(mManager.getAllRoutes(), routes)) { +                            removedLatch.countDown(); +                        }                      } -                } -            } -            @Override -            public void onRoutesAdded(List<MediaRoute2Info> routes) { -                assertTrue(routes.size() > 0); -                if (removedLatch.getCount() > 0) { -                    return; -                } -                for (MediaRoute2Info route : routes) { -                    if (route.getOriginalId().equals(ROUTE_ID2) -                            && route.getName().equals(ROUTE_NAME2)) { -                        addedLatch.countDown(); -                    } -                } -            } -        }); +                }); -        MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2); -        assertNotNull(routeToRemove); +        mService.addRoutes(routes); +        assertTrue( +                "Added routes not found or onRoutesUpdated() never called.", +                addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); -        mService.removeRoute(ROUTE_ID2); -        assertTrue(removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); +        MediaRoute2Info newRoute2 = +                new MediaRoute2Info.Builder(routes.get(1)) +                        .setConnectionState(newConnectionState) +                        .build(); +        routes.set(1, newRoute2); +        mService.addRoute(routes.get(1)); +        assertTrue( +                "Modified route not found or onRoutesUpdated() never called.", +                changedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + +        List<String> routeIds = new ArrayList<>(); +        routeIds.add(routeId0); +        routeIds.add(routeId1); + +        mService.removeRoutes(routeIds); +        assertTrue( +                "Removed routes not found or onRoutesUpdated() never called.", +                removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); +    } -        mService.addRoute(routeToRemove); -        assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); +    private static boolean checkRoutesMatch( +            List<MediaRoute2Info> routesReceived, List<MediaRoute2Info> expectedRoutes) { +        for (MediaRoute2Info expectedRoute : expectedRoutes) { +            MediaRoute2Info matchingRoute = +                    routesReceived.stream() +                            .filter(r -> r.getOriginalId().equals(expectedRoute.getOriginalId())) +                            .findFirst() +                            .orElse(null); + +            if (matchingRoute == null) { +                return false; +            } +            assertTrue(TextUtils.equals(expectedRoute.getName(), matchingRoute.getName())); +            assertEquals(expectedRoute.getFeatures(), matchingRoute.getFeatures()); +            assertEquals(expectedRoute.getConnectionState(), matchingRoute.getConnectionState()); +        } + +        return true; +    } + +    private static boolean checkRoutesRemoved( +            List<MediaRoute2Info> routesReceived, List<MediaRoute2Info> routesRemoved) { +        for (MediaRoute2Info removedRoute : routesRemoved) { +            if (routesReceived.stream() +                    .anyMatch(r -> r.getOriginalId().equals(removedRoute.getOriginalId()))) { +                return false; +            } +        } +        return true;      }      @Test @@ -874,28 +918,31 @@ public class MediaRouter2ManagerTest {          // A dummy callback is required to send route feature info.          RouteCallback routeCallback = new RouteCallback() {}; -        MediaRouter2Manager.Callback managerCallback = new MediaRouter2Manager.Callback() { -            @Override -            public void onRoutesAdded(List<MediaRoute2Info> routes) { -                for (MediaRoute2Info route : routes) { -                    if (!route.isSystemRoute() -                            && hasMatchingFeature(route.getFeatures(), preference -                            .getPreferredFeatures())) { -                        addedLatch.countDown(); -                        break; +        MediaRouter2Manager.Callback managerCallback = +                new MediaRouter2Manager.Callback() { +                    @Override +                    public void onRoutesUpdated() { +                        List<MediaRoute2Info> routes = mManager.getAllRoutes(); +                        for (MediaRoute2Info route : routes) { +                            if (!route.isSystemRoute() +                                    && hasMatchingFeature( +                                            route.getFeatures(), +                                            preference.getPreferredFeatures())) { +                                addedLatch.countDown(); +                                break; +                            } +                        }                      } -                } -            } -            @Override -            public void onDiscoveryPreferenceChanged(String packageName, -                    RouteDiscoveryPreference discoveryPreference) { -                if (TextUtils.equals(mPackageName, packageName) -                        && Objects.equals(preference, discoveryPreference)) { -                    preferenceLatch.countDown(); -                } -            } -        }; +                    @Override +                    public void onDiscoveryPreferenceChanged( +                            String packageName, RouteDiscoveryPreference discoveryPreference) { +                        if (TextUtils.equals(mPackageName, packageName) +                                && Objects.equals(preference, discoveryPreference)) { +                            preferenceLatch.countDown(); +                        } +                    } +                };          mManager.registerCallback(mExecutor, managerCallback);          mRouter2.registerRouteCallback(mExecutor, routeCallback, preference); @@ -923,15 +970,17 @@ public class MediaRouter2ManagerTest {      void awaitOnRouteChangedManager(Runnable task, String routeId,              Predicate<MediaRoute2Info> predicate) throws Exception {          CountDownLatch latch = new CountDownLatch(1); -        MediaRouter2Manager.Callback callback = new MediaRouter2Manager.Callback() { -            @Override -            public void onRoutesChanged(List<MediaRoute2Info> changed) { -                MediaRoute2Info route = createRouteMap(changed).get(routeId); -                if (route != null && predicate.test(route)) { -                    latch.countDown(); -                } -            } -        }; +        MediaRouter2Manager.Callback callback = +                new MediaRouter2Manager.Callback() { +                    @Override +                    public void onRoutesUpdated() { +                        MediaRoute2Info route = +                                createRouteMap(mManager.getAllRoutes()).get(routeId); +                        if (route != null && predicate.test(route)) { +                            latch.countDown(); +                        } +                    } +                };          mManager.registerCallback(mExecutor, callback);          try {              task.run(); diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java index a51e3714b6f7..a7ae5f45b795 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java @@ -30,7 +30,9 @@ import android.os.Bundle;  import android.os.IBinder;  import android.text.TextUtils; +import java.util.Collections;  import java.util.HashMap; +import java.util.List;  import java.util.Map;  import java.util.Objects; @@ -146,19 +148,44 @@ public class StubMediaRoute2ProviderService extends MediaRoute2ProviderService {       * they have the same route id.       */      public void addRoute(@NonNull MediaRoute2Info route) { -        Objects.requireNonNull(route, "route must not be null"); -        mRoutes.put(route.getOriginalId(), route); -        publishRoutes(); +        addRoutes(Collections.singletonList(route));      }      /** -     * Removes a route and publishes it. +     * Adds a list of routes and publishes it. It will replace existing routes with matching ids. +     * +     * @param routes list of routes to be added.       */ +    public void addRoutes(@NonNull List<MediaRoute2Info> routes) { +        Objects.requireNonNull(routes, "Routes must not be null."); +        for (MediaRoute2Info route : routes) { +            Objects.requireNonNull(route, "Route must not be null"); +            mRoutes.put(route.getOriginalId(), route); +        } +        publishRoutes(); +    } + +    /** Removes a route and publishes it. */      public void removeRoute(@NonNull String routeId) { -        Objects.requireNonNull(routeId, "routeId must not be null"); -        MediaRoute2Info route = mRoutes.get(routeId); -        if (route != null) { -            mRoutes.remove(routeId); +        removeRoutes(Collections.singletonList(routeId)); +    } + +    /** +     * Removes a list of routes and publishes the changes. +     * +     * @param routes list of route ids to be removed. +     */ +    public void removeRoutes(@NonNull List<String> routes) { +        Objects.requireNonNull(routes, "Routes must not be null"); +        boolean hasRemovedRoutes = false; +        for (String routeId : routes) { +            MediaRoute2Info route = mRoutes.get(routeId); +            if (route != null) { +                mRoutes.remove(routeId); +                hasRemovedRoutes = true; +            } +        } +        if (hasRemovedRoutes) {              publishRoutes();          }      } diff --git a/packages/SettingsLib/Spa/OWNERS b/packages/SettingsLib/Spa/OWNERS index b352b045c352..288787241caa 100644 --- a/packages/SettingsLib/Spa/OWNERS +++ b/packages/SettingsLib/Spa/OWNERS @@ -1,6 +1,6 @@ +set noparent +  chaohuiw@google.com  hanxu@google.com  kellyz@google.com  pierreqian@google.com - -per-file *.xml = set noparent diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/HomePage.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/HomePage.kt index 5adbc32c9ece..171a16118052 100644 --- a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/HomePage.kt +++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/HomePage.kt @@ -52,6 +52,8 @@ private fun HomePage() {          PreferencePageProvider.EntryItem()          ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = 0) + +        SliderPageProvider.EntryItem()      }  } diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/PageRepository.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/PageRepository.kt index da278d1f1e7a..c24541a903da 100644 --- a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/PageRepository.kt +++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/PageRepository.kt @@ -22,9 +22,15 @@ object Destinations {      const val Home = "Home"      const val Preference = "Preference"      const val Argument = "Argument" +    const val Slider = "Slider"  }  val codelabPageRepository = SettingsPageRepository( -    allPages = listOf(HomePageProvider, PreferencePageProvider, ArgumentPageProvider), +    allPages = listOf( +        HomePageProvider, +        PreferencePageProvider, +        ArgumentPageProvider, +        SliderPageProvider, +    ),      startDestination = Destinations.Home,  ) diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SliderPage.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SliderPage.kt new file mode 100644 index 000000000000..6e965813afc4 --- /dev/null +++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SliderPage.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 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.settingslib.spa.codelab.page + +import android.os.Bundle +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AccessAlarm +import androidx.compose.material.icons.outlined.MusicNote +import androidx.compose.material.icons.outlined.MusicOff +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.api.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.ui.SettingsSlider +import com.android.settingslib.spa.widget.ui.SettingsSliderModel + +object SliderPageProvider : SettingsPageProvider { +    override val name = Destinations.Slider + +    @Composable +    override fun Page(arguments: Bundle?) { +        SliderPage() +    } + +    @Composable +    fun EntryItem() { +        Preference(object : PreferenceModel { +            override val title = "Sample Slider" +            override val onClick = navigator(Destinations.Slider) +        }) +    } +} + +@Composable +private fun SliderPage() { +    Column(Modifier.verticalScroll(rememberScrollState())) { +        SettingsSlider(object : SettingsSliderModel { +            override val title = "Slider" +            override val initValue = 40 +        }) + +        SettingsSlider(object : SettingsSliderModel { +            override val title = "Slider with icon" +            override val initValue = 30 +            override val onValueChangeFinished = { +                println("onValueChangeFinished") +            } +            override val icon = Icons.Outlined.AccessAlarm +        }) + +        val initValue = 0 +        var icon by remember { mutableStateOf(Icons.Outlined.MusicOff) } +        var sliderPosition by remember { mutableStateOf(initValue) } +        SettingsSlider(object : SettingsSliderModel { +            override val title = "Slider with changeable icon" +            override val initValue = initValue +            override val onValueChange = { it: Int -> +                sliderPosition = it +                icon = if (it > 0) Icons.Outlined.MusicNote else Icons.Outlined.MusicOff +            } +            override val onValueChangeFinished = { +                println("onValueChangeFinished: the value is $sliderPosition") +            } +            override val icon = icon +        }) + +        SettingsSlider(object : SettingsSliderModel { +            override val title = "Slider with steps" +            override val initValue = 2 +            override val valueRange = 1..5 +            override val showSteps = true +        }) +    } +} + +@Preview(showBackground = true) +@Composable +private fun SliderPagePreview() { +    SettingsTheme { +        SliderPage() +    } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt new file mode 100644 index 000000000000..27fdc916a434 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2022 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.settingslib.spa.framework.theme + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext + +data class SettingsColorScheme( +    val background: Color = Color.Unspecified, +    val categoryTitle: Color = Color.Unspecified, +    val surface: Color = Color.Unspecified, +    val surfaceHeader: Color = Color.Unspecified, +    val secondaryText: Color = Color.Unspecified, +    val primaryContainer: Color = Color.Unspecified, +    val onPrimaryContainer: Color = Color.Unspecified, +) + +internal val LocalColorScheme = staticCompositionLocalOf { SettingsColorScheme() } + +@Composable +internal fun settingsColorScheme(isDarkTheme: Boolean): SettingsColorScheme { +    val context = LocalContext.current +    return remember(isDarkTheme) { +        when { +            isDarkTheme -> dynamicDarkColorScheme(context) +            else -> dynamicLightColorScheme(context) +        } +    } +} + +/** + * Creates a light dynamic color scheme. + * + * Use this function to create a color scheme based off the system wallpaper. If the developer + * changes the wallpaper this color scheme will change accordingly. This dynamic scheme is a + * light theme variant. + * + * @param context The context required to get system resource data. + */ +private fun dynamicLightColorScheme(context: Context): SettingsColorScheme { +    val tonalPalette = dynamicTonalPalette(context) +    return SettingsColorScheme( +        background = tonalPalette.neutral95, +        categoryTitle = tonalPalette.primary40, +        surface = tonalPalette.neutral99, +        surfaceHeader = tonalPalette.neutral90, +        secondaryText = tonalPalette.neutralVariant30, +        primaryContainer = tonalPalette.primary90, +        onPrimaryContainer = tonalPalette.neutral10, +    ) +} + +/** + * Creates a dark dynamic color scheme. + * + * Use this function to create a color scheme based off the system wallpaper. If the developer + * changes the wallpaper this color scheme will change accordingly. This dynamic scheme is a dark + * theme variant. + * + * @param context The context required to get system resource data. + */ +private fun dynamicDarkColorScheme(context: Context): SettingsColorScheme { +    val tonalPalette = dynamicTonalPalette(context) +    return SettingsColorScheme( +        background = tonalPalette.neutral10, +        categoryTitle = tonalPalette.primary90, +        surface = tonalPalette.neutral20, +        surfaceHeader = tonalPalette.neutral30, +        secondaryText = tonalPalette.neutralVariant80, +        primaryContainer = tonalPalette.secondary90, +        onPrimaryContainer = tonalPalette.neutral10, +    ) +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt index 29998258ed37..e6fa74e34cc8 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt @@ -19,6 +19,8 @@ package com.android.settingslib.spa.framework.theme  import androidx.compose.foundation.isSystemInDarkTheme  import androidx.compose.material3.MaterialTheme  import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable  /**   * The Material 3 Theme for Settings. @@ -26,9 +28,21 @@ import androidx.compose.runtime.Composable  @Composable  fun SettingsTheme(content: @Composable () -> Unit) {      val isDarkTheme = isSystemInDarkTheme() -    val colorScheme = materialColorScheme(isDarkTheme) +    val settingsColorScheme = settingsColorScheme(isDarkTheme) +    val colorScheme = materialColorScheme(isDarkTheme).copy( +        background = settingsColorScheme.background, +    ) -    MaterialTheme(colorScheme = colorScheme) { -        content() +    CompositionLocalProvider(LocalColorScheme provides settingsColorScheme(isDarkTheme)) { +        MaterialTheme(colorScheme = colorScheme, typography = rememberSettingsTypography()) { +            content() +        }      }  } + +object SettingsTheme { +    val colorScheme: SettingsColorScheme +        @Composable +        @ReadOnlyComposable +        get() = LocalColorScheme.current +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTonalPalette.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTonalPalette.kt new file mode 100644 index 000000000000..f81f5e734fb4 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTonalPalette.kt @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2022 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.settingslib.spa.framework.theme + +import android.R +import android.content.Context +import androidx.annotation.ColorRes +import androidx.annotation.DoNotInline +import androidx.compose.ui.graphics.Color + +/** + * Tonal Palette structure in Material. + * + * A tonal palette is comprised of 5 tonal ranges. Each tonal range includes the 13 stops, or + * tonal swatches. + * + * Tonal range names are: + * - Neutral (N) + * - Neutral variant (NV) + * - Primary (P) + * - Secondary (S) + * - Tertiary (T) + */ +internal class SettingsTonalPalette( +    // The neutral tonal range from the generated dynamic color palette. +    // Ordered from the lightest shade [neutral100] to the darkest shade [neutral0]. +    val neutral100: Color, +    val neutral99: Color, +    val neutral95: Color, +    val neutral90: Color, +    val neutral80: Color, +    val neutral70: Color, +    val neutral60: Color, +    val neutral50: Color, +    val neutral40: Color, +    val neutral30: Color, +    val neutral20: Color, +    val neutral10: Color, +    val neutral0: Color, + +    // The neutral variant tonal range, sometimes called "neutral 2",  from the +    // generated dynamic color palette. +    // Ordered from the lightest shade [neutralVariant100] to the darkest shade [neutralVariant0]. +    val neutralVariant100: Color, +    val neutralVariant99: Color, +    val neutralVariant95: Color, +    val neutralVariant90: Color, +    val neutralVariant80: Color, +    val neutralVariant70: Color, +    val neutralVariant60: Color, +    val neutralVariant50: Color, +    val neutralVariant40: Color, +    val neutralVariant30: Color, +    val neutralVariant20: Color, +    val neutralVariant10: Color, +    val neutralVariant0: Color, + +    // The primary tonal range from the generated dynamic color palette. +    // Ordered from the lightest shade [primary100] to the darkest shade [primary0]. +    val primary100: Color, +    val primary99: Color, +    val primary95: Color, +    val primary90: Color, +    val primary80: Color, +    val primary70: Color, +    val primary60: Color, +    val primary50: Color, +    val primary40: Color, +    val primary30: Color, +    val primary20: Color, +    val primary10: Color, +    val primary0: Color, + +    // The secondary tonal range from the generated dynamic color palette. +    // Ordered from the lightest shade [secondary100] to the darkest shade [secondary0]. +    val secondary100: Color, +    val secondary99: Color, +    val secondary95: Color, +    val secondary90: Color, +    val secondary80: Color, +    val secondary70: Color, +    val secondary60: Color, +    val secondary50: Color, +    val secondary40: Color, +    val secondary30: Color, +    val secondary20: Color, +    val secondary10: Color, +    val secondary0: Color, + +    // The tertiary tonal range from the generated dynamic color palette. +    // Ordered from the lightest shade [tertiary100] to the darkest shade [tertiary0]. +    val tertiary100: Color, +    val tertiary99: Color, +    val tertiary95: Color, +    val tertiary90: Color, +    val tertiary80: Color, +    val tertiary70: Color, +    val tertiary60: Color, +    val tertiary50: Color, +    val tertiary40: Color, +    val tertiary30: Color, +    val tertiary20: Color, +    val tertiary10: Color, +    val tertiary0: Color, +) + +/** Dynamic colors in Material. */ +internal fun dynamicTonalPalette(context: Context) = SettingsTonalPalette( +    // The neutral tonal range from the generated dynamic color palette. +    neutral100 = ColorResourceHelper.getColor(context, R.color.system_neutral1_0), +    neutral99 = ColorResourceHelper.getColor(context, R.color.system_neutral1_10), +    neutral95 = ColorResourceHelper.getColor(context, R.color.system_neutral1_50), +    neutral90 = ColorResourceHelper.getColor(context, R.color.system_neutral1_100), +    neutral80 = ColorResourceHelper.getColor(context, R.color.system_neutral1_200), +    neutral70 = ColorResourceHelper.getColor(context, R.color.system_neutral1_300), +    neutral60 = ColorResourceHelper.getColor(context, R.color.system_neutral1_400), +    neutral50 = ColorResourceHelper.getColor(context, R.color.system_neutral1_500), +    neutral40 = ColorResourceHelper.getColor(context, R.color.system_neutral1_600), +    neutral30 = ColorResourceHelper.getColor(context, R.color.system_neutral1_700), +    neutral20 = ColorResourceHelper.getColor(context, R.color.system_neutral1_800), +    neutral10 = ColorResourceHelper.getColor(context, R.color.system_neutral1_900), +    neutral0 = ColorResourceHelper.getColor(context, R.color.system_neutral1_1000), + +    // The neutral variant tonal range, sometimes called "neutral 2",  from the +    // generated dynamic color palette. +    neutralVariant100 = ColorResourceHelper.getColor(context, R.color.system_neutral2_0), +    neutralVariant99 = ColorResourceHelper.getColor(context, R.color.system_neutral2_10), +    neutralVariant95 = ColorResourceHelper.getColor(context, R.color.system_neutral2_50), +    neutralVariant90 = ColorResourceHelper.getColor(context, R.color.system_neutral2_100), +    neutralVariant80 = ColorResourceHelper.getColor(context, R.color.system_neutral2_200), +    neutralVariant70 = ColorResourceHelper.getColor(context, R.color.system_neutral2_300), +    neutralVariant60 = ColorResourceHelper.getColor(context, R.color.system_neutral2_400), +    neutralVariant50 = ColorResourceHelper.getColor(context, R.color.system_neutral2_500), +    neutralVariant40 = ColorResourceHelper.getColor(context, R.color.system_neutral2_600), +    neutralVariant30 = ColorResourceHelper.getColor(context, R.color.system_neutral2_700), +    neutralVariant20 = ColorResourceHelper.getColor(context, R.color.system_neutral2_800), +    neutralVariant10 = ColorResourceHelper.getColor(context, R.color.system_neutral2_900), +    neutralVariant0 = ColorResourceHelper.getColor(context, R.color.system_neutral2_1000), + +    // The primary tonal range from the generated dynamic color palette. +    primary100 = ColorResourceHelper.getColor(context, R.color.system_accent1_0), +    primary99 = ColorResourceHelper.getColor(context, R.color.system_accent1_10), +    primary95 = ColorResourceHelper.getColor(context, R.color.system_accent1_50), +    primary90 = ColorResourceHelper.getColor(context, R.color.system_accent1_100), +    primary80 = ColorResourceHelper.getColor(context, R.color.system_accent1_200), +    primary70 = ColorResourceHelper.getColor(context, R.color.system_accent1_300), +    primary60 = ColorResourceHelper.getColor(context, R.color.system_accent1_400), +    primary50 = ColorResourceHelper.getColor(context, R.color.system_accent1_500), +    primary40 = ColorResourceHelper.getColor(context, R.color.system_accent1_600), +    primary30 = ColorResourceHelper.getColor(context, R.color.system_accent1_700), +    primary20 = ColorResourceHelper.getColor(context, R.color.system_accent1_800), +    primary10 = ColorResourceHelper.getColor(context, R.color.system_accent1_900), +    primary0 = ColorResourceHelper.getColor(context, R.color.system_accent1_1000), + +    // The secondary tonal range from the generated dynamic color palette. +    secondary100 = ColorResourceHelper.getColor(context, R.color.system_accent2_0), +    secondary99 = ColorResourceHelper.getColor(context, R.color.system_accent2_10), +    secondary95 = ColorResourceHelper.getColor(context, R.color.system_accent2_50), +    secondary90 = ColorResourceHelper.getColor(context, R.color.system_accent2_100), +    secondary80 = ColorResourceHelper.getColor(context, R.color.system_accent2_200), +    secondary70 = ColorResourceHelper.getColor(context, R.color.system_accent2_300), +    secondary60 = ColorResourceHelper.getColor(context, R.color.system_accent2_400), +    secondary50 = ColorResourceHelper.getColor(context, R.color.system_accent2_500), +    secondary40 = ColorResourceHelper.getColor(context, R.color.system_accent2_600), +    secondary30 = ColorResourceHelper.getColor(context, R.color.system_accent2_700), +    secondary20 = ColorResourceHelper.getColor(context, R.color.system_accent2_800), +    secondary10 = ColorResourceHelper.getColor(context, R.color.system_accent2_900), +    secondary0 = ColorResourceHelper.getColor(context, R.color.system_accent2_1000), + +    // The tertiary tonal range from the generated dynamic color palette. +    tertiary100 = ColorResourceHelper.getColor(context, R.color.system_accent3_0), +    tertiary99 = ColorResourceHelper.getColor(context, R.color.system_accent3_10), +    tertiary95 = ColorResourceHelper.getColor(context, R.color.system_accent3_50), +    tertiary90 = ColorResourceHelper.getColor(context, R.color.system_accent3_100), +    tertiary80 = ColorResourceHelper.getColor(context, R.color.system_accent3_200), +    tertiary70 = ColorResourceHelper.getColor(context, R.color.system_accent3_300), +    tertiary60 = ColorResourceHelper.getColor(context, R.color.system_accent3_400), +    tertiary50 = ColorResourceHelper.getColor(context, R.color.system_accent3_500), +    tertiary40 = ColorResourceHelper.getColor(context, R.color.system_accent3_600), +    tertiary30 = ColorResourceHelper.getColor(context, R.color.system_accent3_700), +    tertiary20 = ColorResourceHelper.getColor(context, R.color.system_accent3_800), +    tertiary10 = ColorResourceHelper.getColor(context, R.color.system_accent3_900), +    tertiary0 = ColorResourceHelper.getColor(context, R.color.system_accent3_1000), +) + +private object ColorResourceHelper { +    @DoNotInline +    fun getColor(context: Context, @ColorRes id: Int): Color { +        return Color(context.resources.getColor(id, context.theme)) +    } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt new file mode 100644 index 000000000000..07f09ba95ca3 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 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.settingslib.spa.framework.theme + +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp + +private class SettingsTypography { +    private val brand = FontFamily.Default +    private val plain = FontFamily.Default + +    val typography = Typography( +        displayLarge = TextStyle( +            fontFamily = brand, +            fontWeight = FontWeight.Normal, +            fontSize = 57.sp, +            lineHeight = 64.sp, +            letterSpacing = (-0.2).sp +        ), +        displayMedium = TextStyle( +            fontFamily = brand, +            fontWeight = FontWeight.Normal, +            fontSize = 45.sp, +            lineHeight = 52.sp, +            letterSpacing = 0.0.sp +        ), +        displaySmall = TextStyle( +            fontFamily = brand, +            fontWeight = FontWeight.Normal, +            fontSize = 36.sp, +            lineHeight = 44.sp, +            letterSpacing = 0.0.sp +        ), +        headlineLarge = TextStyle( +            fontFamily = brand, +            fontWeight = FontWeight.Normal, +            fontSize = 32.sp, +            lineHeight = 40.sp, +            letterSpacing = 0.0.sp +        ), +        headlineMedium = TextStyle( +            fontFamily = brand, +            fontWeight = FontWeight.Normal, +            fontSize = 28.sp, +            lineHeight = 36.sp, +            letterSpacing = 0.0.sp +        ), +        headlineSmall = TextStyle( +            fontFamily = brand, +            fontWeight = FontWeight.Normal, +            fontSize = 24.sp, +            lineHeight = 32.sp, +            letterSpacing = 0.0.sp +        ), +        titleLarge = TextStyle( +            fontFamily = brand, +            fontWeight = FontWeight.Normal, +            fontSize = 22.sp, +            lineHeight = 28.sp, +            letterSpacing = 0.02.em, +        ), +        titleMedium = TextStyle( +            fontFamily = brand, +            fontWeight = FontWeight.Normal, +            fontSize = 20.sp, +            lineHeight = 24.sp, +            letterSpacing = 0.02.em, +        ), +        titleSmall = TextStyle( +            fontFamily = brand, +            fontWeight = FontWeight.Normal, +            fontSize = 18.sp, +            lineHeight = 20.sp, +            letterSpacing = 0.02.em, +        ), +        bodyLarge = TextStyle( +            fontFamily = plain, +            fontWeight = FontWeight.Normal, +            fontSize = 16.sp, +            lineHeight = 24.sp, +            letterSpacing = 0.01.em, +        ), +        bodyMedium = TextStyle( +            fontFamily = plain, +            fontWeight = FontWeight.Normal, +            fontSize = 14.sp, +            lineHeight = 20.sp, +            letterSpacing = 0.01.em, +        ), +        bodySmall = TextStyle( +            fontFamily = plain, +            fontWeight = FontWeight.Normal, +            fontSize = 12.sp, +            lineHeight = 16.sp, +            letterSpacing = 0.01.em, +        ), +        labelLarge = TextStyle( +            fontFamily = plain, +            fontWeight = FontWeight.Medium, +            fontSize = 16.sp, +            lineHeight = 24.sp, +            letterSpacing = 0.01.em, +        ), +        labelMedium = TextStyle( +            fontFamily = plain, +            fontWeight = FontWeight.Medium, +            fontSize = 14.sp, +            lineHeight = 20.sp, +            letterSpacing = 0.01.em, +        ), +        labelSmall = TextStyle( +            fontFamily = plain, +            fontWeight = FontWeight.Medium, +            fontSize = 12.sp, +            lineHeight = 16.sp, +            letterSpacing = 0.01.em, +        ), +    ) +} + +@Composable +internal fun rememberSettingsTypography(): Typography { +    return remember { SettingsTypography().typography } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt new file mode 100644 index 000000000000..0454ac37cfb7 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2022 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.settingslib.spa.widget.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AccessAlarm +import androidx.compose.material.icons.outlined.MusicNote +import androidx.compose.material.icons.outlined.MusicOff +import androidx.compose.material3.Icon +import androidx.compose.material3.Slider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.BaseLayout +import kotlin.math.roundToInt + +/** + * The widget model for [SettingsSlider] widget. + */ +interface SettingsSliderModel { +    /** +     * The title of this [SettingsSlider]. +     */ +    val title: String + +    /** +     * The initial position of the [SettingsSlider]. +     */ +    val initValue: Int + +    /** +     * The value range for this [SettingsSlider]. +     */ +    val valueRange: IntRange +        get() = 0..100 + +    /** +     * The lambda to be invoked during the value change by dragging or a click. This callback is +     * used to get the real time value of the [SettingsSlider]. +     */ +    val onValueChange: ((value: Int) -> Unit)? +        get() = null + +    /** +     * The lambda to be invoked when value change has ended. This callback is used to get when the +     * user has completed selecting a new value by ending a drag or a click. +     */ +    val onValueChangeFinished: (() -> Unit)? +        get() = null + +    /** +     * The icon image for [SettingsSlider]. If not specified, the slider hides the icon by default. +     */ +    val icon: ImageVector? +        get() = null + +    /** +     * Indicates whether to show step marks. If show step marks, when user finish sliding, +     * the slider will automatically jump to the nearest step mark. Otherwise, the slider hides +     * the step marks by default. +     * +     * The step is fixed to 1. +     */ +    val showSteps: Boolean +        get() = false +} + +/** + * Settings slider widget. + * + * Data is provided through [SettingsSliderModel]. + */ +@Composable +fun SettingsSlider(model: SettingsSliderModel) { +    SettingsSlider( +        title = model.title, +        initValue = model.initValue, +        valueRange = model.valueRange, +        onValueChange = model.onValueChange, +        onValueChangeFinished = model.onValueChangeFinished, +        icon = model.icon, +        showSteps = model.showSteps, +    ) +} + +@Composable +internal fun SettingsSlider( +    title: String, +    initValue: Int, +    valueRange: IntRange = 0..100, +    onValueChange: ((value: Int) -> Unit)? = null, +    onValueChangeFinished: (() -> Unit)? = null, +    icon: ImageVector? = null, +    showSteps: Boolean = false, +    modifier: Modifier = Modifier, +) { +    var sliderPosition by rememberSaveable { mutableStateOf(initValue.toFloat()) } +    BaseLayout( +        title = title, +        subTitle = { +            Slider( +                value = sliderPosition, +                onValueChange = { +                    sliderPosition = it +                    onValueChange?.invoke(sliderPosition.roundToInt()) +                }, +                modifier = modifier, +                valueRange = valueRange.first.toFloat()..valueRange.last.toFloat(), +                steps = if (showSteps) (valueRange.count() - 2) else 0, +                onValueChangeFinished = onValueChangeFinished, +            ) +        }, +        icon = if (icon != null) ({ +            Icon(imageVector = icon, contentDescription = null) +        }) else null, +    ) +} + +@Preview +@Composable +private fun SettingsSliderPreview() { +    SettingsTheme { +        val initValue = 30 +        var sliderPosition by rememberSaveable { mutableStateOf(initValue) } +        SettingsSlider( +            title = "Alarm Volume", +            initValue = 30, +            onValueChange = { sliderPosition = it }, +            onValueChangeFinished = { +                println("onValueChangeFinished: the value is $sliderPosition") +            }, +            icon = Icons.Outlined.AccessAlarm, +        ) +    } +} + +@Preview +@Composable +private fun SettingsSliderIconChangePreview() { +    SettingsTheme { +        var icon by remember { mutableStateOf(Icons.Outlined.MusicNote) } +        SettingsSlider( +            title = "Media Volume", +            initValue = 40, +            onValueChange = { it: Int -> +                icon = if (it > 0) Icons.Outlined.MusicNote else Icons.Outlined.MusicOff +            }, +            icon = icon, +        ) +    } +} + +@Preview +@Composable +private fun SettingsSliderStepsPreview() { +    SettingsTheme { +        SettingsSlider( +            title = "Display Text", +            initValue = 2, +            valueRange = 1..5, +            showSteps = true, +        ) +    } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index d9262cce3cb9..766c036d521c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -528,7 +528,7 @@ public class InfoMediaManager extends MediaManager {      class RouterManagerCallback implements MediaRouter2Manager.Callback {          @Override -        public void onRoutesAdded(List<MediaRoute2Info> routes) { +        public void onRoutesUpdated() {              refreshDevices();          } @@ -540,16 +540,6 @@ public class InfoMediaManager extends MediaManager {          }          @Override -        public void onRoutesChanged(List<MediaRoute2Info> routes) { -            refreshDevices(); -        } - -        @Override -        public void onRoutesRemoved(List<MediaRoute2Info> routes) { -            refreshDevices(); -        } - -        @Override          public void onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) {              if (DEBUG) {                  Log.d(TAG, "onTransferred() oldSession : " + oldSession.getName() diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java index ee7b7d6b180f..f4af6e852580 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java @@ -112,7 +112,7 @@ public class InfoMediaManagerTest {          final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID);          assertThat(mediaDevice).isNull(); -        mInfoMediaManager.mMediaRouterCallback.onRoutesAdded(routes); +        mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated();          final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0);          assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -135,7 +135,7 @@ public class InfoMediaManagerTest {          assertThat(mediaDevice).isNull();          mInfoMediaManager.mPackageName = ""; -        mInfoMediaManager.mMediaRouterCallback.onRoutesAdded(routes); +        mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated();          final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0);          assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -199,7 +199,7 @@ public class InfoMediaManagerTest {          final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID);          assertThat(mediaDevice).isNull(); -        mInfoMediaManager.mMediaRouterCallback.onRoutesChanged(routes); +        mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated();          final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0);          assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -222,7 +222,7 @@ public class InfoMediaManagerTest {          assertThat(mediaDevice).isNull();          mInfoMediaManager.mPackageName = ""; -        mInfoMediaManager.mMediaRouterCallback.onRoutesChanged(routes); +        mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated();          final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0);          assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -263,7 +263,7 @@ public class InfoMediaManagerTest {          final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID);          assertThat(mediaDevice).isNull(); -        mInfoMediaManager.mMediaRouterCallback.onRoutesRemoved(routes); +        mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated();          final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0);          assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -286,7 +286,7 @@ public class InfoMediaManagerTest {          assertThat(mediaDevice).isNull();          mInfoMediaManager.mPackageName = ""; -        mInfoMediaManager.mMediaRouterCallback.onRoutesRemoved(routes); +        mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated();          final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0);          assertThat(infoDevice.getId()).isEqualTo(TEST_ID); diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index e27cbeaab139..bfa8af957208 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -927,8 +927,9 @@ class MediaRouter2ServiceImpl {                          routerRecord.mUserRecord.mHandler, routerRecord, manager));          } -        userRecord.mHandler.sendMessage(obtainMessage(UserHandler::notifyRoutesToManager, -                userRecord.mHandler, manager)); +        userRecord.mHandler.sendMessage( +                obtainMessage( +                        UserHandler::notifyInitialRoutesToManager, userRecord.mHandler, manager));      }      private void unregisterManagerLocked(@NonNull IMediaRouter2Manager manager, boolean died) { @@ -1311,6 +1312,36 @@ class MediaRouter2ServiceImpl {                  new CopyOnWriteArrayList<>();          private final Map<String, RouterRecord> mSessionToRouterMap = new ArrayMap<>(); +        /** +         * Latest list of routes sent to privileged {@link android.media.MediaRouter2 routers} and +         * {@link android.media.MediaRouter2Manager managers}. +         * +         * <p>Privileged routers are instances of {@link android.media.MediaRouter2 MediaRouter2} +         * that have {@code MODIFY_AUDIO_ROUTING} permission. +         * +         * <p>This list contains all routes exposed by route providers. This includes routes from +         * both system route providers and user route providers. +         * +         * <p>See {@link #getRouters(boolean hasModifyAudioRoutingPermission)}. +         */ +        private final Map<String, MediaRoute2Info> mLastNotifiedRoutesToPrivilegedRouters = +                new ArrayMap<>(); + +        /** +         * Latest list of routes sent to non-privileged {@link android.media.MediaRouter2 routers}. +         * +         * <p>Non-privileged routers are instances of {@link android.media.MediaRouter2 +         * MediaRouter2} that do <i><b>not</b></i> have {@code MODIFY_AUDIO_ROUTING} permission. +         * +         * <p>This list contains all routes exposed by user route providers. It might also include +         * the current default route from {@link #mSystemProvider} to expose local route updates +         * (e.g. volume changes) to non-privileged routers. +         * +         * <p>See {@link SystemMediaRoute2Provider#mDefaultRoute}. +         */ +        private final Map<String, MediaRoute2Info> mLastNotifiedRoutesToNonPrivilegedRouters = +                new ArrayMap<>(); +          private boolean mRunning;          // TODO: (In Android S+) Pull out SystemMediaRoute2Provider out of UserHandler. @@ -1425,91 +1456,182 @@ class MediaRouter2ServiceImpl {          }          private void onProviderStateChangedOnHandler(@NonNull MediaRoute2Provider provider) { -            int providerInfoIndex = getLastProviderInfoIndex(provider.getUniqueId());              MediaRoute2ProviderInfo currentInfo = provider.getProviderInfo(); + +            int providerInfoIndex = +                    indexOfRouteProviderInfoByUniqueId(provider.getUniqueId(), mLastProviderInfos); +              MediaRoute2ProviderInfo prevInfo = -                    (providerInfoIndex < 0) ? null : mLastProviderInfos.get(providerInfoIndex); -            if (Objects.equals(prevInfo, currentInfo)) return; +                    providerInfoIndex == -1 ? null : mLastProviderInfos.get(providerInfoIndex); + +            // Ignore if no changes +            if (Objects.equals(prevInfo, currentInfo)) { +                return; +            } + +            boolean hasAddedOrModifiedRoutes = false; +            boolean hasRemovedRoutes = false; + +            boolean isSystemProvider = provider.mIsSystemRouteProvider; -            List<MediaRoute2Info> addedRoutes = new ArrayList<>(); -            List<MediaRoute2Info> removedRoutes = new ArrayList<>(); -            List<MediaRoute2Info> changedRoutes = new ArrayList<>();              if (prevInfo == null) { +                // Provider is being added.                  mLastProviderInfos.add(currentInfo); -                addedRoutes.addAll(currentInfo.getRoutes()); +                addToRoutesMap(currentInfo.getRoutes(), isSystemProvider); +                // Check if new provider exposes routes. +                hasAddedOrModifiedRoutes = !currentInfo.getRoutes().isEmpty();              } else if (currentInfo == null) { +                // Provider is being removed. +                hasRemovedRoutes = true;                  mLastProviderInfos.remove(prevInfo); -                removedRoutes.addAll(prevInfo.getRoutes()); +                removeFromRoutesMap(prevInfo.getRoutes(), isSystemProvider);              } else { +                // Provider is being updated.                  mLastProviderInfos.set(providerInfoIndex, currentInfo); -                final Collection<MediaRoute2Info> prevRoutes = prevInfo.getRoutes();                  final Collection<MediaRoute2Info> currentRoutes = currentInfo.getRoutes(); +                // Checking for individual routes.                  for (MediaRoute2Info route : currentRoutes) {                      if (!route.isValid()) { -                        Slog.w(TAG, "onProviderStateChangedOnHandler: Ignoring invalid route : " -                                + route); +                        Slog.w( +                                TAG, +                                "onProviderStateChangedOnHandler: Ignoring invalid route : " +                                        + route);                          continue;                      } +                      MediaRoute2Info prevRoute = prevInfo.getRoute(route.getOriginalId()); -                    if (prevRoute == null) { -                        addedRoutes.add(route); -                    } else if (!Objects.equals(prevRoute, route)) { -                        changedRoutes.add(route); +                    if (prevRoute == null || !Objects.equals(prevRoute, route)) { +                        hasAddedOrModifiedRoutes = true; +                        mLastNotifiedRoutesToPrivilegedRouters.put(route.getId(), route); +                        if (!isSystemProvider) { +                            mLastNotifiedRoutesToNonPrivilegedRouters.put(route.getId(), route); +                        }                      }                  } +                // Checking for individual removals                  for (MediaRoute2Info prevRoute : prevInfo.getRoutes()) {                      if (currentInfo.getRoute(prevRoute.getOriginalId()) == null) { -                        removedRoutes.add(prevRoute); +                        hasRemovedRoutes = true; +                        mLastNotifiedRoutesToPrivilegedRouters.remove(prevRoute.getId()); +                        if (!isSystemProvider) { +                            mLastNotifiedRoutesToNonPrivilegedRouters.remove(prevRoute.getId()); +                        }                      }                  }              } +            dispatchUpdates( +                    hasAddedOrModifiedRoutes, +                    hasRemovedRoutes, +                    isSystemProvider, +                    mSystemProvider.getDefaultRoute()); +        } + +        /** +         * Adds provided routes to {@link #mLastNotifiedRoutesToPrivilegedRouters}. Also adds them +         * to {@link #mLastNotifiedRoutesToNonPrivilegedRouters} if they were provided by a +         * non-system route provider. Overwrites any route with matching id that already exists. +         * +         * @param routes list of routes to be added. +         * @param isSystemRoutes indicates whether routes come from a system route provider. +         */ +        private void addToRoutesMap( +                @NonNull Collection<MediaRoute2Info> routes, boolean isSystemRoutes) { +            for (MediaRoute2Info route : routes) { +                if (!isSystemRoutes) { +                    mLastNotifiedRoutesToNonPrivilegedRouters.put(route.getId(), route); +                } +                mLastNotifiedRoutesToPrivilegedRouters.put(route.getId(), route); +            } +        } + +        /** +         * Removes provided routes from {@link #mLastNotifiedRoutesToPrivilegedRouters}. Also +         * removes them from {@link #mLastNotifiedRoutesToNonPrivilegedRouters} if they were +         * provided by a non-system route provider. +         * +         * @param routes list of routes to be removed. +         * @param isSystemRoutes whether routes come from a system route provider. +         */ +        private void removeFromRoutesMap( +                @NonNull Collection<MediaRoute2Info> routes, boolean isSystemRoutes) { +            for (MediaRoute2Info route : routes) { +                if (!isSystemRoutes) { +                    mLastNotifiedRoutesToNonPrivilegedRouters.remove(route.getId()); +                } +                mLastNotifiedRoutesToPrivilegedRouters.remove(route.getId()); +            } +        } + +        /** +         * Dispatches the latest route updates in {@link #mLastNotifiedRoutesToPrivilegedRouters} +         * and {@link #mLastNotifiedRoutesToNonPrivilegedRouters} to registered {@link +         * android.media.MediaRouter2 routers} and {@link MediaRouter2Manager managers} after a call +         * to {@link #onProviderStateChangedOnHandler(MediaRoute2Provider)}. Ignores if no changes +         * were made. +         * +         * @param hasAddedOrModifiedRoutes whether routes were added or modified. +         * @param hasRemovedRoutes whether routes were removed. +         * @param isSystemProvider whether the latest update was caused by a system provider. +         * @param defaultRoute the current default route in {@link #mSystemProvider}. +         */ +        private void dispatchUpdates( +                boolean hasAddedOrModifiedRoutes, +                boolean hasRemovedRoutes, +                boolean isSystemProvider, +                MediaRoute2Info defaultRoute) { + +            // Ignore if no changes. +            if (!hasAddedOrModifiedRoutes && !hasRemovedRoutes) { +                return; +            } +              List<IMediaRouter2> routersWithModifyAudioRoutingPermission = getRouters(true);              List<IMediaRouter2> routersWithoutModifyAudioRoutingPermission = getRouters(false);              List<IMediaRouter2Manager> managers = getManagers(); -            List<MediaRoute2Info> defaultRoute = new ArrayList<>(); -            defaultRoute.add(mSystemProvider.getDefaultRoute()); - -            if (addedRoutes.size() > 0) { -                notifyRoutesAddedToRouters(routersWithModifyAudioRoutingPermission, addedRoutes); -                if (!provider.mIsSystemRouteProvider) { -                    notifyRoutesAddedToRouters(routersWithoutModifyAudioRoutingPermission, -                            addedRoutes); -                } else if (prevInfo == null) { -                    notifyRoutesAddedToRouters(routersWithoutModifyAudioRoutingPermission, -                            defaultRoute); -                } // 'else' is handled as changed routes -                notifyRoutesAddedToManagers(managers, addedRoutes); -            } -            if (removedRoutes.size() > 0) { -                notifyRoutesRemovedToRouters(routersWithModifyAudioRoutingPermission, -                        removedRoutes); -                if (!provider.mIsSystemRouteProvider) { -                    notifyRoutesRemovedToRouters(routersWithoutModifyAudioRoutingPermission, -                            removedRoutes); -                } -                notifyRoutesRemovedToManagers(managers, removedRoutes); -            } -            if (changedRoutes.size() > 0) { -                notifyRoutesChangedToRouters(routersWithModifyAudioRoutingPermission, -                        changedRoutes); -                if (!provider.mIsSystemRouteProvider) { -                    notifyRoutesChangedToRouters(routersWithoutModifyAudioRoutingPermission, -                            changedRoutes); -                } else if (prevInfo != null) { -                    notifyRoutesChangedToRouters(routersWithoutModifyAudioRoutingPermission, -                            defaultRoute); -                } // 'else' is handled as added routes -                notifyRoutesChangedToManagers(managers, changedRoutes); -            } -        } - -        private int getLastProviderInfoIndex(@NonNull String providerId) { -            for (int i = 0; i < mLastProviderInfos.size(); i++) { -                MediaRoute2ProviderInfo providerInfo = mLastProviderInfos.get(i); -                if (TextUtils.equals(providerInfo.getUniqueId(), providerId)) { + +            // Managers receive all provider updates with all routes. +            notifyRoutesUpdatedToManagers( +                    managers, new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values())); + +            // Routers with modify audio permission (usually system routers) receive all provider +            // updates with all routes. +            notifyRoutesUpdatedToRouters( +                    routersWithModifyAudioRoutingPermission, +                    new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values())); + +            if (!isSystemProvider) { +                // Regular routers receive updates from all non-system providers with all non-system +                // routes. +                notifyRoutesUpdatedToRouters( +                        routersWithoutModifyAudioRoutingPermission, +                        new ArrayList<>(mLastNotifiedRoutesToNonPrivilegedRouters.values())); +            } else if (hasAddedOrModifiedRoutes) { +                // On system provider updates, regular routers receive the updated default route. +                // This is the only system route they should receive. +                mLastNotifiedRoutesToNonPrivilegedRouters.put(defaultRoute.getId(), defaultRoute); +                notifyRoutesUpdatedToRouters( +                        routersWithoutModifyAudioRoutingPermission, +                        new ArrayList<>(mLastNotifiedRoutesToNonPrivilegedRouters.values())); +            } +        } + +        /** +         * Returns the index of the first element in {@code lastProviderInfos} that matches the +         * specified unique id. +         * +         * @param uniqueId unique id of {@link MediaRoute2ProviderInfo} to be found. +         * @param lastProviderInfos list of {@link MediaRoute2ProviderInfo}. +         * @return index of found element, or -1 if not found. +         */ +        private static int indexOfRouteProviderInfoByUniqueId( +                @NonNull String uniqueId, +                @NonNull List<MediaRoute2ProviderInfo> lastProviderInfos) { +            for (int i = 0; i < lastProviderInfos.size(); i++) { +                MediaRoute2ProviderInfo providerInfo = lastProviderInfos.get(i); +                if (TextUtils.equals(providerInfo.getUniqueId(), uniqueId)) {                      return i;                  }              } @@ -1989,41 +2111,19 @@ class MediaRouter2ServiceImpl {              }          } -        private void notifyRoutesAddedToRouters(@NonNull List<IMediaRouter2> routers, -                @NonNull List<MediaRoute2Info> routes) { -            for (IMediaRouter2 router : routers) { -                try { -                    router.notifyRoutesAdded(routes); -                } catch (RemoteException ex) { -                    Slog.w(TAG, "Failed to notify routes added. Router probably died.", ex); -                } -            } -        } - -        private void notifyRoutesRemovedToRouters(@NonNull List<IMediaRouter2> routers, -                @NonNull List<MediaRoute2Info> routes) { -            for (IMediaRouter2 router : routers) { -                try { -                    router.notifyRoutesRemoved(routes); -                } catch (RemoteException ex) { -                    Slog.w(TAG, "Failed to notify routes removed. Router probably died.", ex); -                } -            } -        } - -        private void notifyRoutesChangedToRouters(@NonNull List<IMediaRouter2> routers, -                @NonNull List<MediaRoute2Info> routes) { +        private void notifyRoutesUpdatedToRouters( +                @NonNull List<IMediaRouter2> routers, @NonNull List<MediaRoute2Info> routes) {              for (IMediaRouter2 router : routers) {                  try { -                    router.notifyRoutesChanged(routes); +                    router.notifyRoutesUpdated(routes);                  } catch (RemoteException ex) { -                    Slog.w(TAG, "Failed to notify routes changed. Router probably died.", ex); +                    Slog.w(TAG, "Failed to notify routes updated. Router probably died.", ex);                  }              }          } -        private void notifySessionInfoChangedToRouters(@NonNull List<IMediaRouter2> routers, -                @NonNull RoutingSessionInfo sessionInfo) { +        private void notifySessionInfoChangedToRouters( +                @NonNull List<IMediaRouter2> routers, @NonNull RoutingSessionInfo sessionInfo) {              for (IMediaRouter2 router : routers) {                  try {                      router.notifySessionInfoChanged(sessionInfo); @@ -2033,48 +2133,31 @@ class MediaRouter2ServiceImpl {              }          } -        private void notifyRoutesToManager(@NonNull IMediaRouter2Manager manager) { -            List<MediaRoute2Info> routes = new ArrayList<>(); -            for (MediaRoute2ProviderInfo providerInfo : mLastProviderInfos) { -                routes.addAll(providerInfo.getRoutes()); -            } -            if (routes.size() == 0) { +        /** +         * Notifies {@code manager} with all known routes. This only happens once after {@code +         * manager} is registered through {@link #registerManager(IMediaRouter2Manager, String) +         * registerManager()}. +         * +         * @param manager {@link IMediaRouter2Manager} to be notified. +         */ +        private void notifyInitialRoutesToManager(@NonNull IMediaRouter2Manager manager) { +            if (mLastNotifiedRoutesToPrivilegedRouters.isEmpty()) {                  return;              }              try { -                manager.notifyRoutesAdded(routes); +                manager.notifyRoutesUpdated( +                        new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values()));              } catch (RemoteException ex) {                  Slog.w(TAG, "Failed to notify all routes. Manager probably died.", ex);              }          } -        private void notifyRoutesAddedToManagers(@NonNull List<IMediaRouter2Manager> managers, -                @NonNull List<MediaRoute2Info> routes) { -            for (IMediaRouter2Manager manager : managers) { -                try { -                    manager.notifyRoutesAdded(routes); -                } catch (RemoteException ex) { -                    Slog.w(TAG, "Failed to notify routes added. Manager probably died.", ex); -                } -            } -        } - -        private void notifyRoutesRemovedToManagers(@NonNull List<IMediaRouter2Manager> managers, -                @NonNull List<MediaRoute2Info> routes) { -            for (IMediaRouter2Manager manager : managers) { -                try { -                    manager.notifyRoutesRemoved(routes); -                } catch (RemoteException ex) { -                    Slog.w(TAG, "Failed to notify routes removed. Manager probably died.", ex); -                } -            } -        } - -        private void notifyRoutesChangedToManagers(@NonNull List<IMediaRouter2Manager> managers, +        private void notifyRoutesUpdatedToManagers( +                @NonNull List<IMediaRouter2Manager> managers,                  @NonNull List<MediaRoute2Info> routes) {              for (IMediaRouter2Manager manager : managers) {                  try { -                    manager.notifyRoutesChanged(routes); +                    manager.notifyRoutesUpdated(routes);                  } catch (RemoteException ex) {                      Slog.w(TAG, "Failed to notify routes changed. Manager probably died.", ex);                  }  |