diff options
140 files changed, 3078 insertions, 1214 deletions
diff --git a/api/coverage/tools/ExtractFlaggedApis.kt b/api/coverage/tools/ExtractFlaggedApis.kt index 0a3ae4f790b0..e50f7f876f51 100644 --- a/api/coverage/tools/ExtractFlaggedApis.kt +++ b/api/coverage/tools/ExtractFlaggedApis.kt @@ -88,6 +88,6 @@ fun getFlagAnnotation(item: Item): String? {      return item.modifiers          .findAnnotation("android.annotation.FlaggedApi")          ?.findAttribute("value") -        ?.value +        ?.legacyValue          ?.value() as? String  } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 7c293cb9cb3b..f5277fd86a57 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1983,7 +1983,7 @@ public class Notification implements Parcelable           * treatment.           * @hide           */ -        public static final String EXTRA_IS_MAGIC = "android.extra.IS_MAGIC"; +        public static final String EXTRA_IS_ANIMATED = "android.extra.IS_ANIMATED";          private final Bundle mExtras;          @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index c41a5ce02e61..3dfad5396634 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -214,6 +214,13 @@ flag {  }  flag { +    name: "request_key_capture_api" +    namespace: "input" +    description: "Adds support for key capture APIs" +    bug: "375435312" +} + +flag {      name: "fix_search_modifier_fallbacks"      namespace: "input"      description: "Fixes a bug in which fallbacks from Search based key combinations were not activating." diff --git a/core/java/android/view/IDisplayWindowListener.aidl b/core/java/android/view/IDisplayWindowListener.aidl index 67ae7430e0b7..79f61f3f0d77 100644 --- a/core/java/android/view/IDisplayWindowListener.aidl +++ b/core/java/android/view/IDisplayWindowListener.aidl @@ -64,4 +64,9 @@ oneway interface IDisplayWindowListener {       * Called when the keep clear ares on a display have changed.       */      void onKeepClearAreasChanged(int displayId, in List<Rect> restricted, in List<Rect> unrestricted); + +    /** +     * Called when the eligibility of the desktop mode for a display have changed. +     */ +    void onDesktopModeEligibleChanged(int displayId);  } diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 9b3d6242b213..28a922d56019 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -148,7 +148,7 @@ public enum DesktopModeFlags {      INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC(              Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true),      INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES( -            Flags::inheritTaskBoundsForTrampolineTaskLaunches, false), +            Flags::inheritTaskBoundsForTrampolineTaskLaunches, true),      SKIP_DECOR_VIEW_RELAYOUT_WHEN_CLOSING_BUGFIX(              Flags::skipDecorViewRelayoutWhenClosingBugfix, false),      // go/keep-sorted end diff --git a/core/java/com/android/internal/app/MediaRouteChooserContentManager.java b/core/java/com/android/internal/app/MediaRouteChooserContentManager.java index 09c6f5e6caaa..64538fdbdac1 100644 --- a/core/java/com/android/internal/app/MediaRouteChooserContentManager.java +++ b/core/java/com/android/internal/app/MediaRouteChooserContentManager.java @@ -17,21 +17,57 @@  package com.android.internal.app;  import android.content.Context; +import android.media.MediaRouter; +import android.text.TextUtils;  import android.view.Gravity; +import android.view.LayoutInflater;  import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter;  import android.widget.LinearLayout;  import android.widget.ListView; +import android.widget.TextView;  import com.android.internal.R; +import java.util.Comparator; +  public class MediaRouteChooserContentManager { +    /** +     * A delegate interface that a MediaRouteChooser UI should implement. It allows the content +     * manager to inform the UI of any UI changes that need to be made in response to content +     * updates. +     */ +    public interface Delegate { +        /** +         * Dismiss the UI to transition to a different workflow. +         */ +        void dismissView(); + +        /** +         * Returns true if the progress bar should be shown when the list view is empty. +         */ +        boolean showProgressBarWhenEmpty(); +    } +      Context mContext; +    Delegate mDelegate; -    private final boolean mShowProgressBarWhenEmpty; +    private final MediaRouter mRouter; +    private final MediaRouterCallback mCallback; -    public MediaRouteChooserContentManager(Context context, boolean showProgressBarWhenEmpty) { +    private int mRouteTypes; +    private RouteAdapter mAdapter; +    private boolean mAttachedToWindow; + +    public MediaRouteChooserContentManager(Context context, Delegate delegate) {          mContext = context; -        mShowProgressBarWhenEmpty = showProgressBarWhenEmpty; +        mDelegate = delegate; + +        mRouter = context.getSystemService(MediaRouter.class); +        mCallback = new MediaRouterCallback(); +        mAdapter = new RouteAdapter(mContext);      }      /** @@ -41,9 +77,11 @@ public class MediaRouteChooserContentManager {      public void bindViews(View containerView) {          View emptyView = containerView.findViewById(android.R.id.empty);          ListView listView = containerView.findViewById(R.id.media_route_list); +        listView.setAdapter(mAdapter); +        listView.setOnItemClickListener(mAdapter);          listView.setEmptyView(emptyView); -        if (!mShowProgressBarWhenEmpty) { +        if (!mDelegate.showProgressBarWhenEmpty()) {              containerView.findViewById(R.id.media_route_progress_bar).setVisibility(View.GONE);              // Center the empty view when the progress bar is not shown. @@ -53,4 +91,170 @@ public class MediaRouteChooserContentManager {              emptyView.setLayoutParams(params);          }      } + +    /** +     * Called when this UI is attached to a window.. +     */ +    public void onAttachedToWindow() { +        mAttachedToWindow = true; +        mRouter.addCallback(mRouteTypes, mCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); +        refreshRoutes(); +    } + +    /** +     * Called when this UI is detached from a window.. +     */ +    public void onDetachedFromWindow() { +        mAttachedToWindow = false; +        mRouter.removeCallback(mCallback); +    } + +    /** +     * Gets the media route types for filtering the routes that the user can +     * select using the media route chooser dialog. +     * +     * @return The route types. +     */ +    public int getRouteTypes() { +        return mRouteTypes; +    } + +    /** +     * Sets the types of routes that will be shown in the media route chooser dialog +     * launched by this button. +     * +     * @param types The route types to match. +     */ +    public void setRouteTypes(int types) { +        if (mRouteTypes != types) { +            mRouteTypes = types; + +            if (mAttachedToWindow) { +                mRouter.removeCallback(mCallback); +                mRouter.addCallback(types, mCallback, +                        MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); +            } + +            refreshRoutes(); +        } +    } + +    /** +     * Refreshes the list of routes that are shown in the chooser dialog. +     */ +    public void refreshRoutes() { +        if (mAttachedToWindow) { +            mAdapter.update(); +        } +    } + +    /** +     * Returns true if the route should be included in the list. +     * <p> +     * The default implementation returns true for enabled non-default routes that +     * match the route types.  Subclasses can override this method to filter routes +     * differently. +     * </p> +     * +     * @param route The route to consider, never null. +     * @return True if the route should be included in the chooser dialog. +     */ +    public boolean onFilterRoute(MediaRouter.RouteInfo route) { +        return !route.isDefault() && route.isEnabled() && route.matchesTypes(mRouteTypes); +    } + +    private final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo> +            implements AdapterView.OnItemClickListener { +        private final LayoutInflater mInflater; + +        RouteAdapter(Context context) { +            super(context, 0); +            mInflater = LayoutInflater.from(context); +        } + +        public void update() { +            clear(); +            final int count = mRouter.getRouteCount(); +            for (int i = 0; i < count; i++) { +                MediaRouter.RouteInfo route = mRouter.getRouteAt(i); +                if (onFilterRoute(route)) { +                    add(route); +                } +            } +            sort(RouteComparator.sInstance); +            notifyDataSetChanged(); +        } + +        @Override +        public boolean areAllItemsEnabled() { +            return false; +        } + +        @Override +        public boolean isEnabled(int position) { +            return getItem(position).isEnabled(); +        } + +        @Override +        public View getView(int position, View convertView, ViewGroup parent) { +            View view = convertView; +            if (view == null) { +                view = mInflater.inflate(R.layout.media_route_list_item, parent, false); +            } +            MediaRouter.RouteInfo route = getItem(position); +            TextView text1 = view.findViewById(android.R.id.text1); +            TextView text2 = view.findViewById(android.R.id.text2); +            text1.setText(route.getName()); +            CharSequence description = route.getDescription(); +            if (TextUtils.isEmpty(description)) { +                text2.setVisibility(View.GONE); +                text2.setText(""); +            } else { +                text2.setVisibility(View.VISIBLE); +                text2.setText(description); +            } +            view.setEnabled(route.isEnabled()); +            return view; +        } + +        @Override +        public void onItemClick(AdapterView<?> parent, View view, int position, long id) { +            MediaRouter.RouteInfo route = getItem(position); +            if (route.isEnabled()) { +                route.select(); +                mDelegate.dismissView(); +            } +        } +    } + +    private static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> { +        public static final RouteComparator sInstance = new RouteComparator(); + +        @Override +        public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) { +            return lhs.getName().toString().compareTo(rhs.getName().toString()); +        } +    } + +    private final class MediaRouterCallback extends MediaRouter.SimpleCallback { +        @Override +        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) { +            refreshRoutes(); +        } + +        @Override +        public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) { +            refreshRoutes(); +        } + +        @Override +        public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) { +            refreshRoutes(); +        } + +        @Override +        public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) { +            mDelegate.dismissView(); +        } +    }  } diff --git a/core/java/com/android/internal/app/MediaRouteChooserDialog.java b/core/java/com/android/internal/app/MediaRouteChooserDialog.java index 5030a143ea94..fc7ed89f395c 100644 --- a/core/java/com/android/internal/app/MediaRouteChooserDialog.java +++ b/core/java/com/android/internal/app/MediaRouteChooserDialog.java @@ -19,23 +19,14 @@ package com.android.internal.app;  import android.app.AlertDialog;  import android.content.Context;  import android.media.MediaRouter; -import android.media.MediaRouter.RouteInfo;  import android.os.Bundle; -import android.text.TextUtils;  import android.util.TypedValue;  import android.view.LayoutInflater;  import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter;  import android.widget.Button; -import android.widget.ListView; -import android.widget.TextView;  import com.android.internal.R; -import java.util.Comparator; -  /**   * This class implements the route chooser dialog for {@link MediaRouter}.   * <p> @@ -47,15 +38,11 @@ import java.util.Comparator;   *   * TODO: Move this back into the API, as in the support library media router.   */ -public class MediaRouteChooserDialog extends AlertDialog { -    private final MediaRouter mRouter; -    private final MediaRouterCallback mCallback; - -    private int mRouteTypes; +public class MediaRouteChooserDialog extends AlertDialog implements +        MediaRouteChooserContentManager.Delegate {      private View.OnClickListener mExtendedSettingsClickListener; -    private RouteAdapter mAdapter;      private Button mExtendedSettingsButton; -    private boolean mAttachedToWindow; +    private final boolean mShowProgressBarWhenEmpty;      private final MediaRouteChooserContentManager mContentManager; @@ -66,19 +53,8 @@ public class MediaRouteChooserDialog extends AlertDialog {      public MediaRouteChooserDialog(Context context, int theme, boolean showProgressBarWhenEmpty) {          super(context, theme); -        mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE); -        mCallback = new MediaRouterCallback(); -        mContentManager = new MediaRouteChooserContentManager(context, showProgressBarWhenEmpty); -    } - -    /** -     * Gets the media route types for filtering the routes that the user can -     * select using the media route chooser dialog. -     * -     * @return The route types. -     */ -    public int getRouteTypes() { -        return mRouteTypes; +        mShowProgressBarWhenEmpty = showProgressBarWhenEmpty; +        mContentManager = new MediaRouteChooserContentManager(context, this);      }      /** @@ -88,17 +64,7 @@ public class MediaRouteChooserDialog extends AlertDialog {       * @param types The route types to match.       */      public void setRouteTypes(int types) { -        if (mRouteTypes != types) { -            mRouteTypes = types; - -            if (mAttachedToWindow) { -                mRouter.removeCallback(mCallback); -                mRouter.addCallback(types, mCallback, -                        MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); -            } - -            refreshRoutes(); -        } +        mContentManager.setRouteTypes(types);      }      public void setExtendedSettingsClickListener(View.OnClickListener listener) { @@ -108,21 +74,6 @@ public class MediaRouteChooserDialog extends AlertDialog {          }      } -    /** -     * Returns true if the route should be included in the list. -     * <p> -     * The default implementation returns true for enabled non-default routes that -     * match the route types.  Subclasses can override this method to filter routes -     * differently. -     * </p> -     * -     * @param route The route to consider, never null. -     * @return True if the route should be included in the chooser dialog. -     */ -    public boolean onFilterRoute(MediaRouter.RouteInfo route) { -        return !route.isDefault() && route.isEnabled() && route.matchesTypes(mRouteTypes); -    } -      @Override      protected void onCreate(Bundle savedInstanceState) {          // Note: setView must be called before super.onCreate(). @@ -130,7 +81,7 @@ public class MediaRouteChooserDialog extends AlertDialog {                  R.layout.media_route_chooser_dialog, null);          setView(containerView); -        setTitle(mRouteTypes == MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY +        setTitle(mContentManager.getRouteTypes() == MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY                  ? R.string.media_route_chooser_title_for_remote_display                  : R.string.media_route_chooser_title); @@ -139,11 +90,6 @@ public class MediaRouteChooserDialog extends AlertDialog {          super.onCreate(savedInstanceState); -        mAdapter = new RouteAdapter(getContext()); -        ListView listView = findViewById(R.id.media_route_list); -        listView.setAdapter(mAdapter); -        listView.setOnItemClickListener(mAdapter); -          mExtendedSettingsButton = findViewById(R.id.media_route_extended_settings_button);          updateExtendedSettingsButton(); @@ -161,27 +107,23 @@ public class MediaRouteChooserDialog extends AlertDialog {      @Override      public void onAttachedToWindow() {          super.onAttachedToWindow(); - -        mAttachedToWindow = true; -        mRouter.addCallback(mRouteTypes, mCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); -        refreshRoutes(); +        mContentManager.onAttachedToWindow();      }      @Override      public void onDetachedFromWindow() { -        mAttachedToWindow = false; -        mRouter.removeCallback(mCallback); - +        mContentManager.onDetachedFromWindow();          super.onDetachedFromWindow();      } -    /** -     * Refreshes the list of routes that are shown in the chooser dialog. -     */ -    public void refreshRoutes() { -        if (mAttachedToWindow) { -            mAdapter.update(); -        } +    @Override +    public void dismissView() { +        dismiss(); +    } + +    @Override +    public boolean showProgressBarWhenEmpty() { +        return mShowProgressBarWhenEmpty;      }      static boolean isLightTheme(Context context) { @@ -189,99 +131,4 @@ public class MediaRouteChooserDialog extends AlertDialog {          return context.getTheme().resolveAttribute(R.attr.isLightTheme, value, true)                  && value.data != 0;      } - -    private final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo> -            implements ListView.OnItemClickListener { -        private final LayoutInflater mInflater; - -        public RouteAdapter(Context context) { -            super(context, 0); -            mInflater = LayoutInflater.from(context); -        } - -        public void update() { -            clear(); -            final int count = mRouter.getRouteCount(); -            for (int i = 0; i < count; i++) { -                MediaRouter.RouteInfo route = mRouter.getRouteAt(i); -                if (onFilterRoute(route)) { -                    add(route); -                } -            } -            sort(RouteComparator.sInstance); -            notifyDataSetChanged(); -        } - -        @Override -        public boolean areAllItemsEnabled() { -            return false; -        } - -        @Override -        public boolean isEnabled(int position) { -            return getItem(position).isEnabled(); -        } - -        @Override -        public View getView(int position, View convertView, ViewGroup parent) { -            View view = convertView; -            if (view == null) { -                view = mInflater.inflate(R.layout.media_route_list_item, parent, false); -            } -            MediaRouter.RouteInfo route = getItem(position); -            TextView text1 = view.findViewById(android.R.id.text1); -            TextView text2 = view.findViewById(android.R.id.text2); -            text1.setText(route.getName()); -            CharSequence description = route.getDescription(); -            if (TextUtils.isEmpty(description)) { -                text2.setVisibility(View.GONE); -                text2.setText(""); -            } else { -                text2.setVisibility(View.VISIBLE); -                text2.setText(description); -            } -            view.setEnabled(route.isEnabled()); -            return view; -        } - -        @Override -        public void onItemClick(AdapterView<?> parent, View view, int position, long id) { -            MediaRouter.RouteInfo route = getItem(position); -            if (route.isEnabled()) { -                route.select(); -                dismiss(); -            } -        } -    } - -    private final class MediaRouterCallback extends MediaRouter.SimpleCallback { -        @Override -        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) { -            refreshRoutes(); -        } - -        @Override -        public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) { -            refreshRoutes(); -        } - -        @Override -        public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) { -            refreshRoutes(); -        } - -        @Override -        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { -            dismiss(); -        } -    } - -    private static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> { -        public static final RouteComparator sInstance = new RouteComparator(); - -        @Override -        public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) { -            return lhs.getName().toString().compareTo(rhs.getName().toString()); -        } -    }  } diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index 1d40110dc7ca..fc46418478c8 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -318,6 +318,11 @@      <bool name="config_send_satellite_datagram_to_modem_in_demo_mode">false</bool>      <java-symbol type="bool" name="config_send_satellite_datagram_to_modem_in_demo_mode" /> +    <!-- Whether the device supports disabling satellite while satellite enabling is in progress. +         --> +    <bool name="config_support_disable_satellite_while_enable_in_progress">true</bool> +    <java-symbol type="bool" name="config_support_disable_satellite_while_enable_in_progress" /> +      <!-- List of country codes where oem-enabled satellite services are either allowed or disallowed           by the device. Each country code is a lowercase 2 character ISO-3166-1 alpha-2.           --> diff --git a/core/tests/coretests/src/com/android/internal/app/MediaRouteChooserContentManagerTest.kt b/core/tests/coretests/src/com/android/internal/app/MediaRouteChooserContentManagerTest.kt new file mode 100644 index 000000000000..bbed6e0c3618 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/MediaRouteChooserContentManagerTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app + +import android.content.Context +import android.media.MediaRouter +import android.testing.TestableLooper.RunWithLooper +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.R +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +@SmallTest +@RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidJUnit4::class) +class MediaRouteChooserContentManagerTest { +    private val context: Context = getInstrumentation().context + +    @Test +    fun bindViews_showProgressBarWhenEmptyTrue_progressBarVisible() { +        val delegate = mock<MediaRouteChooserContentManager.Delegate> { +            on { showProgressBarWhenEmpty() } doReturn true +        } +        val contentManager = MediaRouteChooserContentManager(context, delegate) +        val containerView = inflateMediaRouteChooserDialog() +        contentManager.bindViews(containerView) + +        assertThat(containerView.findViewById<View>(R.id.media_route_progress_bar).visibility) +            .isEqualTo(View.VISIBLE) +    } + +    @Test +    fun bindViews_showProgressBarWhenEmptyFalse_progressBarNotVisible() { +        val delegate = mock<MediaRouteChooserContentManager.Delegate> { +            on { showProgressBarWhenEmpty() } doReturn false +        } +        val contentManager = MediaRouteChooserContentManager(context, delegate) +        val containerView = inflateMediaRouteChooserDialog() +        contentManager.bindViews(containerView) +        val emptyView = containerView.findViewById<View>(android.R.id.empty) +        val emptyViewLayout = emptyView.layoutParams as? LinearLayout.LayoutParams + +        assertThat(containerView.findViewById<View>(R.id.media_route_progress_bar).visibility) +            .isEqualTo(View.GONE) +        assertThat(emptyView.visibility).isEqualTo(View.VISIBLE) +        assertThat(emptyViewLayout?.gravity).isEqualTo(Gravity.CENTER) +    } + +    @Test +    fun onFilterRoute_routeDefault_returnsFalse() { +        val delegate: MediaRouteChooserContentManager.Delegate = mock() +        val contentManager = MediaRouteChooserContentManager(context, delegate) +        val route: MediaRouter.RouteInfo = mock<MediaRouter.RouteInfo> { +            on { isDefault } doReturn true +        } + +        assertThat(contentManager.onFilterRoute(route)).isEqualTo(false) +    } + +    @Test +    fun onFilterRoute_routeNotEnabled_returnsFalse() { +        val delegate: MediaRouteChooserContentManager.Delegate = mock() +        val contentManager = MediaRouteChooserContentManager(context, delegate) +        val route: MediaRouter.RouteInfo = mock<MediaRouter.RouteInfo> { +            on { isEnabled } doReturn false +        } + +        assertThat(contentManager.onFilterRoute(route)).isEqualTo(false) +    } + +    @Test +    fun onFilterRoute_routeNotMatch_returnsFalse() { +        val delegate: MediaRouteChooserContentManager.Delegate = mock() +        val contentManager = MediaRouteChooserContentManager(context, delegate) +        val route: MediaRouter.RouteInfo = mock<MediaRouter.RouteInfo> { +            on { matchesTypes(anyInt()) } doReturn false +        } + +        assertThat(contentManager.onFilterRoute(route)).isEqualTo(false) +    } + +    @Test +    fun onFilterRoute_returnsTrue() { +        val delegate: MediaRouteChooserContentManager.Delegate = mock() +        val contentManager = MediaRouteChooserContentManager(context, delegate) +        val route: MediaRouter.RouteInfo = mock<MediaRouter.RouteInfo> { +            on { isDefault } doReturn false +            on { isEnabled } doReturn true +            on { matchesTypes(anyInt()) } doReturn true +        } + +        assertThat(contentManager.onFilterRoute(route)).isEqualTo(true) +    } + +    @Test +    fun onAttachedToWindow() { +        val delegate: MediaRouteChooserContentManager.Delegate = mock() +        val mediaRouter: MediaRouter = mock() +        val layoutInflater: LayoutInflater = mock() +        val context: Context = mock<Context> { +            on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE +            on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter +            on { getSystemService(Context.LAYOUT_INFLATER_SERVICE) } doReturn layoutInflater +        } +        val contentManager = MediaRouteChooserContentManager(context, delegate) +        contentManager.routeTypes = MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY + +        contentManager.onAttachedToWindow() + +        verify(mediaRouter).addCallback(eq(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY), any(), +            eq(MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN)) +    } + +    @Test +    fun onDetachedFromWindow() { +        val delegate: MediaRouteChooserContentManager.Delegate = mock() +        val layoutInflater: LayoutInflater = mock() +        val mediaRouter: MediaRouter = mock() +        val context: Context = mock<Context> { +            on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE +            on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter +            on { getSystemService(Context.LAYOUT_INFLATER_SERVICE) } doReturn layoutInflater +        } +        val contentManager = MediaRouteChooserContentManager(context, delegate) + +        contentManager.onDetachedFromWindow() + +        verify(mediaRouter).removeCallback(any()) +    } + +    @Test +    fun setRouteTypes() { +        val delegate: MediaRouteChooserContentManager.Delegate = mock() +        val mediaRouter: MediaRouter = mock() +        val layoutInflater: LayoutInflater = mock() +        val context: Context = mock<Context> { +            on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE +            on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter +            on { getSystemService(Context.LAYOUT_INFLATER_SERVICE) } doReturn layoutInflater +        } +        val contentManager = MediaRouteChooserContentManager(context, delegate) +        contentManager.onAttachedToWindow() + +        contentManager.routeTypes = MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY + +        assertThat(contentManager.routeTypes).isEqualTo(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) +        verify(mediaRouter).addCallback(eq(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY), any(), +            eq(MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN)) +    } + +    private fun inflateMediaRouteChooserDialog(): View { +        return LayoutInflater.from(context) +            .inflate(R.layout.media_route_chooser_dialog, null, false) +    } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 70fa48cca0b0..6a7b5cc0e1ba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -81,6 +81,7 @@ import android.view.WindowInsets;  import android.view.WindowManager;  import android.window.ScreenCapture;  import android.window.ScreenCapture.SynchronousScreenCaptureListener; +import android.window.TransitionInfo;  import android.window.WindowContainerToken;  import android.window.WindowContainerTransaction; @@ -156,7 +157,7 @@ import java.util.function.IntConsumer;   */  public class BubbleController implements ConfigurationChangeListener,          RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider, -        BubbleBarDragListener { +        BubbleBarDragListener, BubbleTaskUnfoldTransitionMerger {      private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; @@ -2175,6 +2176,32 @@ public class BubbleController implements ConfigurationChangeListener,          });      } +    @Override +    public boolean mergeTaskWithUnfold(@NonNull ActivityManager.RunningTaskInfo taskInfo, +            @NonNull TransitionInfo.Change change, +            @NonNull SurfaceControl.Transaction startT, +            @NonNull SurfaceControl.Transaction finishT) { +        if (!mBubbleTransitions.mTaskViewTransitions.isTaskViewTask(taskInfo)) { +            // if this task isn't managed by bubble transitions just bail. +            return false; +        } +        if (isShowingAsBubbleBar()) { +            // if bubble bar is enabled, the task view will switch to a new surface on unfold, so we +            // should not merge the transition. +            return false; +        } + +        boolean merged = mBubbleTransitions.mTaskViewTransitions.updateBoundsForUnfold( +                change.getEndAbsBounds(), startT, finishT, change.getTaskInfo(), change.getLeash()); +        if (merged) { +            BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); +            if (selectedBubble != null && selectedBubble.getExpandedView() != null) { +                selectedBubble.getExpandedView().onContainerClipUpdate(); +            } +        } +        return merged; +    } +      /** When bubbles are floating, this will be used to notify the floating views. */      private final BubbleViewCallback mBubbleStackViewCallback = new BubbleViewCallback() {          @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 290ef1633819..ac8393576477 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -843,7 +843,8 @@ public class BubbleExpandedView extends LinearLayout {          onContainerClipUpdate();      } -    private void onContainerClipUpdate() { +    /** Updates the clip bounds. */ +    public void onContainerClipUpdate() {          if (mTopClip == 0 && mBottomClip == 0 && mRightClip == 0 && mLeftClip == 0) {              if (mIsClipping) {                  mIsClipping = false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskUnfoldTransitionMerger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskUnfoldTransitionMerger.kt new file mode 100644 index 000000000000..13fabc8b1d91 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskUnfoldTransitionMerger.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.app.ActivityManager +import android.view.SurfaceControl +import android.window.TransitionInfo + +/** Merges a bubble task transition with the unfold transition. */ +interface BubbleTaskUnfoldTransitionMerger { + +    /** Attempts to merge the transition. Returns `true` if the change was merged. */ +    fun mergeTaskWithUnfold( +        taskInfo: ActivityManager.RunningTaskInfo, +        change: TransitionInfo.Change, +        startT: SurfaceControl.Transaction, +        finishT: SurfaceControl.Transaction +    ): Boolean +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index 46f6e40ec5e8..06d734c71f6a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -355,6 +355,19 @@ public class DisplayController {          }      } +    private void onDesktopModeEligibleChanged(int displayId) { +        synchronized (mDisplays) { +            if (mDisplays.get(displayId) == null || getDisplay(displayId) == null) { +                Slog.w(TAG, "Skipping onDesktopModeEligibleChanged on unknown" +                        + " display, displayId=" + displayId); +                return; +            } +            for (int i = mDisplayChangedListeners.size() - 1; i >= 0; --i) { +                mDisplayChangedListeners.get(i).onDesktopModeEligibleChanged(displayId); +            } +        } +    } +      private static class DisplayRecord {          private int mDisplayId;          private Context mContext; @@ -422,6 +435,13 @@ public class DisplayController {                          new ArraySet<>(restricted), new ArraySet<>(unrestricted));              });          } + +        @Override +        public void onDesktopModeEligibleChanged(int displayId) { +            mMainExecutor.execute(() -> { +                DisplayController.this.onDesktopModeEligibleChanged(displayId); +            }); +        }      }      /** @@ -467,5 +487,10 @@ public class DisplayController {           * Called when the display topology has changed.           */          default void onTopologyChanged(DisplayTopology topology) {} + +        /** +         * Called when the eligibility of the desktop mode for a display have changed. +         */ +        default void onDesktopModeEligibleChanged(int displayId) {}      }  } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 5fbbb0bf1e78..07745d487f64 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -62,6 +62,7 @@ import com.android.wm.shell.bubbles.BubbleEducationController;  import com.android.wm.shell.bubbles.BubbleLogger;  import com.android.wm.shell.bubbles.BubblePositioner;  import com.android.wm.shell.bubbles.BubbleResizabilityChecker; +import com.android.wm.shell.bubbles.BubbleTaskUnfoldTransitionMerger;  import com.android.wm.shell.bubbles.bar.BubbleBarDragListener;  import com.android.wm.shell.bubbles.storage.BubblePersistentRepository;  import com.android.wm.shell.common.DisplayController; @@ -244,6 +245,13 @@ public abstract class WMShellModule {                  context, logger, positioner, educationController, mainExecutor, bgExecutor);      } +    @WMSingleton +    @Provides +    static Optional<BubbleTaskUnfoldTransitionMerger> provideBubbleTaskUnfoldTransitionMerger( +            Optional<BubbleController> bubbleController) { +        return bubbleController.map(controller -> controller); +    } +      // Note: Handler needed for LauncherApps.register      @WMSingleton      @Provides @@ -705,7 +713,8 @@ public abstract class WMShellModule {              Transitions transitions,              @ShellMainThread ShellExecutor executor,              @ShellMainThread Handler handler, -            ShellInit shellInit) { +            ShellInit shellInit, +            Optional<BubbleTaskUnfoldTransitionMerger> bubbleTaskUnfoldTransitionMerger) {          return new UnfoldTransitionHandler(                  shellInit,                  progressProvider.get(), @@ -714,7 +723,8 @@ public abstract class WMShellModule {                  transactionPool,                  executor,                  handler, -                transitions); +                transitions, +                bubbleTaskUnfoldTransitionMerger);      }      @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index 3b98f8123b46..25737c4950d6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -89,6 +89,15 @@ class DesktopDisplayEventHandler(          // TODO: b/362720497 - move desks in closing display to the remaining desk.      } +    override fun onDesktopModeEligibleChanged(displayId: Int) { +        if ( +            DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue && +                displayId != DEFAULT_DISPLAY +        ) { +            desktopDisplayModeController.refreshDisplayWindowingMode() +        } +    } +      override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) {          val remainingDesks = desktopRepository.getNumberOfDesks(lastDisplayId)          if (remainingDesks == 0) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index 3c44fe8061aa..55179511af6e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -22,7 +22,6 @@ import android.app.ActivityManager.RunningTaskInfo  import android.app.TaskInfo  import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK  import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK  import android.content.pm.ActivityInfo.LAUNCH_MULTIPLE  import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE  import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK @@ -303,21 +302,19 @@ fun getInheritedExistingTaskBounds(          // Top task is an instance of launching activity. Activity will be launching in a new          // task with the existing task also being closed. Inherit existing task bounds to          // prevent new task jumping. -        (isLaunchingNewTask(launchMode, intentFlags) && isClosingExitingInstance(intentFlags)) -> +        (isLaunchingNewSingleTask(launchMode) && isClosingExitingInstance(intentFlags)) ->              lastTask.configuration.windowConfiguration.bounds          else -> null      }  }  /** - * Returns true if the launch mode or intent will result in a new task being created for the - * activity. + * Returns true if the launch mode will result in a single new task being created for the activity.   */ -private fun isLaunchingNewTask(launchMode: Int, intentFlags: Int) = +private fun isLaunchingNewSingleTask(launchMode: Int) =      launchMode == LAUNCH_SINGLE_TASK ||          launchMode == LAUNCH_SINGLE_INSTANCE || -        launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK || -        (intentFlags and FLAG_ACTIVITY_NEW_TASK) != 0 +        launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK  /**   * Returns true if the intent will result in an existing task instance being closed if a new one diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 1e5514407ee5..b60fd5bb6c73 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -449,6 +449,11 @@ class DesktopTasksController(              return false          } +        // Secondary displays are always desktop-first +        if (displayId != DEFAULT_DISPLAY) { +            return true +        } +          val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)          // A non-organized display (e.g., non-trusted virtual displays used in CTS) doesn't have          // TDA. @@ -1140,6 +1145,7 @@ class DesktopTasksController(          }          val t =              if (remoteTransition == null) { +                logV("startLaunchTransition -- no remoteTransition -- wct = $launchTransaction")                  desktopMixedTransitionHandler.startLaunchTransition(                      transitionType = transitionType,                      wct = launchTransaction, @@ -2793,11 +2799,14 @@ class DesktopTasksController(          taskInfo: RunningTaskInfo,          deskId: Int?,      ): RunOnTransitStart? { -        // This windowing mode is to get the transition animation started; once we complete -        // split select, we will change windowing mode to undefined and inherit from split stage. -        // Going to undefined here causes task to flicker to the top left. -        // Cancelling the split select flow will revert it to fullscreen. -        wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) +        if (!DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue) { +            // This windowing mode is to get the transition animation started; once we complete +            // split select, we will change windowing mode to undefined and inherit from split +            // stage. +            // Going to undefined here causes task to flicker to the top left. +            // Cancelling the split select flow will revert it to fullscreen. +            wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) +        }          // The task's density may have been overridden in freeform; revert it here as we don't          // want it overridden in multi-window.          wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index e7492f17835a..2476ee1a4090 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -174,7 +174,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs              SurfaceControl.Transaction finishT) {          mTaskChangeListener.ifPresent(listener -> listener.onTaskChanging(change.getTaskInfo()));          mWindowDecorViewModel.onTaskChanging( -                change.getTaskInfo(), change.getLeash(), startT, finishT, change.getMode()); +                change.getTaskInfo(), change.getLeash(), startT, finishT);      }      private void onToFrontTransitionReady( @@ -184,7 +184,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs          mTaskChangeListener.ifPresent(                  listener -> listener.onTaskMovingToFront(change.getTaskInfo()));          mWindowDecorViewModel.onTaskChanging( -                change.getTaskInfo(), change.getLeash(), startT, finishT, change.getMode()); +                change.getTaskInfo(), change.getLeash(), startT, finishT);      }      private void onToBackTransitionReady( @@ -194,7 +194,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs          mTaskChangeListener.ifPresent(                  listener -> listener.onTaskMovingToBack(change.getTaskInfo()));          mWindowDecorViewModel.onTaskChanging( -                change.getTaskInfo(), change.getLeash(), startT, finishT, change.getMode()); +                change.getTaskInfo(), change.getLeash(), startT, finishT);      }      @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 014c810d1e5f..10db5ca03637 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -124,7 +124,6 @@ import android.view.SurfaceControl;  import android.view.WindowManager;  import android.widget.Toast;  import android.window.DesktopExperienceFlags; -import android.window.DesktopModeFlags;  import android.window.DisplayAreaInfo;  import android.window.RemoteTransition;  import android.window.TransitionInfo; @@ -684,8 +683,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,          if (!enteredSplitSelect) {              return null;          } -        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue() -                && !DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue()) { +        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) {              mTaskOrganizer.applyTransaction(wct);              return null;          } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index a6f872634ee9..22848c38bb1c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -728,6 +728,48 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV          taskView.notifyAppeared(newTask);      } +    /** +     * Updates bounds for the task view during an unfold transition. +     * +     * @return true if the task was found and a transition for this task is pending. false +     * otherwise. +     */ +    public boolean updateBoundsForUnfold(Rect bounds, SurfaceControl.Transaction startTransaction, +            SurfaceControl.Transaction finishTransaction, +            ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { +        final TaskViewTaskController taskView = findTaskView(taskInfo); +        if (taskView == null) { +            return false; +        } + +        final PendingTransition pendingTransition = findPending(taskView, TRANSIT_CHANGE); +        if (pendingTransition == null) { +            return false; +        } + +        mPending.remove(pendingTransition); + +        // reparent the task under the task view surface and set the bounds on it +        startTransaction.reparent(leash, taskView.getSurfaceControl()) +                .setPosition(leash, 0, 0) +                .setWindowCrop(leash, bounds.width(), bounds.height()) +                .show(leash); +        // the finish transaction would reparent the task back to the transition root, so reparent +        // it again to the task view surface +        finishTransaction.reparent(leash, taskView.getSurfaceControl()) +                .setPosition(leash, 0, 0) +                .setWindowCrop(leash, bounds.width(), bounds.height()); +        if (useRepo()) { +            final TaskViewRepository.TaskViewState state = mTaskViewRepo.byTaskView(taskView); +            if (state != null) { +                state.mBounds.set(bounds); +            } +        } else { +            updateBoundsState(taskView, bounds); +        } +        return true; +    } +      private void updateBounds(TaskViewTaskController taskView, Rect boundsOnScreen,              SurfaceControl.Transaction startTransaction,              SurfaceControl.Transaction finishTransaction, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java index 7fd19a7d2a88..706a366441cd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java @@ -38,6 +38,7 @@ import androidx.annotation.Nullable;  import androidx.annotation.VisibleForTesting;  import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.bubbles.BubbleTaskUnfoldTransitionMerger;  import com.android.wm.shell.shared.TransactionPool;  import com.android.wm.shell.shared.TransitionUtil;  import com.android.wm.shell.sysui.ShellInit; @@ -53,6 +54,7 @@ import java.lang.annotation.Retention;  import java.lang.annotation.RetentionPolicy;  import java.util.ArrayList;  import java.util.List; +import java.util.Optional;  import java.util.concurrent.Executor;  /** @@ -80,6 +82,7 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene      private final ShellUnfoldProgressProvider mUnfoldProgressProvider;      private final Transitions mTransitions; +    private final Optional<BubbleTaskUnfoldTransitionMerger> mBubbleTaskUnfoldTransitionMerger;      private final Executor mExecutor;      private final TransactionPool mTransactionPool;      private final Handler mHandler; @@ -108,12 +111,14 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene              TransactionPool transactionPool,              Executor executor,              Handler handler, -            Transitions transitions) { +            Transitions transitions, +            Optional<BubbleTaskUnfoldTransitionMerger> bubbleTaskUnfoldTransitionMerger) {          mUnfoldProgressProvider = unfoldProgressProvider;          mTransitions = transitions;          mTransactionPool = transactionPool;          mExecutor = executor;          mHandler = handler; +        mBubbleTaskUnfoldTransitionMerger = bubbleTaskUnfoldTransitionMerger;          mAnimators.add(splitUnfoldTaskAnimator);          mAnimators.add(fullscreenUnfoldAnimator); @@ -237,14 +242,26 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene          }          // TODO (b/286928742) unfold transition handler should be part of mixed handler to          //  handle merges better. +          for (int i = 0; i < info.getChanges().size(); ++i) {              final TransitionInfo.Change change = info.getChanges().get(i);              final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();              if (taskInfo != null                      && taskInfo.configuration.windowConfiguration.isAlwaysOnTop()) { -                // Tasks that are always on top (e.g. bubbles), will handle their own transition -                // as they are on top of everything else. So skip merging transitions here. -                return; +                // Tasks that are always on top, excluding bubbles, will handle their own transition +                // as they are on top of everything else. If this is a transition for a bubble task, +                // attempt to merge it. Otherwise skip merging transitions. +                if (mBubbleTaskUnfoldTransitionMerger.isPresent()) { +                    boolean merged = +                            mBubbleTaskUnfoldTransitionMerger +                                    .get() +                                    .mergeTaskWithUnfold(taskInfo, change, startT, finishT); +                    if (!merged) { +                        return; +                    } +                } else { +                    return; +                }              }          }          // Apply changes happening during the unfold animation immediately diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 42321e56e72b..7871179a50de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -49,7 +49,6 @@ import android.view.SurfaceControl;  import android.view.View;  import android.view.ViewConfiguration;  import android.window.DisplayAreaInfo; -import android.window.TransitionInfo;  import android.window.WindowContainerToken;  import android.window.WindowContainerTransaction; @@ -234,8 +233,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT              RunningTaskInfo taskInfo,              SurfaceControl taskSurface,              SurfaceControl.Transaction startT, -            SurfaceControl.Transaction finishT, -            @TransitionInfo.TransitionMode int changeMode) { +            SurfaceControl.Transaction finishT) {          final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);          if (!shouldShowWindowDecor(taskInfo)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java index 4511fbe10764..2b2cdf84005c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java @@ -31,7 +31,6 @@ import android.view.KeyCharacterMap;  import android.view.KeyEvent;  import android.view.SurfaceControl;  import android.view.View; -import android.window.TransitionInfo;  import android.window.WindowContainerToken;  import android.window.WindowContainerTransaction; @@ -160,8 +159,7 @@ public abstract class CarWindowDecorViewModel              RunningTaskInfo taskInfo,              SurfaceControl taskSurface,              SurfaceControl.Transaction startT, -            SurfaceControl.Transaction finishT, -            @TransitionInfo.TransitionMode int changeMode) { +            SurfaceControl.Transaction finishT) {          final CarWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);          if (!shouldShowWindowDecor(taskInfo)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 69e1f36dec0b..0082d7971ad2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -27,7 +27,6 @@ import static android.view.MotionEvent.ACTION_HOVER_EXIT;  import static android.view.MotionEvent.ACTION_MOVE;  import static android.view.MotionEvent.ACTION_UP;  import static android.view.WindowInsets.Type.statusBars; -import static android.view.WindowManager.TRANSIT_TO_BACK;  import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU;  import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; @@ -80,7 +79,6 @@ import android.view.ViewConfiguration;  import android.view.ViewRootImpl;  import android.window.DesktopModeFlags;  import android.window.TaskSnapshot; -import android.window.TransitionInfo;  import android.window.WindowContainerToken;  import android.window.WindowContainerTransaction; @@ -602,8 +600,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,              RunningTaskInfo taskInfo,              SurfaceControl taskSurface,              SurfaceControl.Transaction startT, -            SurfaceControl.Transaction finishT, -            @TransitionInfo.TransitionMode int changeMode) { +            SurfaceControl.Transaction finishT) {          final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);          if (!shouldShowWindowDecor(taskInfo)) {              if (decoration != null) { @@ -617,8 +614,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,          } else {              decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,                      false /* shouldSetTaskPositionAndCrop */, -                    mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion, -                    /*isMovingToBack= */ changeMode == TRANSIT_TO_BACK); +                    mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion);          }      } @@ -633,7 +629,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,          decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,                  false /* shouldSetTaskPositionAndCrop */,                  mFocusTransitionObserver.hasGlobalFocus(taskInfo), -                mExclusionRegion, /* isMovingToBack= */ false); +                mExclusionRegion);      }      @Override @@ -1892,7 +1888,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,          windowDecoration.relayout(taskInfo, startT, finishT,                  false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */,                  mFocusTransitionObserver.hasGlobalFocus(taskInfo), -                mExclusionRegion, /* isMovingToBack= */ false); +                mExclusionRegion);          if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) {              incrementEventReceiverTasks(taskInfo.displayId);          } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 50bc7b5e865b..d24308137936 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -217,7 +217,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin      private boolean mIsDragging = false;      private Runnable mLoadAppInfoRunnable;      private Runnable mSetAppInfoRunnable; -    private boolean mIsMovingToBack;      public DesktopModeWindowDecoration(              Context context, @@ -479,7 +478,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin          // causes flickering. See b/270202228.          final boolean applyTransactionOnDraw = taskInfo.isFreeform();          relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, -                hasGlobalFocus, displayExclusionRegion, mIsMovingToBack); +                hasGlobalFocus, displayExclusionRegion);          if (!applyTransactionOnDraw) {              t.apply();          } @@ -506,8 +505,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin      void relayout(ActivityManager.RunningTaskInfo taskInfo,              SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,              boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, -            boolean hasGlobalFocus, @NonNull Region displayExclusionRegion, -            boolean isMovingToBack) { +            boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) {          Trace.beginSection("DesktopModeWindowDecoration#relayout");          if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_APP_TO_WEB.isTrue()) { @@ -530,7 +528,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin          final boolean inFullImmersive = mDesktopUserRepositories.getProfile(taskInfo.userId)                  .isTaskInFullImmersiveState(taskInfo.taskId); -        mIsMovingToBack = isMovingToBack;          updateRelayoutParams(mRelayoutParams, mContext, taskInfo, mSplitScreenController,                  applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop,                  mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive, @@ -539,8 +536,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin                  /* shouldIgnoreCornerRadius= */ mIsRecentsTransitionRunning                          && DesktopModeFlags                          .ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue(), -                mDesktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(taskInfo), -                mIsRecentsTransitionRunning, mIsMovingToBack); +                mDesktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(taskInfo));          final WindowDecorLinearLayout oldRootView = mResult.mRootView;          final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; @@ -633,6 +629,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin          mBgExecutor.execute(mLoadAppInfoRunnable);      } +    private boolean showInputLayer() { +        if (!DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue()) { +            return isCaptionVisible(); +        } +        // Don't show the input layer during the recents transition, otherwise it could become +        // touchable while in overview, during quick-switch or even for a short moment after going +        // Home. +        return isCaptionVisible() && !mIsRecentsTransitionRunning; +    } +      private boolean isCaptionVisible() {          return mTaskInfo.isVisible && mIsCaptionVisible;      } @@ -874,7 +880,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin          if (!isAppHandle(mWindowDecorViewHolder)) return;          asAppHandle(mWindowDecorViewHolder).bindData(new AppHandleViewHolder.HandleData(                  mTaskInfo, determineHandlePosition(), mResult.mCaptionWidth, -                mResult.mCaptionHeight, /* showInputLayer= */ isCaptionVisible(), +                mResult.mCaptionHeight, /* showInputLayer= */ showInputLayer(),                  /* isCaptionVisible= */ isCaptionVisible()          ));      } @@ -959,9 +965,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin              boolean hasGlobalFocus,              @NonNull Region displayExclusionRegion,              boolean shouldIgnoreCornerRadius, -            boolean shouldExcludeCaptionFromAppBounds, -            boolean isRecentsTransitionRunning, -            boolean isMovingToBack) { +            boolean shouldExcludeCaptionFromAppBounds) {          final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());          final boolean isAppHeader =                  captionLayoutId == R.layout.desktop_mode_app_header; @@ -979,19 +983,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin          relayoutParams.mAsyncViewHost = isAppHandle;          boolean showCaption; -        // If this relayout is occurring from an observed TRANSIT_TO_BACK transition, do not -        // show caption (this includes split select transition). -        if (DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue() -                && isMovingToBack && !isDragging) { -            showCaption = false; -        } else if (DesktopModeFlags.ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX.isTrue() && isDragging) { +        if (DesktopModeFlags.ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX.isTrue() && isDragging) {              // If the task is being dragged, the caption should not be hidden so that it continues              // receiving input              showCaption = true; -        } else if (DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue() -                && isRecentsTransitionRunning) { -            // Caption should not be visible in recents. -            showCaption = false;          } else if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) {              if (inFullImmersiveMode) {                  showCaption = (isStatusBarVisible && !isKeyguardVisibleAndOccluded); @@ -1895,18 +1890,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin       * <p> When a Recents transition is active we allow that transition to take ownership of the       * corner radius of its task surfaces, so each window decoration should stop updating the corner       * radius of its task surface during that time. -     * -     * We should not allow input to reach the input layer during a Recents transition, so -     * update the handle view holder accordingly if transition status changes.       */      void setIsRecentsTransitionRunning(boolean isRecentsTransitionRunning) { -        if (mIsRecentsTransitionRunning != isRecentsTransitionRunning) { -            mIsRecentsTransitionRunning = isRecentsTransitionRunning; -            if (DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue()) { -                // We don't relayout decor on recents transition, so we need to call it directly. -                relayout(mTaskInfo, mHasGlobalFocus, mRelayoutParams.mDisplayExclusionRegion); -            } -        } +        mIsRecentsTransitionRunning = isRecentsTransitionRunning;      }      /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java index 5e4a0a5860f0..1563259f4a1a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java @@ -18,7 +18,6 @@ package com.android.wm.shell.windowdecor;  import android.app.ActivityManager;  import android.view.SurfaceControl; -import android.window.TransitionInfo;  import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;  import com.android.wm.shell.splitscreen.SplitScreenController; @@ -84,14 +83,12 @@ public interface WindowDecorViewModel {       * @param taskSurface the surface of the task       * @param startT      the start transaction to be applied before the transition       * @param finishT     the finish transaction to restore states after the transition -     * @param changeMode  the type of change to the task       */      void onTaskChanging(              ActivityManager.RunningTaskInfo taskInfo,              SurfaceControl taskSurface,              SurfaceControl.Transaction startT, -            SurfaceControl.Transaction finishT, -            @TransitionInfo.TransitionMode int changeMode); +            SurfaceControl.Transaction finishT);      /**       * Notifies that the given task is about to close to give the window decoration a chance to diff --git a/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/SimulatedConnectedDisplayTestRule.kt b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/SimulatedConnectedDisplayTestRule.kt index 68f7ef09ee70..f9b69d3f5f7e 100644 --- a/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/SimulatedConnectedDisplayTestRule.kt +++ b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/SimulatedConnectedDisplayTestRule.kt @@ -41,7 +41,6 @@ import org.junit.runners.model.Statement  class SimulatedConnectedDisplayTestRule : TestRule {      private val context = InstrumentationRegistry.getInstrumentation().targetContext -    private val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation      private val displayManager = context.getSystemService(DisplayManager::class.java)      private val addedDisplays = mutableListOf<Int>() @@ -102,7 +101,8 @@ class SimulatedConnectedDisplayTestRule : TestRule {          // Add the overlay displays          Settings.Global.putString(              InstrumentationRegistry.getInstrumentation().context.contentResolver, -            Settings.Global.OVERLAY_DISPLAY_DEVICES, displaySettings +            Settings.Global.OVERLAY_DISPLAY_DEVICES, +            displaySettings          )          withTimeoutOrNull(TIMEOUT) {              displayAddedFlow.take(displays.size).collect { displayId -> @@ -125,10 +125,6 @@ class SimulatedConnectedDisplayTestRule : TestRule {      }      private fun cleanupTestDisplays() = runBlocking { -        if (addedDisplays.isEmpty()) { -            return@runBlocking -        } -          val displayRemovedFlow: Flow<Int> = callbackFlow {              val listener = object : DisplayListener {                  override fun onDisplayAdded(displayId: Int) {} @@ -146,16 +142,24 @@ class SimulatedConnectedDisplayTestRule : TestRule {              }          } -        // Remove overlay displays +        // Remove overlay displays. We'll execute this regardless of addedDisplays just to +        // ensure all overlay displays are removed before and after the test. +        // Note: If we want to restore the original overlay display added before this test (and its +        // topology), it will be complicated as re-adding overlay display would lead to different +        // displayId and topology could not be restored easily.          Settings.Global.putString(              InstrumentationRegistry.getInstrumentation().context.contentResolver, -            Settings.Global.OVERLAY_DISPLAY_DEVICES, null) +            Settings.Global.OVERLAY_DISPLAY_DEVICES, +            null +        ) -        withTimeoutOrNull(TIMEOUT) { -            displayRemovedFlow.take(addedDisplays.size).collect { displayId -> -                addedDisplays.remove(displayId) -            } -        } ?: error("Timed out waiting for displays to be removed.") +        if (!addedDisplays.isEmpty()) { +            withTimeoutOrNull(TIMEOUT) { +                displayRemovedFlow.take(addedDisplays.size).collect { displayId -> +                    addedDisplays.remove(displayId) +                } +            } ?: error("Timed out waiting for displays to be removed: $addedDisplays") +        }      }      private companion object { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt index 9268db60aa51..85a431be8e8b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest  import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession  import com.android.dx.mockito.inline.extended.ExtendedMockito.never  import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.server.display.feature.flags.Flags as DisplayFlags  import com.android.window.flags.Flags  import com.android.wm.shell.RootTaskDisplayAreaOrganizer  import com.android.wm.shell.ShellTestCase @@ -246,6 +247,13 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {          verify(desktopDisplayModeController).refreshDisplayWindowingMode()      } +    @Test +    @EnableFlags(DisplayFlags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) +    fun testDesktopModeEligibleChanged() { +        onDisplaysChangedListenerCaptor.lastValue.onDesktopModeEligibleChanged(externalDisplayId) +        verify(desktopDisplayModeController).refreshDisplayWindowingMode() +    } +      private class FakeDesktopRepositoryInitializer : DesktopRepositoryInitializer {          override var deskRecreationFactory: DesktopRepositoryInitializer.DeskRecreationFactory =              DesktopRepositoryInitializer.DeskRecreationFactory { _, _, deskId -> deskId } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index d495f76e7814..b8c2273e1465 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -32,9 +32,9 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED  import android.content.ComponentName  import android.content.Context  import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK  import android.content.pm.ActivityInfo  import android.content.pm.ActivityInfo.CONFIG_DENSITY +import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE  import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE  import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT  import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED @@ -1200,9 +1200,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()          existingTask.topActivity = testComponent          existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500))          // Set up new instance of already existing task. -        val launchingTask = setUpFullscreenTask() +        val launchingTask = +            setUpFullscreenTask().apply { +                topActivityInfo = ActivityInfo().apply { launchMode = LAUNCH_SINGLE_INSTANCE } +            }          launchingTask.topActivity = testComponent -        launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)          // Move new instance to desktop. By default multi instance is not supported so first          // instance will close. @@ -1224,10 +1226,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()          existingTask.topActivity = testComponent          existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500))          // Set up new instance of already existing task. -        val launchingTask = setUpFreeformTask(active = false) +        val launchingTask = +            setUpFreeformTask(active = false).apply { +                topActivityInfo = ActivityInfo().apply { launchMode = LAUNCH_SINGLE_INSTANCE } +            }          taskRepository.removeTask(launchingTask.displayId, launchingTask.taskId)          launchingTask.topActivity = testComponent -        launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)          // Move new instance to desktop. By default multi instance is not supported so first          // instance will close. @@ -4283,6 +4287,25 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()      }      @Test +    @EnableFlags( +        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, +        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, +    ) +    fun handleRequest_fullscreenTask_noInDesk_enforceDesktop_secondaryDisplay_movesToDesk() { +        val deskId = 5 +        taskRepository.addDesk(displayId = SECONDARY_DISPLAY_ID, deskId = deskId) +        taskRepository.setDeskInactive(deskId) +        whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) +        whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + +        val fullscreenTask = createFullscreenTask(displayId = SECONDARY_DISPLAY_ID) +        val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + +        assertNotNull(wct, "should handle request") +        verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask) +    } + +    @Test      fun handleRequest_fullscreenTask_notInDesk_enforceDesktop_fullscreenDisplay_returnNull() {          taskRepository.setDeskInactive(deskId = 0)          whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index f6e49853eddf..82373ff1bc41 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -571,8 +571,7 @@ public class StageCoordinatorTests extends ShellTestCase {      }      @Test -    @DisableFlags({Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, -            Flags.FLAG_ENABLE_INPUT_LAYER_TRANSITION_FIX}) +    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)      public void testRequestEnterSplit_didNotEnterSplitSelect_doesNotApplyTransaction() {          final WindowContainerTransaction wct = new WindowContainerTransaction();          mStageCoordinator.registerSplitSelectListener( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java index 3a455ba6b5df..f11839ad4e72 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java @@ -24,8 +24,12 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT;  import static com.google.common.truth.Truth.assertThat;  import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt;  import static org.mockito.Mockito.doReturn;  import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never;  import static org.mockito.Mockito.spy;  import static org.mockito.Mockito.verify;  import static org.mockito.Mockito.when; @@ -77,16 +81,15 @@ public class TaskViewTransitionsTest extends ShellTestCase {      @Mock      TaskViewTaskController mTaskViewTaskController;      @Mock -    ActivityManager.RunningTaskInfo mTaskInfo; -    @Mock      WindowContainerToken mToken;      @Mock      ShellTaskOrganizer mOrganizer;      @Mock      SyncTransactionQueue mSyncQueue; -    Executor mExecutor = command -> command.run(); +    Executor mExecutor = Runnable::run; +    ActivityManager.RunningTaskInfo mTaskInfo;      TaskViewRepository mTaskViewRepository;      TaskViewTransitions mTaskViewTransitions; @@ -305,4 +308,66 @@ public class TaskViewTransitionsTest extends ShellTestCase {          verify(mTaskViewTaskController).setTaskNotFound();      } + +    @Test +    public void updateBoundsForUnfold_taskNotFound_doesNothing() { +        assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + +        ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); +        taskInfo.token = mock(WindowContainerToken.class); +        taskInfo.taskId = 666; +        Rect bounds = new Rect(100, 50, 200, 250); +        SurfaceControl.Transaction startTransaction = mock(SurfaceControl.Transaction.class); +        SurfaceControl.Transaction finishTransaction = mock(SurfaceControl.Transaction.class); +        assertThat( +                mTaskViewTransitions.updateBoundsForUnfold(bounds, startTransaction, +                        finishTransaction, taskInfo, mock(SurfaceControl.class))) +                .isFalse(); + +        verify(startTransaction, never()).reparent(any(), any()); +    } + +    @Test +    public void updateBoundsForUnfold_noPendingTransition_doesNothing() { +        assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + +        Rect bounds = new Rect(100, 50, 200, 250); +        mTaskViewTransitions.setTaskBounds(mTaskViewTaskController, bounds); +        assertThat(mTaskViewTransitions.hasPending()).isFalse(); + +        SurfaceControl.Transaction startTransaction = mock(SurfaceControl.Transaction.class); +        SurfaceControl.Transaction finishTransaction = mock(SurfaceControl.Transaction.class); +        assertThat( +                mTaskViewTransitions.updateBoundsForUnfold(bounds, startTransaction, +                        finishTransaction, mTaskInfo, mock(SurfaceControl.class))) +                .isFalse(); +        verify(startTransaction, never()).reparent(any(), any()); +    } + +    @Test +    public void updateBoundsForUnfold() { +        assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + +        Rect bounds = new Rect(100, 50, 200, 250); +        mTaskViewTransitions.updateVisibilityState(mTaskViewTaskController, /* visible= */ true); +        mTaskViewTransitions.setTaskBounds(mTaskViewTaskController, bounds); +        assertThat(mTaskViewTransitions.hasPending()).isTrue(); + +        SurfaceControl.Transaction startTransaction = createMockTransaction(); +        SurfaceControl.Transaction finishTransaction = createMockTransaction(); +        assertThat( +                mTaskViewTransitions.updateBoundsForUnfold(bounds, startTransaction, +                        finishTransaction, mTaskInfo, mock(SurfaceControl.class))) +                .isTrue(); +        assertThat(mTaskViewRepository.byTaskView(mTaskViewTaskController).mBounds) +                .isEqualTo(bounds); +    } + +    private SurfaceControl.Transaction createMockTransaction() { +        SurfaceControl.Transaction transaction = mock(SurfaceControl.Transaction.class); +        when(transaction.reparent(any(), any())).thenReturn(transaction); +        when(transaction.setPosition(any(), anyFloat(), anyFloat())).thenReturn(transaction); +        when(transaction.setWindowCrop(any(), anyInt(), anyInt())).thenReturn(transaction); +        return transaction; +    }  } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java index aad18cba4436..e28d0acb579f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java @@ -60,6 +60,7 @@ import org.mockito.InOrder;  import java.util.ArrayList;  import java.util.List; +import java.util.Optional;  import java.util.concurrent.Executor;  public class UnfoldTransitionHandlerTest extends ShellTestCase { @@ -98,7 +99,8 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {                  mTransactionPool,                  executor,                  mHandler, -                mTransitions +                mTransitions, +                /* bubbleTaskUnfoldTransitionMerger= */ Optional.empty()          );          shellInit.init(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt index b1f92411c5a3..067dcec5d65d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt @@ -28,7 +28,6 @@ import android.testing.TestableLooper.RunWithLooper  import android.view.Display  import android.view.Display.DEFAULT_DISPLAY  import android.view.SurfaceControl -import android.view.WindowManager.TRANSIT_CHANGE  import androidx.test.filters.SmallTest  import com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean  import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn @@ -110,7 +109,7 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest :          onTaskOpening(task, taskSurface)          assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))          task.setActivityType(ACTIVITY_TYPE_UNDEFINED) -        onTaskChanging(task, taskSurface, TRANSIT_CHANGE) +        onTaskChanging(task, taskSurface)          assertFalse(windowDecorByTaskIdSpy.contains(task.taskId))          verify(decoration).close() @@ -166,7 +165,7 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest :          setLargeScreen(false)          setUpMockDecorationForTask(task) -        onTaskChanging(task, taskSurface, TRANSIT_CHANGE) +        onTaskChanging(task, taskSurface)          assertFalse(windowDecorByTaskIdSpy.contains(task.taskId))      } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index ad3426e82805..40aa41b2b72a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -51,7 +51,6 @@ import android.view.SurfaceView  import android.view.View  import android.view.ViewRootImpl  import android.view.WindowInsets.Type.statusBars -import android.view.WindowManager.TRANSIT_CHANGE  import android.window.WindowContainerTransaction  import android.window.WindowContainerTransaction.HierarchyOp  import androidx.test.filters.SmallTest @@ -135,7 +134,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest          task.setWindowingMode(WINDOWING_MODE_UNDEFINED)          task.setActivityType(ACTIVITY_TYPE_UNDEFINED) -        onTaskChanging(task, taskSurface, TRANSIT_CHANGE) +        onTaskChanging(task, taskSurface)          assertFalse(windowDecorByTaskIdSpy.contains(task.taskId))          verify(decoration).close() @@ -150,12 +149,12 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest          val taskSurface = SurfaceControl()          setUpMockDecorationForTask(task) -        onTaskChanging(task, taskSurface, TRANSIT_CHANGE) +        onTaskChanging(task, taskSurface)          assertFalse(windowDecorByTaskIdSpy.contains(task.taskId))          task.setWindowingMode(WINDOWING_MODE_FREEFORM)          task.setActivityType(ACTIVITY_TYPE_STANDARD) -        onTaskChanging(task, taskSurface, TRANSIT_CHANGE) +        onTaskChanging(task, taskSurface)          assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))      } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index 2126d1d9b986..80dcd7d69f00 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -362,14 +362,12 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {      protected fun onTaskChanging(          task: RunningTaskInfo,          leash: SurfaceControl = SurfaceControl(), -        changeMode: Int      ) {          desktopModeWindowDecorViewModel.onTaskChanging(              task,              leash,              StubTransaction(),              StubTransaction(), -            changeMode          )      } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 0908f56e1cfb..a0171ea04da3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -174,8 +174,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {      private static final boolean DEFAULT_HAS_GLOBAL_FOCUS = true;      private static final boolean DEFAULT_SHOULD_IGNORE_CORNER_RADIUS = false;      private static final boolean DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS = false; -    private static final boolean DEFAULT_IS_RECENTS_TRANSITION_RUNNING = false; -    private static final boolean DEFAULT_IS_MOVING_TO_BACK = false;      @Mock @@ -443,9 +441,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  /* shouldIgnoreCornerRadius= */ true, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          assertThat(relayoutParams.mCornerRadius).isEqualTo(INVALID_CORNER_RADIUS);      } @@ -544,9 +540,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  /* shouldIgnoreCornerRadius= */ true, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          assertThat(relayoutParams.mCornerRadiusId).isEqualTo(Resources.ID_NULL);      } @@ -748,9 +742,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                /* shouldExcludeCaptionFromAppBounds */ true, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                /* shouldExcludeCaptionFromAppBounds */ true);          // Force consuming flags are disabled.          assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) == 0).isTrue(); @@ -785,9 +777,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) != 0).isTrue();          assertThat( @@ -866,9 +856,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          // Takes status bar inset as padding, ignores caption bar inset.          assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50); @@ -896,9 +884,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          assertThat(relayoutParams.mIsInsetSource).isFalse();      } @@ -925,9 +911,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          // Header is always shown because it's assumed the status bar is always visible.          assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -954,9 +938,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          assertThat(relayoutParams.mIsCaptionVisible).isTrue();      } @@ -982,9 +964,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          assertThat(relayoutParams.mIsCaptionVisible).isFalse();      } @@ -1010,9 +990,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          assertThat(relayoutParams.mIsCaptionVisible).isFalse();      } @@ -1039,9 +1017,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -1060,9 +1036,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          assertThat(relayoutParams.mIsCaptionVisible).isFalse();      } @@ -1089,9 +1063,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          assertThat(relayoutParams.mIsCaptionVisible).isTrue();      } @@ -1118,9 +1090,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);          assertThat(relayoutParams.mIsCaptionVisible).isFalse();      } @@ -1151,65 +1121,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {          assertThat(relayoutParams.mAsyncViewHost).isFalse();      } - -    @Test -    @EnableFlags(Flags.FLAG_ENABLE_INPUT_LAYER_TRANSITION_FIX) -    public void updateRelayoutParams_handle_movingToBack_captionNotVisible() { -        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); -        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); -        final RelayoutParams relayoutParams = new RelayoutParams(); - -        DesktopModeWindowDecoration.updateRelayoutParams( -                relayoutParams, -                mTestableContext, -                taskInfo, -                mMockSplitScreenController, -                DEFAULT_APPLY_START_TRANSACTION_ON_DRAW, -                DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP, -                DEFAULT_IS_STATUSBAR_VISIBLE, -                DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, -                DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, -                DEFAULT_IS_DRAGGING, -                new InsetsState(), -                DEFAULT_HAS_GLOBAL_FOCUS, -                mExclusionRegion, -                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                /* isMovingToBack= */ true); - -        assertThat(relayoutParams.mIsCaptionVisible).isFalse(); -    } - -    @Test -    @EnableFlags(Flags.FLAG_ENABLE_INPUT_LAYER_TRANSITION_FIX) -    public void updateRelayoutParams_handle_inRecentsTransition_captionNotVisible() { -        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); -        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); -        final RelayoutParams relayoutParams = new RelayoutParams(); - -        DesktopModeWindowDecoration.updateRelayoutParams( -                relayoutParams, -                mTestableContext, -                taskInfo, -                mMockSplitScreenController, -                DEFAULT_APPLY_START_TRANSACTION_ON_DRAW, -                DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP, -                DEFAULT_IS_STATUSBAR_VISIBLE, -                DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, -                DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, -                DEFAULT_IS_DRAGGING, -                new InsetsState(), -                DEFAULT_HAS_GLOBAL_FOCUS, -                mExclusionRegion, -                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                /* isRecentsTransitionRunning= */ true, -                DEFAULT_IS_MOVING_TO_BACK); - -        assertThat(relayoutParams.mIsCaptionVisible).isFalse(); -    } -      @Test      public void relayout_fullscreenTask_appliesTransactionImmediately() {          final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); @@ -1846,9 +1757,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {                  DEFAULT_HAS_GLOBAL_FOCUS,                  mExclusionRegion,                  DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, -                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, -                DEFAULT_IS_RECENTS_TRANSITION_RUNNING, -                DEFAULT_IS_MOVING_TO_BACK); +                DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);      }      private DesktopModeWindowDecoration createWindowDecoration( diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp index 48621e4e2094..0b02d3cb4250 100644 --- a/media/tests/projection/Android.bp +++ b/media/tests/projection/Android.bp @@ -3,7 +3,7 @@  //########################################################################  package { -    default_team: "trendy_team_lse_desktop_os_experience", +    default_team: "trendy_team_media_projection",      // See: http://go/android-license-faq      // A large-scale-change added 'default_applicable_licenses' to import      // all of the 'license_kinds' from "frameworks_base_license" diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java index 964268e4ad14..518757dd0d5c 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java @@ -359,11 +359,7 @@ public class CompanionAssociationActivity extends FragmentActivity implements                  if (CompanionDeviceDiscoveryService.getScanResult().getValue().isEmpty()) {                      // If the scan times out, do NOT close the activity automatically and let the                      // user manually cancel the flow. -                    synchronized (LOCK) { -                        if (sDiscoveryStarted) { -                            stopDiscovery(); -                        } -                    } +                    stopDiscovery();                      mTimeoutMessage.setText(getString(R.string.message_discovery_hard_timeout));                      mTimeoutMessage.setVisibility(View.VISIBLE);                  } @@ -455,8 +451,14 @@ public class CompanionAssociationActivity extends FragmentActivity implements      }      private void stopDiscovery() { -        if (mRequest != null && !mRequest.isSelfManaged()) { -            CompanionDeviceDiscoveryService.stop(this); +        if (mRequest == null || mRequest.isSelfManaged()) { +            return; +        } + +        synchronized (LOCK) { +            if (sDiscoveryStarted) { +                CompanionDeviceDiscoveryService.stop(this); +            }          }      } diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java index 50a01b3bc7c9..7b4794506adb 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java @@ -136,7 +136,12 @@ public class CompanionDeviceDiscoveryService extends Service {          intent.setAction(ACTION_START_DISCOVERY);          intent.putExtra(EXTRA_ASSOCIATION_REQUEST, associationRequest); -        context.startService(intent); +        try { +            context.startService(intent); +        } catch (IllegalStateException e) { +            Slog.e(TAG, "Failed to start discovery.", e); +            return false; +        }          return true;      } @@ -144,7 +149,12 @@ public class CompanionDeviceDiscoveryService extends Service {      static void stop(@NonNull Context context) {          final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class);          intent.setAction(ACTION_STOP_DISCOVERY); -        context.startService(intent); + +        try { +            context.startService(intent); +        } catch (IllegalStateException e) { +            Slog.e(TAG, "Failed to stop discovery.", e); +        }      }      static LiveData<List<DeviceFilterPair<?>>> getScanResult() { diff --git a/packages/SettingsLib/SettingsTheme/res/color-v31/settingslib_neutral_variant55.xml b/packages/SettingsLib/SettingsTheme/res/color-v31/settingslib_neutral_variant55.xml new file mode 100644 index 000000000000..53ffa234f432 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/color-v31/settingslib_neutral_variant55.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +    Copyright (C) 2025 The Android Open Source Project + +    Licensed under the Apache License, Version 2.0 (the "License"); +    you may not use this file except in compliance with the License. +    You may obtain a copy of the License at + +         http://www.apache.org/licenses/LICENSE-2.0 + +    Unless required by applicable law or agreed to in writing, software +    distributed under the License is distributed on an "AS IS" BASIS, +    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +    See the License for the specific language governing permissions and +    limitations under the License. +  --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> +    <item android:color="@android:color/system_neutral2_500" android:lStar="55"/> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v36/settingslib_expressive_preference_category.xml b/packages/SettingsLib/SettingsTheme/res/layout-v36/settingslib_expressive_preference_category.xml new file mode 100644 index 000000000000..44b8e7c96a88 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/layout-v36/settingslib_expressive_preference_category.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +  Copyright (C) 2025 The Android Open Source Project + +  Licensed under the Apache License, Version 2.0 (the "License"); +  you may not use this file except in compliance with the License. +  You may obtain a copy of the License at + +       http://www.apache.org/licenses/LICENSE-2.0 + +  Unless required by applicable law or agreed to in writing, software +  distributed under the License is distributed on an "AS IS" BASIS, +  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +  See the License for the specific language governing permissions and +  limitations under the License. +  --> + +<LinearLayout +    xmlns:android="http://schemas.android.com/apk/res/android" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft" +    android:paddingStart="?android:attr/listPreferredItemPaddingStart" +    android:paddingRight="?android:attr/listPreferredItemPaddingRight" +    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" +    android:background="?android:attr/selectableItemBackground" +    android:baselineAligned="false" +    android:layout_marginTop="@dimen/settingslib_expressive_space_small1" +    android:gravity="center_vertical" +    android:filterTouchesWhenObscured="false"> + +    <TextView +        android:id="@android:id/title" +        android:paddingStart="@dimen/settingslib_expressive_space_extrasmall4" +        android:paddingTop="@dimen/settingslib_expressive_space_extrasmall4" +        android:paddingBottom="@dimen/settingslib_expressive_space_extrasmall4" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:layout_gravity="start" +        android:textAlignment="viewStart" +        style="@style/PreferenceCategoryTitleTextStyle"/> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-night-v36/styles.xml b/packages/SettingsLib/SettingsTheme/res/values-night-v36/styles.xml new file mode 100644 index 000000000000..ca99d449b6b3 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/values-night-v36/styles.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +    Copyright (C) 2025 The Android Open Source Project + +    Licensed under the Apache License, Version 2.0 (the "License"); +    you may not use this file except in compliance with the License. +    You may obtain a copy of the License at + +         http://www.apache.org/licenses/LICENSE-2.0 + +    Unless required by applicable law or agreed to in writing, software +    distributed under the License is distributed on an "AS IS" BASIS, +    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +    See the License for the specific language governing permissions and +    limitations under the License. +  --> +<resources> +    <style name="Seekbar.SettingsLib" parent="@android:style/Widget.Material.SeekBar"> +        <item name="android:thumbTint">@android:color/system_accent1_100</item> +        <item name="android:progressTint">@android:color/system_accent1_100</item> +        <item name="android:progressBackgroundTint">@android:color/system_neutral2_500</item> +        <item name="android:progressBackgroundTintMode">src_over</item> +    </style> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/styles.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/styles.xml new file mode 100644 index 000000000000..a31983a04753 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/values-v36/styles.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +    Copyright (C) 2025 The Android Open Source Project + +    Licensed under the Apache License, Version 2.0 (the "License"); +    you may not use this file except in compliance with the License. +    You may obtain a copy of the License at + +         http://www.apache.org/licenses/LICENSE-2.0 + +    Unless required by applicable law or agreed to in writing, software +    distributed under the License is distributed on an "AS IS" BASIS, +    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +    See the License for the specific language governing permissions and +    limitations under the License. +  --> +<resources> +    <style name="Seekbar.SettingsLib" parent="@android:style/Widget.Material.SeekBar"> +        <item name="android:thumbTint">@android:color/system_accent1_800</item> +        <item name="android:progressTint">@android:color/system_accent1_800</item> +        <item name="android:progressBackgroundTint">@color/settingslib_neutral_variant55</item> +        <item name="android:progressBackgroundTintMode">src_over</item> +    </style> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/styles_preference_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/styles_preference_expressive.xml index cec8e45e2bfb..ec6fe887d31e 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v36/styles_preference_expressive.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v36/styles_preference_expressive.xml @@ -40,6 +40,7 @@      </style>      <style name="SettingsLibPreference.Category.Expressive"> +        <item name="layout">@layout/settingslib_expressive_preference_category</item>      </style>      <style name="SettingsLibPreference.CheckBoxPreference.Expressive"> diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml index 1c45ff6ca6cf..54bd069f2fc3 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml @@ -22,6 +22,7 @@          <item name="android:textColorPrimary">@color/settingslib_materialColorOnSurface</item>          <item name="android:textColorSecondary">@color/settingslib_text_color_secondary</item>          <item name="android:textColorTertiary">@color/settingslib_materialColorOutline</item> +        <item name="android:seekBarStyle">@style/Seekbar.SettingsLib</item>          <!-- Set up edge-to-edge configuration for top app bar -->          <item name="android:clipToPadding">false</item>          <item name="android:clipChildren">false</item> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index e81c8220d707..78b307e7b816 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -25,6 +25,7 @@ import android.bluetooth.BluetoothClass;  import android.bluetooth.BluetoothCsipSetCoordinator;  import android.bluetooth.BluetoothDevice;  import android.bluetooth.BluetoothHearingAid; +import android.bluetooth.BluetoothLeBroadcastReceiveState;  import android.bluetooth.BluetoothProfile;  import android.bluetooth.BluetoothUuid;  import android.content.Context; @@ -58,6 +59,7 @@ import com.android.settingslib.flags.Flags;  import com.android.settingslib.utils.ThreadUtils;  import com.android.settingslib.widget.AdaptiveOutlineDrawable; +import com.google.common.collect.ImmutableSet;  import com.google.common.util.concurrent.FutureCallback;  import com.google.common.util.concurrent.Futures;  import com.google.common.util.concurrent.ListenableFuture; @@ -397,6 +399,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>                  }              }              Log.d(TAG, "Disconnect " + this); +            if (Flags.enableLeAudioSharing()) { +                removeBroadcastSource(ImmutableSet.of(mDevice)); +            }              mDevice.disconnect();          }          // Disconnect  PBAP server in case its connected @@ -609,6 +614,16 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>              final BluetoothDevice dev = mDevice;              if (dev != null) {                  mUnpairing = true; +                if (Flags.enableLeAudioSharing()) { +                    Set<BluetoothDevice> devicesToRemoveSource = new HashSet<>(); +                    devicesToRemoveSource.add(dev); +                    if (getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { +                        for (CachedBluetoothDevice member : getMemberDevice()) { +                            devicesToRemoveSource.add(member.getDevice()); +                        } +                    } +                    removeBroadcastSource(devicesToRemoveSource); +                }                  final boolean successful = dev.removeBond();                  if (successful) {                      releaseLruCache(); @@ -623,6 +638,25 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>          }      } +    @WorkerThread +    private void removeBroadcastSource(Set<BluetoothDevice> devices) { +        if (mProfileManager == null || devices.isEmpty()) return; +        LocalBluetoothLeBroadcast broadcast = mProfileManager.getLeAudioBroadcastProfile(); +        LocalBluetoothLeBroadcastAssistant assistant = +                mProfileManager.getLeAudioBroadcastAssistantProfile(); +        if (broadcast != null && assistant != null && broadcast.isEnabled(null)) { +            for (BluetoothDevice device : devices) { +                for (BluetoothLeBroadcastReceiveState state : assistant.getAllSources(device)) { +                    if (BluetoothUtils.D) { +                        Log.d(TAG, "Remove broadcast source " + state.getBroadcastId() +                                + " from device " + device.getAnonymizedAddress()); +                    } +                    assistant.removeSource(device, state.getSourceId()); +                } +            } +        } +    } +      public int getProfileConnectionState(LocalBluetoothProfile profile) {          return profile != null                  ? profile.getConnectionStatus(mDevice) diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java index 635010ec824d..146b66737e83 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java @@ -2383,6 +2383,52 @@ public class CachedBluetoothDeviceTest {                  Integer.parseInt(TWS_BATTERY_LEFT));      } +    @Test +    public void disconnect_removeBroadcastSource() { +        when(mCachedDevice.getGroupId()).thenReturn(1); +        when(mSubCachedDevice.getGroupId()).thenReturn(1); +        mCachedDevice.addMemberDevice(mSubCachedDevice); +        LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); +        LocalBluetoothLeBroadcastAssistant assistant = mock( +                LocalBluetoothLeBroadcastAssistant.class); +        when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); +        when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); +        when(broadcast.isEnabled(null)).thenReturn(true); +        BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); +        when(state.getSourceId()).thenReturn(1); +        when(assistant.getAllSources(mDevice)).thenReturn(ImmutableList.of(state)); +        when(assistant.getAllSources(mSubDevice)).thenReturn(ImmutableList.of(state)); + +        mCachedDevice.disconnect(); +        verify(assistant).removeSource(mDevice, /* sourceId= */1); +        verify(assistant).removeSource(mSubDevice, /* sourceId= */1); +        verify(mDevice).disconnect(); +        verify(mSubDevice).disconnect(); +    } + +    @Test +    public void unpair_removeBroadcastSource() { +        when(mCachedDevice.getGroupId()).thenReturn(1); +        when(mSubCachedDevice.getGroupId()).thenReturn(1); +        mCachedDevice.addMemberDevice(mSubCachedDevice); +        when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); +        LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); +        LocalBluetoothLeBroadcastAssistant assistant = mock( +                LocalBluetoothLeBroadcastAssistant.class); +        when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); +        when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); +        when(broadcast.isEnabled(null)).thenReturn(true); +        BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); +        when(state.getSourceId()).thenReturn(1); +        when(assistant.getAllSources(mDevice)).thenReturn(ImmutableList.of(state)); +        when(assistant.getAllSources(mSubDevice)).thenReturn(ImmutableList.of(state)); + +        mCachedDevice.unpair(); +        verify(assistant).removeSource(mDevice, /* sourceId= */1); +        verify(assistant).removeSource(mSubDevice, /* sourceId= */1); +        verify(mDevice).removeBond(); +    } +      private void updateProfileStatus(LocalBluetoothProfile profile, int status) {          doReturn(status).when(profile).getConnectionStatus(mDevice);          mCachedDevice.onProfileStateChanged(profile, status); diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index fd3f18d4724c..a93291f2db98 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -2001,6 +2001,13 @@ flag {  }  flag { +   name: "notification_animated_actions_treatment" +   namespace: "systemui" +   description: "Special UI treatment for animated actions and replys" +   bug: "383567383" +} + +flag {      name: "show_audio_sharing_slider_in_volume_panel"      namespace: "cross_device_experiences"      description: "Show two sliders in volume panel when audio sharing." diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java index 060f0c94732d..9e08317d2c6b 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java @@ -199,7 +199,9 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner                      info.releaseAllSurfaces();                      // Make sure that the transition leashes created are not leaked.                      for (SurfaceControl leash : leashMap.values()) { -                        finishTransaction.reparent(leash, null); +                        if (leash.isValid()) { +                            finishTransaction.reparent(leash, null); +                        }                      }                      // Don't release here since launcher might still be using them. Instead                      // let launcher release them (eg. via RemoteAnimationTargets) diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt index 07a571b94ce4..c411d272cb22 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.unit.dp  import kotlin.math.roundToInt  import kotlinx.coroutines.CoroutineScope +/** Returns a [remember]ed [OffsetOverscrollEffect]. */  @Composable  @OptIn(ExperimentalMaterial3ExpressiveApi::class)  fun rememberOffsetOverscrollEffect( @@ -63,7 +64,10 @@ data class OffsetOverscrollEffectFactory(      private val animationSpec: AnimationSpec<Float>,  ) : OverscrollFactory {      override fun createOverscrollEffect(): OverscrollEffect { -        return OffsetOverscrollEffect(animationScope, animationSpec) +        return OffsetOverscrollEffect( +            animationScope = animationScope, +            animationSpec = animationSpec, +        )      }  } @@ -80,11 +84,11 @@ class OffsetOverscrollEffect(animationScope: CoroutineScope, animationSpec: Anim                  return layout(placeable.width, placeable.height) {                      val offsetPx = computeOffset(density = this@measure, overscrollDistance)                      if (offsetPx != 0) { -                        placeable.placeRelativeWithLayer( +                        placeable.placeWithLayer(                              with(requireConverter()) { offsetPx.toIntOffset() }                          )                      } else { -                        placeable.placeRelative(0, 0) +                        placeable.place(0, 0)                      }                  }              } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index 8f0fb20cef36..cb03119d1e19 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.Modifier  import androidx.compose.ui.geometry.Offset  import androidx.compose.ui.geometry.Rect  import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color  import androidx.compose.ui.graphics.graphicsLayer  import androidx.compose.ui.layout.boundsInWindow  import androidx.compose.ui.layout.onPlaced @@ -266,7 +267,10 @@ fun ContentScope.QuickSettingsLayout(                  BrightnessSliderContainer(                      viewModel = viewModel.brightnessSliderViewModel,                      containerColors = -                        ContainerColors.singleColor(OverlayShade.Colors.PanelBackground), +                        ContainerColors( +                            idleColor = Color.Transparent, +                            mirrorColor = OverlayShade.Colors.PanelBackground, +                        ),                      modifier = Modifier.fillMaxWidth(),                  )              } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/shared/SessionStorage.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/shared/SessionStorage.kt index dc5891915bfc..d1cbeca2070c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/shared/SessionStorage.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/shared/SessionStorage.kt @@ -16,6 +16,7 @@  package com.android.systemui.scene.session.shared +import androidx.compose.runtime.RememberObserver  import androidx.compose.runtime.getValue  import androidx.compose.runtime.mutableStateOf  import androidx.compose.runtime.setValue @@ -39,6 +40,9 @@ class SessionStorage {      /** Clears the data store; any downstream usage within `@Composable`s will be recomposed. */      fun clear() { +        for (storageEntry in _storage.values) { +            (storageEntry.stored as? RememberObserver)?.onForgotten() +        }          _storage = hashMapOf()      }  } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index b30e12f073ad..89c54bcc1ced 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -37,7 +37,6 @@ import androidx.compose.foundation.layout.systemBarsIgnoringVisibility  import androidx.compose.foundation.layout.waterfall  import androidx.compose.foundation.layout.width  import androidx.compose.foundation.overscroll -import androidx.compose.material3.MaterialTheme  import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass  import androidx.compose.runtime.Composable  import androidx.compose.runtime.ReadOnlyComposable @@ -46,6 +45,7 @@ import androidx.compose.ui.Alignment  import androidx.compose.ui.Modifier  import androidx.compose.ui.graphics.Color  import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.LocalResources  import androidx.compose.ui.res.dimensionResource  import androidx.compose.ui.unit.Dp  import androidx.compose.ui.unit.dp @@ -57,6 +57,8 @@ import com.android.mechanics.behavior.VerticalExpandContainerSpec  import com.android.mechanics.behavior.verticalExpandContainerBackground  import com.android.systemui.Flags  import com.android.systemui.res.R +import com.android.systemui.shade.ui.ShadeColors.notificationScrim +import com.android.systemui.shade.ui.ShadeColors.shadePanel  import com.android.systemui.shade.ui.composable.OverlayShade.rememberShadeExpansionMotion  /** Renders a lightweight shade UI container, as an overlay. */ @@ -190,17 +192,15 @@ object OverlayShade {      }      object Colors { -        val ScrimBackground = Color(0f, 0f, 0f, alpha = 0.3f) +        val ScrimBackground: Color +            @Composable +            @ReadOnlyComposable +            get() = Color(LocalResources.current.notificationScrim(Flags.notificationShadeBlur())) +          val PanelBackground: Color              @Composable              @ReadOnlyComposable -            get() { -                return if (Flags.notificationShadeBlur()) { -                    MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f) -                } else { -                    MaterialTheme.colorScheme.surfaceContainer -                } -            } +            get() = Color(LocalResources.current.shadePanel(Flags.notificationShadeBlur()))      }      object Dimensions { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepositoryTest.kt new file mode 100644 index 000000000000..5609e8b7604c --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepositoryTest.kt @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.cursorposition.data.repository + +import android.os.Handler +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import android.view.Display.DEFAULT_DISPLAY +import android.view.Display.TYPE_EXTERNAL +import android.view.Display.TYPE_INTERNAL +import android.view.InputDevice.SOURCE_MOUSE +import android.view.InputDevice.SOURCE_TOUCHPAD +import android.view.MotionEvent +import androidx.test.filters.SmallTest +import com.android.app.displaylib.PerDisplayInstanceRepositoryImpl +import com.android.systemui.SysuiTestCase +import com.android.systemui.cursorposition.data.model.CursorPosition +import com.android.systemui.cursorposition.data.repository.SingleDisplayCursorPositionRepositoryImpl.Companion.defaultInputEventListenerBuilder +import com.android.systemui.cursorposition.domain.data.repository.TestCursorPositionRepositoryInstanceProvider +import com.android.systemui.display.data.repository.display +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.display.data.repository.fakeDisplayInstanceLifecycleManager +import com.android.systemui.display.data.repository.perDisplayDumpHelper +import com.android.systemui.kosmos.backgroundScope +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.shared.system.InputChannelCompat +import com.android.systemui.shared.system.InputMonitorCompat +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.launch +import org.junit.Before +import org.junit.Rule +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +@RunWithLooper +@kotlinx.coroutines.ExperimentalCoroutinesApi +class MultiDisplayCursorPositionRepositoryTest(private val cursorEventSource: Int) : +    SysuiTestCase() { + +    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() +    private lateinit var underTest: MultiDisplayCursorPositionRepository +    private val kosmos = testKosmos().useUnconfinedTestDispatcher() +    private val displayRepository = kosmos.displayRepository +    private val displayLifecycleManager = kosmos.fakeDisplayInstanceLifecycleManager + +    private lateinit var listener: InputChannelCompat.InputEventListener +    private var emittedCursorPosition: CursorPosition? = null + +    @Mock private lateinit var inputMonitor: InputMonitorCompat +    @Mock private lateinit var inputReceiver: InputChannelCompat.InputEventReceiver + +    private val x = 100f +    private val y = 200f + +    private lateinit var testableLooper: TestableLooper + +    @Before +    fun setup() { +        testableLooper = TestableLooper.get(this) +        val testHandler = Handler(testableLooper.looper) +        whenever(inputMonitor.getInputReceiver(any(), any(), any())).thenReturn(inputReceiver) +        displayLifecycleManager.displayIds.value = setOf(DEFAULT_DISPLAY, DISPLAY_2) + +        val cursorPerDisplayRepository = +            PerDisplayInstanceRepositoryImpl( +                debugName = "testCursorPositionPerDisplayInstanceRepository", +                instanceProvider = +                    TestCursorPositionRepositoryInstanceProvider( +                        testHandler, +                        { channel -> +                            listener = defaultInputEventListenerBuilder.build(channel) +                            listener +                        }, +                    ) { _: String, _: Int -> +                        inputMonitor +                    }, +                displayLifecycleManager, +                kosmos.backgroundScope, +                displayRepository, +                kosmos.perDisplayDumpHelper, +            ) + +        underTest = +            MultiDisplayCursorPositionRepositoryImpl( +                displayRepository, +                backgroundScope = kosmos.backgroundScope, +                cursorPerDisplayRepository, +            ) +    } + +    @Test +    fun getCursorPositionFromDefaultDisplay() = setUpAndRunTest { +        val event = getMotionEvent(x, y, 0) +        listener.onInputEvent(event) + +        assertThat(emittedCursorPosition).isEqualTo(CursorPosition(x, y, 0)) +    } + +    @Test +    fun getCursorPositionFromAdditionDisplay() = setUpAndRunTest { +        addDisplay(id = DISPLAY_2, type = TYPE_EXTERNAL) + +        val event = getMotionEvent(x, y, DISPLAY_2) +        listener.onInputEvent(event) + +        assertThat(emittedCursorPosition).isEqualTo(CursorPosition(x, y, DISPLAY_2)) +    } + +    @Test +    fun noCursorPositionFromRemovedDisplay() = setUpAndRunTest { +        addDisplay(id = DISPLAY_2, type = TYPE_EXTERNAL) +        removeDisplay(DISPLAY_2) + +        val event = getMotionEvent(x, y, DISPLAY_2) +        listener.onInputEvent(event) + +        assertThat(emittedCursorPosition).isEqualTo(null) +    } + +    @Test +    fun disposeInputMonitorAndInputReceiver() = setUpAndRunTest { +        addDisplay(DISPLAY_2, TYPE_EXTERNAL) +        removeDisplay(DISPLAY_2) + +        verify(inputMonitor).dispose() +        verify(inputReceiver).dispose() +    } + +    private fun setUpAndRunTest(block: suspend () -> Unit) = +        kosmos.runTest { +            // Add default display before creating cursor repository +            displayRepository.addDisplays(display(id = DEFAULT_DISPLAY, type = TYPE_INTERNAL)) + +            backgroundScope.launch { +                underTest.cursorPositions.collect { emittedCursorPosition = it } +            } +            // Run all tasks received by TestHandler to create input monitors +            testableLooper.processAllMessages() + +            block() +        } + +    private suspend fun addDisplay(id: Int, type: Int) { +        displayRepository.addDisplays(display(id = id, type = type)) +        testableLooper.processAllMessages() +    } + +    private suspend fun removeDisplay(id: Int) { +        displayRepository.removeDisplay(id) +        testableLooper.processAllMessages() +    } + +    private fun getMotionEvent(x: Float, y: Float, displayId: Int): MotionEvent { +        val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, x, y, 0) +        event.source = cursorEventSource +        event.displayId = displayId +        return event +    } + +    private companion object { +        const val DISPLAY_2 = DEFAULT_DISPLAY + 1 + +        @JvmStatic +        @Parameters(name = "source = {0}") +        fun data(): List<Int> { +            return listOf(SOURCE_MOUSE, SOURCE_TOUCHPAD) +        } +    } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepositoryTest.kt new file mode 100644 index 000000000000..a2e42976f413 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepositoryTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.data.repository + +import android.content.pm.UserInfo +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyboard.shortcut.fakeLauncherApps +import com.android.systemui.keyboard.shortcut.userVisibleAppsRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.settings.fakeUserTracker +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class UserVisibleAppsRepositoryTest : SysuiTestCase() { + +    private val kosmos = testKosmos().useUnconfinedTestDispatcher() +    private val fakeLauncherApps = kosmos.fakeLauncherApps +    private val repo = kosmos.userVisibleAppsRepository +    private val userTracker = kosmos.fakeUserTracker +    private val testScope = kosmos.testScope +    private val userVisibleAppsContainsApplication: +        (pkgName: String, clsName: String) -> Flow<Boolean> = +        { pkgName, clsName -> +            repo.userVisibleApps.map { userVisibleApps -> +                userVisibleApps.any { +                    it.componentName.packageName == pkgName && it.componentName.className == clsName +                } +            } +        } + +    @Before +    fun setup() { +        switchUser(index = PRIMARY_USER_INDEX) +    } + +    @Test +    fun userVisibleApps_emitsUpdatedAppsList_onNewAppInstalled() { +        testScope.runTest { +            val containsPackageOne by +                collectLastValue(userVisibleAppsContainsApplication(TEST_PACKAGE_1, TEST_CLASS_1)) + +            installPackageOneForUserOne() + +            assertThat(containsPackageOne).isTrue() +        } +    } + +    @Test +    fun userVisibleApps_emitsUpdatedAppsList_onAppUserChanged() { +        testScope.runTest { +            val containsPackageOne by +                collectLastValue(userVisibleAppsContainsApplication(TEST_PACKAGE_1, TEST_CLASS_1)) +            val containsPackageTwo by +                collectLastValue(userVisibleAppsContainsApplication(TEST_PACKAGE_2, TEST_CLASS_2)) + +            installPackageOneForUserOne() +            installPackageTwoForUserTwo() + +            assertThat(containsPackageOne).isTrue() +            assertThat(containsPackageTwo).isFalse() + +            switchUser(index = SECONDARY_USER_INDEX) + +            assertThat(containsPackageOne).isFalse() +            assertThat(containsPackageTwo).isTrue() +        } +    } + +    @Test +    fun userVisibleApps_emitsUpdatedAppsList_onAppUninstalled() { +        testScope.runTest { +            val containsPackageOne by +                collectLastValue(userVisibleAppsContainsApplication(TEST_PACKAGE_1, TEST_CLASS_1)) + +            installPackageOneForUserOne() +            uninstallPackageOneForUserOne() + +            assertThat(containsPackageOne).isFalse() +        } +    } + +    private fun switchUser(index: Int) { +        userTracker.set( +            userInfos = +                listOf( +                    UserInfo(/* id= */ PRIMARY_USER_ID, /* name= */ "Primary User", /* flags= */ 0), +                    UserInfo( +                        /* id= */ SECONDARY_USER_ID, +                        /* name= */ "Secondary User", +                        /* flags= */ 0, +                    ), +                ), +            selectedUserIndex = index, +        ) +    } + +    private fun installPackageOneForUserOne() { +        fakeLauncherApps.installPackageForUser( +            TEST_PACKAGE_1, +            TEST_CLASS_1, +            UserHandle(/* userId= */ PRIMARY_USER_ID), +        ) +    } + +    private fun uninstallPackageOneForUserOne() { +        fakeLauncherApps.uninstallPackageForUser( +            TEST_PACKAGE_1, +            TEST_CLASS_1, +            UserHandle(/* userId= */ PRIMARY_USER_ID), +        ) +    } + +    private fun installPackageTwoForUserTwo() { +        fakeLauncherApps.installPackageForUser( +            TEST_PACKAGE_2, +            TEST_CLASS_2, +            UserHandle(/* userId= */ SECONDARY_USER_ID), +        ) +    } + +    companion object { +        const val TEST_PACKAGE_1 = "test.package.one" +        const val TEST_PACKAGE_2 = "test.package.two" +        const val TEST_CLASS_1 = "TestClassOne" +        const val TEST_CLASS_2 = "TestClassTwo" +        const val PRIMARY_USER_ID = 10 +        const val PRIMARY_USER_INDEX = 0 +        const val SECONDARY_USER_ID = 11 +        const val SECONDARY_USER_INDEX = 1 +    } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 676e1ea5321a..579c242f974a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -41,16 +41,12 @@ import static org.mockito.Mockito.when;  import android.animation.Animator;  import android.annotation.IdRes; -import android.content.ContentResolver;  import android.content.res.Configuration;  import android.content.res.Resources;  import android.os.Handler;  import android.os.Looper; -import android.os.PowerManager; -import android.os.UserManager;  import android.util.DisplayMetrics;  import android.view.Display; -import android.view.LayoutInflater;  import android.view.MotionEvent;  import android.view.View;  import android.view.ViewGroup; @@ -65,10 +61,8 @@ import com.android.internal.logging.UiEventLogger;  import com.android.internal.logging.testing.UiEventLoggerFake;  import com.android.internal.statusbar.IStatusBarService;  import com.android.internal.util.LatencyTracker; -import com.android.keyguard.KeyguardSliceViewController;  import com.android.keyguard.KeyguardUpdateMonitor;  import com.android.keyguard.dagger.KeyguardStatusBarViewComponent; -import com.android.keyguard.logging.KeyguardLogger;  import com.android.systemui.SysuiTestCase;  import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;  import com.android.systemui.classifier.FalsingCollectorFake; @@ -142,7 +136,6 @@ import com.android.systemui.statusbar.notification.stack.AmbientState;  import com.android.systemui.statusbar.notification.stack.NotificationListContainer;  import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;  import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator;  import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor;  import com.android.systemui.statusbar.phone.CentralSurfaces;  import com.android.systemui.statusbar.phone.ConfigurationControllerImpl; @@ -164,9 +157,7 @@ import com.android.systemui.statusbar.policy.CastController;  import com.android.systemui.statusbar.policy.ConfigurationController;  import com.android.systemui.statusbar.policy.KeyguardStateController;  import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; -import com.android.systemui.statusbar.policy.SplitShadeStateController;  import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository; -import com.android.systemui.statusbar.window.StatusBarWindowStateController;  import com.android.systemui.unfold.SysUIUnfoldComponent;  import com.android.systemui.user.domain.interactor.UserSwitcherInteractor;  import com.android.systemui.util.kotlin.JavaAdapter; @@ -214,7 +205,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {      @Mock protected DozeParameters mDozeParameters;      @Mock protected ScreenOffAnimationController mScreenOffAnimationController;      @Mock protected NotificationPanelView mView; -    @Mock protected LayoutInflater mLayoutInflater;      @Mock protected DynamicPrivacyController mDynamicPrivacyController;      @Mock protected ShadeTouchableRegionManager mShadeTouchableRegionManager;      @Mock protected KeyguardStateController mKeyguardStateController; @@ -223,7 +213,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {      @Mock protected CommandQueue mCommandQueue;      @Mock protected VibratorHelper mVibratorHelper;      @Mock protected LatencyTracker mLatencyTracker; -    @Mock protected PowerManager mPowerManager;      @Mock protected AccessibilityManager mAccessibilityManager;      @Mock protected MetricsLogger mMetricsLogger;      @Mock protected Resources mResources; @@ -242,14 +231,12 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {      @Mock protected ScrimController mScrimController;      @Mock protected MediaDataManager mMediaDataManager;      @Mock protected AmbientState mAmbientState; -    @Mock protected UserManager mUserManager;      @Mock protected UiEventLogger mUiEventLogger;      @Mock protected KeyguardMediaController mKeyguardMediaController;      @Mock protected NavigationModeController mNavigationModeController;      @Mock protected NavigationBarController mNavigationBarController;      @Mock protected QuickSettingsControllerImpl mQsController;      @Mock protected ShadeHeaderController mShadeHeaderController; -    @Mock protected ContentResolver mContentResolver;      @Mock protected TapAgainViewController mTapAgainViewController;      @Mock protected KeyguardIndicationController mKeyguardIndicationController;      @Mock protected FragmentService mFragmentService; @@ -261,12 +248,10 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {      @Mock protected DumpManager mDumpManager;      @Mock protected NotificationsQSContainerController mNotificationsQSContainerController;      @Mock protected QsFrameTranslateController mQsFrameTranslateController; -    @Mock protected StatusBarWindowStateController mStatusBarWindowStateController;      @Mock protected KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;      @Mock protected NotificationShadeWindowController mNotificationShadeWindowController;      @Mock protected SysUiState mSysUiState;      @Mock protected NotificationListContainer mNotificationListContainer; -    @Mock protected NotificationStackSizeCalculator mNotificationStackSizeCalculator;      @Mock protected UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;      @Mock protected QS mQs;      @Mock protected QSFragmentLegacy mQSFragment; @@ -281,8 +266,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {      @Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor;      @Mock protected MotionEvent mDownMotionEvent;      @Mock protected CoroutineDispatcher mMainDispatcher; -    @Mock protected KeyguardSliceViewController mKeyguardSliceViewController; -    private final KeyguardLogger mKeyguardLogger = new KeyguardLogger(logcatLogBuffer());      @Captor      protected ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>              mEmptySpaceClickListenerCaptor; @@ -363,9 +346,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {                  mock(DeviceEntryUdfpsInteractor.class);          when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false)); -        final SplitShadeStateController splitShadeStateController = -                new ResourcesSplitShadeStateController(); -          mShadeInteractor = new ShadeInteractorImpl(                  mTestScope.getBackgroundScope(),                  mKosmos.getDeviceProvisioningInteractor(), @@ -380,8 +360,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {                          mTestScope.getBackgroundScope(),                          mFakeKeyguardRepository,                          mShadeRepository -                ), -                mKosmos.getShadeModeInteractor()); +                ));          SystemClock systemClock = new FakeSystemClock();          mStatusBarStateController = new StatusBarStateControllerImpl(                  mUiEventLogger, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java index db0c07c50dc6..348eee8de313 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java @@ -199,8 +199,7 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase {                          mTestScope.getBackgroundScope(),                          mKeyguardRepository,                          mShadeRepository -                ), -                mKosmos.getShadeModeInteractor()); +                ));          when(mResources.getDimensionPixelSize(                  R.dimen.lockscreen_shade_qs_transition_distance)).thenReturn(DEFAULT_HEIGHT); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index 67af7a54988e..039a32ba9127 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -353,7 +353,6 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {          notificationShadeDepthController.addListener(listener)          notificationShadeDepthController.updateBlurCallback.doFrame(0)          verify(wallpaperController).setNotificationShadeZoom(anyFloat()) -        verify(listener).onWallpaperZoomOutChanged(anyFloat())          verify(blurUtils).applyBlur(any(), anyInt(), eq(false))      } @@ -369,7 +368,6 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {          notificationShadeDepthController.updateBlurCallback.doFrame(0)          verify(wallpaperController).setNotificationShadeZoom(eq(0f)) -        verify(listener).onWallpaperZoomOutChanged(eq(0f))      }      @Test @@ -384,7 +382,6 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {          notificationShadeDepthController.updateBlurCallback.doFrame(0)          verify(wallpaperController).setNotificationShadeZoom(floatThat { it != 0f }) -        verify(listener).onWallpaperZoomOutChanged(floatThat { it != 0f })      }      @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/ConversationNotificationProcessorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/ConversationNotificationProcessorTest.kt index 0caddf46cd3a..d56890dc5d3f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/ConversationNotificationProcessorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/ConversationNotificationProcessorTest.kt @@ -137,9 +137,28 @@ class ConversationNotificationProcessorTest : SysuiTestCase() {      @Test      @EnableFlags(Flags.FLAG_NM_SUMMARIZATION) +    fun processNotification_messagingStyleUpdateSummarizationToNull() { +        val nb = getMessagingNotification() +        val newRow: ExpandableNotificationRow = testHelper.createRow(nb.build()) +        newRow.entry.setRanking( +            RankingBuilder(newRow.entry.ranking).setSummarization("hello").build() +        ) +        assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) +            .isNotNull() + +        newRow.entry.setRanking(RankingBuilder(newRow.entry.ranking).setSummarization(null).build()) + +        assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) +            .isNotNull() +        assertThat(nb.build().extras.getCharSequence(EXTRA_SUMMARIZED_CONTENT)).isNull() +    } + +    @Test +    @EnableFlags(Flags.FLAG_NM_SUMMARIZATION)      fun processNotification_messagingStyleWithoutSummarization() {          val nb = getMessagingNotification()          val newRow: ExpandableNotificationRow = testHelper.createRow(nb.build()) +          assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger))              .isNotNull()          assertThat(nb.build().extras.getCharSequence(EXTRA_SUMMARIZED_CONTENT)).isNull() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimatorTest.kt index 0bb473721446..c22b03cc1a1b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimatorTest.kt @@ -207,6 +207,21 @@ class PhysicsPropertyAnimatorTest : SysuiTestCase() {      }      @Test +    fun testCancelAnimationResetsOffset() { +        PhysicsPropertyAnimator.setProperty( +            view, +            property, +            200f, +            animationProperties, +            true, +            finishListener, +        ) +        val propertyData = ViewState.getChildTag(view, property.tag) as PropertyData +        propertyData.animator?.cancel() +        Assert.assertTrue(propertyData.offset == 0f) +    } + +    @Test      fun testUsingListenerProperties() {          val finishListener2 = Mockito.mock(DynamicAnimation.OnAnimationEndListener::class.java)          val animationProperties: AnimationProperties = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index b3d678b1fda6..247c66aebad7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -17,7 +17,9 @@ package com.android.systemui.statusbar.notification.collection.coordinator  import android.app.Notification.GROUP_ALERT_ALL  import android.app.Notification.GROUP_ALERT_SUMMARY +import android.app.NotificationChannel.SYSTEM_RESERVED_IDS  import android.platform.test.annotations.EnableFlags +import android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION  import android.testing.TestableLooper.RunWithLooper  import androidx.test.ext.junit.runners.AndroidJUnit4  import androidx.test.filters.SmallTest @@ -50,6 +52,7 @@ import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinde  import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision  import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.DecisionImpl  import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionLogger  import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider  import com.android.systemui.statusbar.notification.row.mockNotificationActionClickManager  import com.android.systemui.statusbar.notification.shared.NotificationBundleUi @@ -61,7 +64,6 @@ import com.android.systemui.util.mockito.eq  import com.android.systemui.util.mockito.mock  import com.android.systemui.util.mockito.withArgCaptor  import com.android.systemui.util.time.FakeSystemClock -import java.util.ArrayList  import java.util.function.Consumer  import kotlinx.coroutines.test.runTest  import org.junit.Assert.assertEquals @@ -105,6 +107,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {      private val notifPipeline: NotifPipeline = mock()      private val logger = HeadsUpCoordinatorLogger(logcatLogBuffer(), verbose = true) +    private val interruptLogger: VisualInterruptionDecisionLogger = mock()      private val headsUpManager: HeadsUpManagerImpl = mock()      private val headsUpViewBinder: HeadsUpViewBinder = mock()      private val visualInterruptionDecisionProvider: VisualInterruptionDecisionProvider = mock() @@ -135,6 +138,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {              HeadsUpCoordinator(                  kosmos.applicationCoroutineScope,                  logger, +                interruptLogger,                  systemClock,                  notifCollection,                  headsUpManager, @@ -920,6 +924,48 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {          assertFalse(groupSummary.hasInterrupted())      } +    private fun helpTestNoTransferToBundleChildForChannel(channelId: String) { +        // Set up for normal alert transfer from summary to child +        // but here child is classified so it should not happen +        val bundleChild = +            helper.createClassifiedEntry(/* isSummary= */ false, GROUP_ALERT_SUMMARY, channelId); +        setShouldHeadsUp(bundleChild, true) +        setShouldHeadsUp(groupSummary, true) +        whenever(notifPipeline.allNotifs).thenReturn(listOf(groupSummary, bundleChild)) + +        collectionListener.onEntryAdded(groupSummary) +        collectionListener.onEntryAdded(bundleChild) + +        beforeTransformGroupsListener.onBeforeTransformGroups(listOf(groupSummary, bundleChild)) +        beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupSummary, bundleChild)) + +        verify(headsUpViewBinder, never()).bindHeadsUpView(eq(groupSummary), any(), any()) +        verify(headsUpViewBinder, never()).bindHeadsUpView(eq(bundleChild), any(), any()) + +        verify(headsUpManager, never()).showNotification(groupSummary) +        verify(headsUpManager, never()).showNotification(bundleChild) + +        // Capture last param +        val decision = withArgCaptor { +            verify(interruptLogger) +                .logDecision(capture(), capture(), capture()) +        } +        assertFalse(decision.shouldInterrupt) +        assertEquals(decision.logReason, "disqualified-transfer-target") + +        // Must clear invocations, otherwise these calls get stored for the next call from the same +        // test, which complains that there are more invocations than expected +        clearInvocations(interruptLogger) +    } + +    @Test +    @EnableFlags(FLAG_NOTIFICATION_CLASSIFICATION) +    fun testNoTransfer_toBundleChild() { +        for (id in SYSTEM_RESERVED_IDS) { +            helpTestNoTransferToBundleChildForChannel(id) +        } +    } +      @Test      fun testOnRankingApplied_newEntryShouldAlert() {          // GIVEN that mEntry has never interrupted in the past, and now should diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java index d61fc05c699f..28ca891b2771 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java @@ -16,6 +16,7 @@  package com.android.systemui.statusbar.notification.logging; +import com.android.systemui.statusbar.notification.collection.EntryAdapter;  import com.android.systemui.statusbar.notification.collection.NotificationEntry;  import com.android.systemui.statusbar.notification.logging.nano.Notifications; @@ -49,6 +50,11 @@ public class NotificationPanelLoggerFake implements NotificationPanelLogger {      public void logNotificationDrag(NotificationEntry draggedNotification) {      } +    @Override +    public void logNotificationDrag(EntryAdapter draggedNotification) { + +    } +      public static class CallRecord {          public boolean isLockscreen;          public Notifications.NotificationList list; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java index fd49f60e7ae1..bbff9cf2f616 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java @@ -40,9 +40,12 @@ import androidx.test.filters.SmallTest;  import com.android.systemui.SysuiTestCase;  import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;  import com.android.systemui.shade.ShadeController; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; +import com.android.systemui.statusbar.notification.collection.NotificationEntry;  import com.android.systemui.statusbar.notification.headsup.PinnedStatus;  import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;  import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;  import org.junit.Before;  import org.junit.Test; @@ -99,7 +102,13 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {          mRow.doDragCallback(0, 0);          verify(controller).startDragAndDrop(mRow);          verify(mHeadsUpManager, times(1)).releaseAllImmediately(); -        verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any()); +        if (NotificationBundleUi.isEnabled()) { +            verify(mNotificationPanelLogger, times(1)) +                    .logNotificationDrag(any(EntryAdapter.class)); +        } else { +            verify(mNotificationPanelLogger, times(1)) +                    .logNotificationDrag(any(NotificationEntry.class)); +        }      }      @Test @@ -111,7 +120,13 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {          verify(controller).startDragAndDrop(mRow);          verify(mShadeController).animateCollapseShade(eq(0), eq(true),                  eq(false), anyFloat()); -        verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any()); +        if (NotificationBundleUi.isEnabled()) { +            verify(mNotificationPanelLogger, times(1)) +                    .logNotificationDrag(any(EntryAdapter.class)); +        } else { +            verify(mNotificationPanelLogger, times(1)) +                    .logNotificationDrag(any(NotificationEntry.class)); +        }      }      @Test @@ -129,8 +144,13 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {          // Verify that we never start the actual drag since there is no content          verify(mRow, never()).startDragAndDrop(any(), any(), any(), anyInt()); -        verify(mNotificationPanelLogger, never()).logNotificationDrag(any()); -    } +        if (NotificationBundleUi.isEnabled()) { +            verify(mNotificationPanelLogger, never()) +                    .logNotificationDrag(any(EntryAdapter.class)); +        } else { +            verify(mNotificationPanelLogger, never()) +                    .logNotificationDrag(any(NotificationEntry.class)); +        }    }      private ExpandableNotificationRowDragController createSpyController() {          return spy(mController); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java index 19e98387a120..533b7a6a6acf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java @@ -16,6 +16,7 @@  package com.android.systemui.statusbar.phone; +import static android.app.NotificationManager.IMPORTANCE_LOW;  import static com.google.common.truth.Truth.assertThat;  import static org.mockito.Mockito.mock; @@ -23,6 +24,7 @@ import static org.mockito.Mockito.when;  import android.app.ActivityManager;  import android.app.Notification; +import android.app.NotificationChannel;  import android.content.Context;  import android.os.UserHandle;  import android.service.notification.StatusBarNotification; @@ -85,6 +87,33 @@ public final class NotificationGroupTestHelper {          return entry;      } +    public NotificationEntry createClassifiedEntry(boolean isSummary, +            int groupAlertBehavior, String channelId) { + +        Notification notif = new Notification.Builder(mContext, TEST_CHANNEL_ID) +                .setContentTitle("Title") +                .setSmallIcon(R.drawable.ic_person) +                .setGroupAlertBehavior(groupAlertBehavior) +                .setGroupSummary(isSummary) +                .setGroup(TEST_GROUP_ID) +                .build(); + +        NotificationChannel channel = new NotificationChannel(channelId, channelId, IMPORTANCE_LOW); +        NotificationEntry entry = new NotificationEntryBuilder() +                .setPkg(TEST_PACKAGE_NAME) +                .setOpPkg(TEST_PACKAGE_NAME) +                .setId(mId++) +                .setNotification(notif) +                .updateRanking((rankingBuilder -> rankingBuilder.setChannel(channel))) +                .setUser(new UserHandle(ActivityManager.getCurrentUser())) +                .build(); + +        ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); +        entry.setRow(row); +        when(row.getEntryLegacy()).thenReturn(entry); +        return entry; +    } +      public NotificationEntry createEntry(int id, String tag, boolean isSummary,              int groupAlertBehavior) {          Notification notif = new Notification.Builder(mContext, TEST_CHANNEL_ID) diff --git a/packages/SystemUI/res/drawable/magic_action_button_background.xml b/packages/SystemUI/res/drawable/animated_action_button_background.xml index 7199b2dfbe5a..1cecec535ad9 100644 --- a/packages/SystemUI/res/drawable/magic_action_button_background.xml +++ b/packages/SystemUI/res/drawable/animated_action_button_background.xml @@ -10,10 +10,10 @@              android:insetRight="0dp"              android:insetTop="8dp">              <shape android:shape="rectangle"> -                <corners android:radius="@dimen/magic_action_button_corner_radius" /> +                <corners android:radius="@dimen/animated_action_button_corner_radius" />                  <solid android:color="@androidprv:color/materialColorPrimaryContainer" />                  <stroke -                    android:width="@dimen/magic_action_button_outline_stroke_width" +                    android:width="@dimen/animated_action_button_outline_stroke_width"                      android:color="@androidprv:color/materialColorOutlineVariant" />              </shape>          </inset> diff --git a/packages/SystemUI/res/layout/animated_action_button.xml b/packages/SystemUI/res/layout/animated_action_button.xml new file mode 100644 index 000000000000..3e5e35b815e1 --- /dev/null +++ b/packages/SystemUI/res/layout/animated_action_button.xml @@ -0,0 +1,17 @@ +<com.android.systemui.statusbar.notification.row.AnimatedActionButton +    xmlns:android="http://schemas.android.com/apk/res/android" +    style="@android:style/Widget.Material.Button" +    android:layout_width="wrap_content" +    android:layout_height="@dimen/animated_action_button_touch_target_height" +    android:background="@drawable/animated_action_button_background" +    android:drawablePadding="@dimen/animated_action_button_drawable_padding" +    android:ellipsize="none" +    android:fontFamily="google-sans-flex-medium" +    android:gravity="center" +    android:minWidth="0dp" +    android:paddingHorizontal="@dimen/animated_action_button_padding_horizontal" +    android:paddingVertical="@dimen/animated_action_button_inset_vertical" +    android:stateListAnimator="@null" +    android:textColor="@color/animated_action_button_text_color" +    android:textSize="@dimen/animated_action_button_font_size" +    android:textStyle="normal" /> diff --git a/packages/SystemUI/res/layout/magic_action_button.xml b/packages/SystemUI/res/layout/magic_action_button.xml deleted file mode 100644 index 63fc1e485635..000000000000 --- a/packages/SystemUI/res/layout/magic_action_button.xml +++ /dev/null @@ -1,17 +0,0 @@ -<com.android.systemui.statusbar.notification.row.MagicActionButton -    xmlns:android="http://schemas.android.com/apk/res/android" -    style="@android:style/Widget.Material.Button" -    android:layout_width="wrap_content" -    android:layout_height="@dimen/magic_action_button_touch_target_height" -    android:background="@drawable/magic_action_button_background" -    android:drawablePadding="@dimen/magic_action_button_drawable_padding" -    android:ellipsize="none" -    android:fontFamily="google-sans-flex-medium" -    android:gravity="center" -    android:minWidth="0dp" -    android:paddingHorizontal="@dimen/magic_action_button_padding_horizontal" -    android:paddingVertical="@dimen/magic_action_button_inset_vertical" -    android:stateListAnimator="@null" -    android:textColor="@color/magic_action_button_text_color" -    android:textSize="@dimen/magic_action_button_font_size" -    android:textStyle="normal" /> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 7c6a1b1bf63d..ff16e063f5b1 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -145,9 +145,9 @@      <color name="smart_reply_button_background">#ffffffff</color>      <color name="smart_reply_button_stroke">@*android:color/accent_device_default</color> -    <!-- Magic Action colors --> -    <color name="magic_action_button_text_color">@androidprv:color/materialColorOnSurface</color> -    <color name="magic_action_button_stroke_color">@androidprv:color/materialColorOnSurface</color> +    <!-- Animated Action colors --> +    <color name="animated_action_button_text_color">@androidprv:color/materialColorOnSurface</color> +    <color name="animated_action_button_stroke_color">@androidprv:color/materialColorOnSurface</color>      <!-- Biometric dialog colors -->      <color name="biometric_dialog_gray">#ff757575</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 55e94028b95e..4dfb8cdf7920 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1160,16 +1160,16 @@      <dimen name="notification_2025_smart_reply_button_corner_radius">18dp</dimen>      <dimen name="notification_2025_smart_reply_button_min_height">48dp</dimen> -    <!-- Magic Action params. --> +    <!-- Animated Action params. -->      <!-- Corner radius = half of min_height to create rounded sides. --> -    <dimen name="magic_action_button_corner_radius">16dp</dimen> -    <dimen name="magic_action_button_icon_size">20dp</dimen> -    <dimen name="magic_action_button_outline_stroke_width">1dp</dimen> -    <dimen name="magic_action_button_padding_horizontal">12dp</dimen> -    <dimen name="magic_action_button_inset_vertical">8dp</dimen> -    <dimen name="magic_action_button_drawable_padding">8dp</dimen> -    <dimen name="magic_action_button_touch_target_height">48dp</dimen> -    <dimen name="magic_action_button_font_size">12sp</dimen> +    <dimen name="animated_action_button_corner_radius">16dp</dimen> +    <dimen name="animated_action_button_icon_size">20dp</dimen> +    <dimen name="animated_action_button_outline_stroke_width">1dp</dimen> +    <dimen name="animated_action_button_padding_horizontal">12dp</dimen> +    <dimen name="animated_action_button_inset_vertical">8dp</dimen> +    <dimen name="animated_action_button_drawable_padding">8dp</dimen> +    <dimen name="animated_action_button_touch_target_height">48dp</dimen> +    <dimen name="animated_action_button_font_size">12sp</dimen>      <!-- A reasonable upper bound for the height of the smart reply button. The measuring code              needs to start with a guess for the maximum size. Currently two-line smart reply buttons diff --git a/packages/SystemUI/src/com/android/systemui/cursorposition/data/model/CursorPosition.kt b/packages/SystemUI/src/com/android/systemui/cursorposition/data/model/CursorPosition.kt new file mode 100644 index 000000000000..65174cc41028 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cursorposition/data/model/CursorPosition.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.cursorposition.data.model + +/** + * Represents the position of cursor hotspot on the screen. Hotspot is the specific pixel that + * signifies the location of the pointer's interaction with the user interface. By default, hotspot + * of a cursor is the tip of arrow. + * + * @property x The x-coordinate of the cursor hotspot, relative to the top-left corner of the + *   screen. + * @property y The y-coordinate of the cursor hotspot, relative to the top-left corner of the + *   screen. + * @property displayId The display on which the cursor is located. + */ +data class CursorPosition(val x: Float, val y: Float, val displayId: Int) diff --git a/packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepository.kt b/packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepository.kt new file mode 100644 index 000000000000..37f4a4c87114 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepository.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.cursorposition.data.repository + +import com.android.app.displaylib.PerDisplayRepository +import com.android.systemui.cursorposition.data.model.CursorPosition +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.display.data.repository.DisplayRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +/** Repository for cursor position of multi displays. */ +interface MultiDisplayCursorPositionRepository { +    val cursorPositions: Flow<CursorPosition?> +} + +/** + * Implementation of [MultiDisplayCursorPositionRepository] that aggregates cursor position updates + * from multiple displays. + * + * This class uses a [DisplayRepository] to track added displays and a [PerDisplayRepository] to + * manage [SingleDisplayCursorPositionRepository] instances for each display. [PerDisplayRepository] + * would destroy the instance if the display is removed. This class combines the cursor position + * from all displays into a single cursorPositions StateFlow. + */ +@SysUISingleton +class MultiDisplayCursorPositionRepositoryImpl +@Inject +constructor( +    private val displayRepository: DisplayRepository, +    @Background private val backgroundScope: CoroutineScope, +    private val cursorRepositories: PerDisplayRepository<SingleDisplayCursorPositionRepository>, +) : MultiDisplayCursorPositionRepository { + +    private val allDisplaysCursorPositions: Flow<CursorPosition> = +        displayRepository.displayAdditionEvent +            .mapNotNull { c -> c?.displayId } +            .onStart { emitAll(displayRepository.displayIds.value.asFlow()) } +            .flatMapMerge { +                val repo = cursorRepositories[it] +                repo?.cursorPositions ?: emptyFlow() +            } + +    override val cursorPositions: StateFlow<CursorPosition?> = +        allDisplaysCursorPositions.stateIn(backgroundScope, SharingStarted.WhileSubscribed(), null) +} diff --git a/packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/SingleDisplayCursorPositionRepository.kt b/packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/SingleDisplayCursorPositionRepository.kt new file mode 100644 index 000000000000..f532fb22a19c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/SingleDisplayCursorPositionRepository.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.cursorposition.data.repository + +import android.os.Handler +import android.os.Looper +import android.view.Choreographer +import android.view.InputDevice.SOURCE_MOUSE +import android.view.InputDevice.SOURCE_TOUCHPAD +import android.view.MotionEvent +import com.android.app.displaylib.PerDisplayInstanceProviderWithTeardown +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.cursorposition.data.model.CursorPosition +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.shared.system.InputChannelCompat +import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver +import com.android.systemui.shared.system.InputMonitorCompat +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import javax.inject.Inject +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +/** Repository for cursor position in single display. */ +interface SingleDisplayCursorPositionRepository { +    /** Flow of [CursorPosition] for the display. */ +    val cursorPositions: Flow<CursorPosition> + +    /** Destroys the repository. */ +    fun destroy() +} + +/** + * Implementation of [SingleDisplayCursorPositionRepository]. + * + * @param displayId the display id + * @param backgroundHandler the background handler + * @param listenerBuilder the builder for [InputChannelCompat.InputEventListener] + * @param inputMonitorBuilder the builder for [InputMonitorCompat] + */ +class SingleDisplayCursorPositionRepositoryImpl +@AssistedInject +constructor( +    @Assisted displayId: Int, +    @Background private val backgroundHandler: Handler, +    @Assisted +    private val listenerBuilder: InputEventListenerBuilder = defaultInputEventListenerBuilder, +    @Assisted private val inputMonitorBuilder: InputMonitorBuilder = defaultInputMonitorBuilder, +) : SingleDisplayCursorPositionRepository { + +    private var scope: ProducerScope<CursorPosition>? = null + +    private fun createInputMonitorCallbackFlow(displayId: Int): Flow<CursorPosition> = +        conflatedCallbackFlow { +                val inputMonitor: InputMonitorCompat = inputMonitorBuilder.build(TAG, displayId) +                val inputReceiver: InputEventReceiver = +                    inputMonitor.getInputReceiver( +                        Looper.myLooper(), +                        Choreographer.getInstance(), +                        listenerBuilder.build(this), +                    ) +                scope = this +                awaitClose { +                    inputMonitor.dispose() +                    inputReceiver.dispose() +                } +            } +            // Use backgroundHandler as dispatcher because it has a looper (unlike +            // "backgroundDispatcher" which does not have a looper) and input receiver could use +            // its background looper and choreographer +            .flowOn(backgroundHandler.asCoroutineDispatcher()) + +    override val cursorPositions: Flow<CursorPosition> = createInputMonitorCallbackFlow(displayId) + +    override fun destroy() { +        scope?.close() +    } + +    @AssistedFactory +    interface Factory { +        /** +         * Creates a new instance of [SingleDisplayCursorPositionRepositoryImpl] for a given +         * [displayId]. +         */ +        fun create( +            displayId: Int, +            listenerBuilder: InputEventListenerBuilder = defaultInputEventListenerBuilder, +            inputMonitorBuilder: InputMonitorBuilder = defaultInputMonitorBuilder, +        ): SingleDisplayCursorPositionRepositoryImpl +    } + +    companion object { +        private const val TAG = "CursorPositionPerDisplayRepositoryImpl" + +        private val defaultInputMonitorBuilder = InputMonitorBuilder { name, displayId -> +            InputMonitorCompat(name, displayId) +        } + +        val defaultInputEventListenerBuilder = InputEventListenerBuilder { channel -> +            InputChannelCompat.InputEventListener { event -> +                if ( +                    event is MotionEvent && +                        (event.source == SOURCE_MOUSE || event.source == SOURCE_TOUCHPAD) +                ) { +                    val cursorEvent = CursorPosition(event.x, event.y, event.displayId) +                    channel.trySendWithFailureLogging(cursorEvent, TAG) +                } +            } +        } +    } +} + +fun interface InputEventListenerBuilder { +    fun build(channel: SendChannel<CursorPosition>): InputChannelCompat.InputEventListener +} + +fun interface InputMonitorBuilder { +    fun build(name: String, displayId: Int): InputMonitorCompat +} + +@SysUISingleton +class SingleDisplayCursorPositionRepositoryFactory +@Inject +constructor(private val factory: SingleDisplayCursorPositionRepositoryImpl.Factory) : +    PerDisplayInstanceProviderWithTeardown<SingleDisplayCursorPositionRepository> { +    override fun createInstance(displayId: Int): SingleDisplayCursorPositionRepository { +        return factory.create(displayId) +    } + +    override fun destroyInstance(instance: SingleDisplayCursorPositionRepository) { +        instance.destroy() +    } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepository.kt new file mode 100644 index 000000000000..5a4ee16e0e64 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepository.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.data.repository + +import android.content.Context +import android.content.pm.LauncherActivityInfo +import android.content.pm.LauncherApps +import android.os.Handler +import android.os.UserHandle +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.settings.UserTracker +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +@SysUISingleton +class UserVisibleAppsRepository +@Inject +constructor( +    private val userTracker: UserTracker, +    @Background private val bgExecutor: Executor, +    @Background private val bgHandler: Handler, +    private val launcherApps: LauncherApps, +) { + +    val userVisibleApps: Flow<List<LauncherActivityInfo>> +        get() = conflatedCallbackFlow { +            val packageChangeCallback: LauncherApps.Callback = +                object : LauncherApps.Callback() { +                    override fun onPackageAdded(packageName: String, userHandle: UserHandle) { +                        trySendWithFailureLogging( +                            element = retrieveLauncherApps(), +                            loggingTag = TAG, +                            elementDescription = ON_PACKAGE_ADDED, +                        ) +                    } + +                    override fun onPackageChanged(packageName: String, userHandle: UserHandle) { +                        trySendWithFailureLogging( +                            element = retrieveLauncherApps(), +                            loggingTag = TAG, +                            elementDescription = ON_PACKAGE_CHANGED, +                        ) +                    } + +                    override fun onPackageRemoved(packageName: String, userHandle: UserHandle) { +                        trySendWithFailureLogging( +                            element = retrieveLauncherApps(), +                            loggingTag = TAG, +                            elementDescription = ON_PACKAGE_REMOVED, +                        ) +                    } + +                    override fun onPackagesAvailable( +                        packages: Array<out String>, +                        userHandle: UserHandle, +                        replacing: Boolean, +                    ) { +                        trySendWithFailureLogging( +                            element = retrieveLauncherApps(), +                            loggingTag = TAG, +                            elementDescription = ON_PACKAGES_AVAILABLE, +                        ) +                    } + +                    override fun onPackagesUnavailable( +                        packages: Array<out String>, +                        userHandle: UserHandle, +                        replacing: Boolean, +                    ) { +                        trySendWithFailureLogging( +                            element = retrieveLauncherApps(), +                            loggingTag = TAG, +                            elementDescription = ON_PACKAGES_UNAVAILABLE, +                        ) +                    } +                } + +            val userChangeCallback = +                object : UserTracker.Callback { +                    override fun onUserChanged(newUser: Int, userContext: Context) { +                        trySendWithFailureLogging( +                            element = retrieveLauncherApps(), +                            loggingTag = TAG, +                            elementDescription = ON_USER_CHANGED, +                        ) +                    } +                } + +            userTracker.addCallback(userChangeCallback, bgExecutor) +            launcherApps.registerCallback(packageChangeCallback, bgHandler) + +            trySendWithFailureLogging( +                element = retrieveLauncherApps(), +                loggingTag = TAG, +                elementDescription = INITIAL_VALUE, +            ) + +            awaitClose { +                userTracker.removeCallback(userChangeCallback) +                launcherApps.unregisterCallback(packageChangeCallback) +            } +        } + +    private fun retrieveLauncherApps(): List<LauncherActivityInfo> { +        return launcherApps.getActivityList(/* packageName= */ null, userTracker.userHandle) +    } + +    private companion object { +        const val TAG = "UserVisibleAppsRepository" +        const val ON_PACKAGE_ADDED = "onPackageAdded" +        const val ON_PACKAGE_CHANGED = "onPackageChanged" +        const val ON_PACKAGE_REMOVED = "onPackageRemoved" +        const val ON_PACKAGES_AVAILABLE = "onPackagesAvailable" +        const val ON_PACKAGES_UNAVAILABLE = "onPackagesUnavailable" +        const val ON_USER_CHANGED = "onUserChanged" +        const val INITIAL_VALUE = "InitialValue" +    } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt index fc79c7ff118d..50ebbe497651 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt @@ -32,7 +32,7 @@ import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.Intra  import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection  import com.android.systemui.scene.shared.flag.SceneContainerFlag  import com.android.systemui.shade.ShadeDisplayAware -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor  import javax.inject.Inject  import kotlinx.coroutines.CoroutineScope  import kotlinx.coroutines.flow.StateFlow @@ -45,7 +45,7 @@ class KeyguardBlueprintInteractor  constructor(      private val keyguardBlueprintRepository: KeyguardBlueprintRepository,      @Application private val applicationScope: CoroutineScope, -    shadeInteractor: ShadeInteractor, +    shadeModeInteractor: ShadeModeInteractor,      @ShadeDisplayAware private val configurationInteractor: ConfigurationInteractor,      private val fingerprintPropertyInteractor: FingerprintPropertyInteractor,      private val smartspaceSection: SmartspaceSection, @@ -61,7 +61,7 @@ constructor(      /** Current BlueprintId */      val blueprintId = -        shadeInteractor.isShadeLayoutWide.map { isShadeLayoutWide -> +        shadeModeInteractor.isShadeLayoutWide.map { isShadeLayoutWide ->              val useSplitShade = isShadeLayoutWide && !SceneContainerFlag.isEnabled              when {                  useSplitShade -> SplitShadeKeyguardBlueprint.ID diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index d749e3c11378..e758768aa5e4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -73,7 +73,7 @@ import com.android.systemui.plugins.clocks.ThemeConfig  import com.android.systemui.plugins.clocks.WeatherData  import com.android.systemui.res.R  import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor  import com.android.systemui.shared.clocks.ClockRegistry  import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants  import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots @@ -113,7 +113,7 @@ constructor(      private val udfpsOverlayInteractor: UdfpsOverlayInteractor,      private val indicationController: KeyguardIndicationController,      @Assisted bundle: Bundle, -    private val shadeInteractor: ShadeInteractor, +    private val shadeModeInteractor: ShadeModeInteractor,      private val secureSettings: SecureSettings,      private val defaultShortcutsSection: DefaultShortcutsSection,      private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder, @@ -415,10 +415,7 @@ constructor(              setUpClock(previewContext, rootView)              if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {                  setUpSmartspace(previewContext, keyguardRootView) -                KeyguardPreviewSmartspaceViewBinder.bind( -                    keyguardRootView, -                    smartspaceViewModel, -                ) +                KeyguardPreviewSmartspaceViewBinder.bind(keyguardRootView, smartspaceViewModel)              }              KeyguardPreviewClockViewBinder.bind(                  keyguardRootView, @@ -591,7 +588,7 @@ constructor(      private fun getPreviewShadeLayoutWide(display: Display): Boolean {          return if (display.displayId == 0) { -            shadeInteractor.isShadeLayoutWide.value +            shadeModeInteractor.isShadeLayoutWide.value          } else {              // For the unfolded preview in a folded screen; it's landscape by default              // For the folded preview in an unfolded screen; it's portrait by default diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index e268050234ab..f717431f6a40 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -34,7 +34,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardSection  import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel  import com.android.systemui.res.R  import com.android.systemui.shade.ShadeDisplayAware -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor  import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore  import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder  import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker @@ -56,7 +56,7 @@ constructor(      private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,      private val systemBarUtilsState: SystemBarUtilsState,      private val rootViewModel: KeyguardRootViewModel, -    private val shadeInteractor: ShadeInteractor, +    private val shadeModeInteractor: ShadeModeInteractor,  ) : KeyguardSection() {      private var nicBindingDisposable: DisposableHandle? = null @@ -99,7 +99,7 @@ constructor(              context.resources.getDimensionPixelSize(customR.dimen.status_view_margin_horizontal)          val height = context.resources.getDimensionPixelSize(R.dimen.notification_shelf_height)          val isVisible = rootViewModel.isNotifIconContainerVisible.value -        val isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value +        val isShadeLayoutWide = shadeModeInteractor.isShadeLayoutWide.value          constraintSet.apply {              if (PromotedNotificationUiAod.isEnabled) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt index 2110c4027667..efdc5abf1f67 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt @@ -28,7 +28,7 @@ import androidx.constraintlayout.widget.ConstraintSet.TOP  import com.android.systemui.keyguard.shared.model.KeyguardSection  import com.android.systemui.res.R  import com.android.systemui.shade.ShadeDisplayAware -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor  import com.android.systemui.statusbar.notification.promoted.AODPromotedNotification  import com.android.systemui.statusbar.notification.promoted.PromotedNotificationLogger  import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod @@ -40,7 +40,7 @@ class AodPromotedNotificationSection  constructor(      @ShadeDisplayAware private val context: Context,      private val viewModelFactory: AODPromotedNotificationViewModel.Factory, -    private val shadeInteractor: ShadeInteractor, +    private val shadeModeInteractor: ShadeModeInteractor,      private val logger: PromotedNotificationLogger,  ) : KeyguardSection() {      var view: ComposeView? = null @@ -90,7 +90,7 @@ constructor(              context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons)          constraintSet.apply { -            val isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value +            val isShadeLayoutWide = shadeModeInteractor.isShadeLayoutWide.value              if (isShadeLayoutWide) {                  // When in split shade, align with top of smart space: diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt index f8425c16c341..5cc34e749b46 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt @@ -22,7 +22,7 @@ import com.android.systemui.dagger.SysUISingleton  import com.android.systemui.dagger.qualifiers.Application  import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor  import com.android.systemui.res.R -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor  import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController  import javax.inject.Inject  import kotlinx.coroutines.CoroutineScope @@ -40,7 +40,7 @@ constructor(      smartspaceController: LockscreenSmartspaceController,      keyguardClockViewModel: KeyguardClockViewModel,      smartspaceInteractor: KeyguardSmartspaceInteractor, -    shadeInteractor: ShadeInteractor, +    shadeModeInteractor: ShadeModeInteractor,  ) {      /** Whether the smartspace section is available in the build. */      val isSmartspaceEnabled: Boolean = smartspaceController.isEnabled @@ -91,7 +91,7 @@ constructor(      /* trigger clock and smartspace constraints change when smartspace appears */      val bcSmartspaceVisibility: StateFlow<Int> = smartspaceInteractor.bcSmartspaceVisibility -    val isShadeLayoutWide: StateFlow<Boolean> = shadeInteractor.isShadeLayoutWide +    val isShadeLayoutWide: StateFlow<Boolean> = shadeModeInteractor.isShadeLayoutWide      companion object {          fun getDateWeatherStartMargin(context: Context): Int { diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt index d6d185195c51..063ff15054e8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt @@ -267,7 +267,11 @@ private fun CardCarouselContent(          }          if (behavior.isCarouselDismissible) { -            SwipeToDismiss(content = { PagerContent() }, onDismissed = onDismissed) +            SwipeToDismiss( +                content = { PagerContent() }, +                isSwipingEnabled = isSwipingEnabled, +                onDismissed = onDismissed, +            )          } else {              val overscrollEffect = rememberOffsetOverscrollEffect()              SwipeToReveal( diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt index b80bf4143252..f044257bb343 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt @@ -17,99 +17,187 @@  package com.android.systemui.media.remedia.ui.compose  import androidx.compose.animation.core.Animatable -import androidx.compose.foundation.OverscrollEffect +import androidx.compose.foundation.gestures.Orientation  import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.overscroll  import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf  import androidx.compose.runtime.remember  import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue  import androidx.compose.ui.Modifier  import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.input.pointer.PointerType +import androidx.compose.ui.layout.layout  import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.util.fastCoerceIn  import androidx.compose.ui.util.fastRoundToInt +import com.android.compose.gesture.NestedDraggable +import com.android.compose.gesture.effect.rememberOffsetOverscrollEffect +import com.android.compose.gesture.nestedDraggable +import kotlinx.coroutines.CoroutineScope  import kotlinx.coroutines.launch  /** Swipe to dismiss that supports nested scrolling. */  @Composable  fun SwipeToDismiss( -    content: @Composable (overscrollEffect: OverscrollEffect?) -> Unit, +    content: @Composable () -> Unit, +    isSwipingEnabled: Boolean,      onDismissed: () -> Unit,      modifier: Modifier = Modifier,  ) { -    val scope = rememberCoroutineScope() -    val offsetAnimatable = remember { Animatable(0f) } +    val overscrollEffect = rememberOffsetOverscrollEffect() -    // This is the width of the revealed content UI box. It's not a state because it's not -    // observed in any composition and is an object with a value to avoid the extra cost -    // associated with boxing and unboxing an int. -    val revealedContentBoxWidth = remember { +    // This is the width of the content UI box. It's not a state because it's not observed in any +    // composition and is an object with a value to avoid the extra cost associated with boxing and +    // unboxing an int. +    val contentBoxWidth = remember {          object {              var value = 0          }      } -    val nestedScrollConnection = remember { -        object : NestedScrollConnection { -            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { -                return if (offsetAnimatable.value > 0f && available.x < 0f) { -                    scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } -                    Offset(available.x, 0f) -                } else if (offsetAnimatable.value < 0f && available.x > 0f) { -                    scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } -                    Offset(available.x, 0f) -                } else { -                    Offset.Zero -                } -            } - -            override fun onPostScroll( -                consumed: Offset, -                available: Offset, -                source: NestedScrollSource, -            ): Offset { -                return if (available.x > 0f) { -                    scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } -                    Offset(available.x, 0f) -                } else if (available.x < 0f) { -                    scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } -                    Offset(available.x, 0f) -                } else { -                    Offset.Zero -                } -            } +    // In order to support the drag to dismiss, infrastructure has to be put in place where a +    // NestedDraggable helps by consuming the unconsumed drags and flings and applying the offset. +    // +    // This is the NestedDraggalbe controller. +    val dragController = +        rememberDismissibleContentDragController( +            maxBound = { contentBoxWidth.value.toFloat() }, +            onDismissed = onDismissed, +        ) -            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { -                scope.launch { -                    offsetAnimatable.animateTo( -                        if (offsetAnimatable.value >= revealedContentBoxWidth.value / 2f) { -                            revealedContentBoxWidth.value * 2f -                        } else if (offsetAnimatable.value <= -revealedContentBoxWidth.value / 2f) { -                            -revealedContentBoxWidth.value * 2f -                        } else { -                            0f -                        } -                    ) -                    if (offsetAnimatable.value != 0f) { -                        onDismissed() +    Box( +        modifier = +            modifier +                .layout { measurable, constraints -> +                    val placeable = measurable.measure(constraints) +                    contentBoxWidth.value = placeable.measuredWidth +                    layout(placeable.measuredWidth, placeable.measuredHeight) { +                        placeable.place(0, 0)                      }                  } -                return super.onPostFling(consumed, available) -            } +                .nestedDraggable( +                    enabled = isSwipingEnabled, +                    draggable = +                        remember { +                            object : NestedDraggable { +                                override fun onDragStarted( +                                    position: Offset, +                                    sign: Float, +                                    pointersDown: Int, +                                    pointerType: PointerType?, +                                ): NestedDraggable.Controller { +                                    return dragController +                                } + +                                override fun shouldConsumeNestedPostScroll(sign: Float): Boolean { +                                    return dragController.shouldConsumePostScrolls(sign) +                                } + +                                override fun shouldConsumeNestedPreScroll(sign: Float): Boolean { +                                    return dragController.shouldConsumePreScrolls(sign) +                                } +                            } +                        }, +                    orientation = Orientation.Horizontal, +                ) +                .overscroll(overscrollEffect) +                .absoluteOffset { IntOffset(dragController.offset.fastRoundToInt(), y = 0) } +    ) { +        content() +    } +} + +@Composable +private fun rememberDismissibleContentDragController( +    maxBound: () -> Float, +    onDismissed: () -> Unit, +): DismissibleContentDragController { +    val scope = rememberCoroutineScope() +    return remember { +        DismissibleContentDragController( +            scope = scope, +            maxBound = maxBound, +            onDismissed = onDismissed, +        ) +    } +} + +private class DismissibleContentDragController( +    private val scope: CoroutineScope, +    private val maxBound: () -> Float, +    private val onDismissed: () -> Unit, +) : NestedDraggable.Controller { +    private val offsetAnimatable = Animatable(0f) +    private var lastTarget = 0f +    private var range = 0f..1f +    private var shouldConsumePreScrolls by mutableStateOf(false) + +    override val autoStopNestedDrags: Boolean +        get() = true + +    val offset: Float +        get() = offsetAnimatable.value + +    fun shouldConsumePreScrolls(sign: Float): Boolean { +        if (!shouldConsumePreScrolls) return false + +        if (lastTarget > 0f && sign < 0f) { +            range = 0f..maxBound() +            return true          } + +        if (lastTarget < 0f && sign > 0f) { +            range = -maxBound()..0f +            return true +        } + +        return false      } -    Box( -        modifier = -            modifier -                .onSizeChanged { revealedContentBoxWidth.value = it.width } -                .nestedScroll(nestedScrollConnection) -                .offset { IntOffset(x = offsetAnimatable.value.fastRoundToInt(), y = 0) } -    ) { -        content(null) +    fun shouldConsumePostScrolls(sign: Float): Boolean { +        val max = maxBound() +        if (sign > 0f && lastTarget < max) { +            range = 0f..maxBound() +            return true +        } + +        if (sign < 0f && lastTarget > -max) { +            range = -maxBound()..0f +            return true +        } + +        return false +    } + +    override fun onDrag(delta: Float): Float { +        val previousTarget = lastTarget +        lastTarget = (lastTarget + delta).fastCoerceIn(range.start, range.endInclusive) +        val newTarget = lastTarget +        scope.launch { offsetAnimatable.snapTo(newTarget) } +        return lastTarget - previousTarget +    } + +    override suspend fun onDragStopped(velocity: Float, awaitFling: suspend () -> Unit): Float { +        val rangeMiddle = range.start + (range.endInclusive - range.start) / 2f +        lastTarget = +            when { +                lastTarget >= rangeMiddle -> range.endInclusive +                else -> range.start +            } + +        shouldConsumePreScrolls = lastTarget != 0f +        val newTarget = lastTarget + +        scope.launch { +            offsetAnimatable.animateTo(newTarget) +            if (newTarget != 0f) { +                onDismissed() +            } +        } +        return velocity      }  } diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt index 770762c7a29f..1d7b79d9a07a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.gestures.Orientation  import androidx.compose.foundation.layout.Box  import androidx.compose.foundation.layout.BoxScope  import androidx.compose.foundation.layout.absoluteOffset -import androidx.compose.foundation.layout.offset  import androidx.compose.foundation.overscroll  import androidx.compose.foundation.withoutVisualEffect  import androidx.compose.runtime.Composable @@ -82,7 +81,7 @@ fun SwipeToReveal(      // overscroll visual effect.      //      // This is the NestedDraggalbe controller. -    val revealedContentDragController = rememberRevealedContentDragController { +    val revealedContentDragController = rememberDismissibleContentDragController {          revealedContentBoxWidth.value.toFloat()      } @@ -186,7 +185,7 @@ fun SwipeToReveal(  }  @Composable -private fun rememberRevealedContentDragController( +private fun rememberDismissibleContentDragController(      maxBound: () -> Float  ): RevealedContentDragController {      val scope = rememberCoroutineScope() diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java index ad0acbdaf702..8bc3203ea51e 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java @@ -502,10 +502,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements                  boolean mHasBlurs;                  @Override -                public void onWallpaperZoomOutChanged(float zoomOut) { -                } - -                @Override                  public void onBlurRadiusChanged(int radius) {                      boolean hasBlurs = radius != 0;                      if (hasBlurs == mHasBlurs) { diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt index 18b4b7d2b5cf..465c78e91e53 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt @@ -23,6 +23,7 @@ import com.android.systemui.lifecycle.Hydrator  import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor  import com.android.systemui.scene.domain.interactor.SceneInteractor  import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor  import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel  import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor  import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor @@ -50,6 +51,7 @@ constructor(      val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,      val sceneInteractor: SceneInteractor,      private val shadeInteractor: ShadeInteractor, +    shadeModeInteractor: ShadeModeInteractor,      disableFlagsInteractor: DisableFlagsInteractor,      mediaCarouselInteractor: MediaCarouselInteractor,      activeNotificationsInteractor: ActiveNotificationsInteractor, @@ -62,13 +64,13 @@ constructor(              traceName = "showClock",              initialValue =                  shouldShowClock( -                    isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value, +                    isShadeLayoutWide = shadeModeInteractor.isShadeLayoutWide.value,                      areAnyNotificationsPresent =                          activeNotificationsInteractor.areAnyNotificationsPresentValue,                  ),              source =                  combine( -                    shadeInteractor.isShadeLayoutWide, +                    shadeModeInteractor.isShadeLayoutWide,                      activeNotificationsInteractor.areAnyNotificationsPresent,                      this::shouldShowClock,                  ), diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt index bd7796118038..de1b180f5a7a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt @@ -47,7 +47,7 @@ constructor(          scope.launchTraced("ShadeStateTraceLogger") {              launch {                  val stateLogger = createTraceStateLogger("isShadeLayoutWide") -                shadeInteractor.isShadeLayoutWide.collect { stateLogger.log(it.toString()) } +                shadeModeInteractor.isShadeLayoutWide.collect { stateLogger.log(it.toString()) }              }              launch {                  val stateLogger = createTraceStateLogger("shadeMode") diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index 6d68796454eb..b54b518ffbd6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -56,15 +56,6 @@ interface ShadeInteractor : BaseShadeInteractor {      /** Whether the shade can be expanded from QQS to QS. */      val isExpandToQsEnabled: Flow<Boolean> - -    /** -     * Whether the shade layout should be wide (true) or narrow (false). -     * -     * In a wide layout, notifications and quick settings each take up only half the screen width -     * (whether they are shown at the same time or not). In a narrow layout, they can each be as -     * wide as the entire screen. -     */ -    val isShadeLayoutWide: StateFlow<Boolean>  }  /** ShadeInteractor methods with implementations that differ between non-empty impls. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt index 77e6a833c153..4154e2ca281b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt @@ -46,7 +46,6 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor {      override val isUserInteracting: StateFlow<Boolean> = inactiveFlowBoolean      override val isShadeTouchable: Flow<Boolean> = inactiveFlowBoolean      override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean -    override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean      override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {} diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index e8b5d5bdf7df..fb3fc524536d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -55,11 +55,7 @@ constructor(      userSetupRepository: UserSetupRepository,      userSwitcherInteractor: UserSwitcherInteractor,      private val baseShadeInteractor: BaseShadeInteractor, -    shadeModeInteractor: ShadeModeInteractor, -) : -    ShadeInteractor, -    BaseShadeInteractor by baseShadeInteractor, -    ShadeModeInteractor by shadeModeInteractor { +) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor {      override val isShadeEnabled: StateFlow<Boolean> =          disableFlagsInteractor.disableFlags              .map { it.isShadeEnabled() } @@ -127,8 +123,4 @@ constructor(                  disableFlags.isQuickSettingsEnabled() &&                  !isDozing          } - -    companion object { -        private const val TAG = "ShadeInteractor" -    }  } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt index c264e3525026..d03f09175b04 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt @@ -99,12 +99,12 @@ constructor(              traceName = "showClock",              initialValue =                  shouldShowClock( -                    isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value, +                    isShadeLayoutWide = shadeModeInteractor.isShadeLayoutWide.value,                      overlays = sceneInteractor.currentOverlays.value,                  ),              source =                  combine( -                    shadeInteractor.isShadeLayoutWide, +                    shadeModeInteractor.isShadeLayoutWide,                      sceneInteractor.currentOverlays,                      ::shouldShowClock,                  ), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index d2f424a46620..ba446837a72e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -74,7 +74,7 @@ constructor(      private val singleShadeOverScrollerFactory: SingleShadeLockScreenOverScroller.Factory,      private val activityStarter: ActivityStarter,      wakefulnessLifecycle: WakefulnessLifecycle, -    configurationController: ConfigurationController, +    @ShadeDisplayAware configurationController: ConfigurationController,      falsingManager: FalsingManager,      dumpManager: DumpManager,      qsTransitionControllerFactory: LockscreenShadeQsTransitionController.Factory, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java index 2030606e4274..72ece3db307b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java @@ -73,7 +73,7 @@ public class NotificationGroupingUtil {                  }                  return null;              } else { -                return row.getEntry().getSbn().getNotification(); +                return row.getEntryLegacy().getSbn().getNotification();              }          }      }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index e292bcf1f7a8..f844d1da1a8d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -342,7 +342,6 @@ constructor(              keyguardInteractor.setZoomOut(zoomOutFromShadeRadius)          }          listeners.forEach { -            it.onWallpaperZoomOutChanged(zoomOutFromShadeRadius)              it.onBlurRadiusChanged(appliedBlurRadius)          }          notificationShadeWindowController.setBackgroundBlurRadius(appliedBlurRadius) @@ -761,9 +760,6 @@ constructor(      /** Invoked when changes are needed in z-space */      interface DepthListener { -        /** Current wallpaper zoom out, where 0 is the closest, and 1 the farthest */ -        fun onWallpaperZoomOutChanged(zoomOut: Float) -          fun onBlurRadiusChanged(blurRadius: Int) {}      }  } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt index df8fb5e75368..afbec7f356b2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt @@ -71,46 +71,49 @@ constructor(                  Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT              else if (entry.ranking.isConversation)                  Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL -            else -                Notification.MessagingStyle.CONVERSATION_TYPE_LEGACY +            else Notification.MessagingStyle.CONVERSATION_TYPE_LEGACY          entry.ranking.conversationShortcutInfo?.let { shortcutInfo ->              logger.logAsyncTaskProgress(entry.logKey, "getting shortcut icon")              messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo)              shortcutInfo.label?.let { label -> messagingStyle.conversationTitle = label }          } -        if (NmSummarizationUiFlag.isEnabled && !TextUtils.isEmpty(entry.ranking.summarization)) { -            val icon = context.getDrawable(R.drawable.ic_notification_summarization)?.mutate() -            val imageSpan = -                icon?.let { -                    it.setBounds( -                        /* left= */ 0, -                        /* top= */ 0, -                        icon.getIntrinsicWidth(), -                        icon.getIntrinsicHeight(), -                    ) -                    ImageSpan(it, ImageSpan.ALIGN_CENTER) -                } -            val decoratedSummary = -                SpannableString("x " + entry.ranking.summarization).apply { -                    setSpan( -                        /* what = */ imageSpan, -                        /* start = */ 0, -                        /* end = */ 1, -                        /* flags = */ Spanned.SPAN_INCLUSIVE_EXCLUSIVE, -                    ) -                    entry.ranking.summarization?.let { +        if (NmSummarizationUiFlag.isEnabled) { +            if (!TextUtils.isEmpty(entry.ranking.summarization)) { +                val icon = context.getDrawable(R.drawable.ic_notification_summarization)?.mutate() +                val imageSpan = +                    icon?.let { +                        it.setBounds( +                            /* left= */ 0, +                            /* top= */ 0, +                            icon.getIntrinsicWidth(), +                            icon.getIntrinsicHeight(), +                        ) +                        ImageSpan(it, ImageSpan.ALIGN_CENTER) +                    } +                val decoratedSummary = +                    SpannableString("x " + entry.ranking.summarization).apply {                          setSpan( -                            /* what = */ StyleSpan(Typeface.ITALIC), -                            /* start = */ 2, -                            /* end = */ it.length + 2, -                            /* flags = */ Spanned.SPAN_EXCLUSIVE_INCLUSIVE, +                            /* what = */ imageSpan, +                            /* start = */ 0, +                            /* end = */ 1, +                            /* flags = */ Spanned.SPAN_INCLUSIVE_EXCLUSIVE,                          ) +                        entry.ranking.summarization?.let { +                            setSpan( +                                /* what = */ StyleSpan(Typeface.ITALIC), +                                /* start = */ 2, +                                /* end = */ it.length + 2, +                                /* flags = */ Spanned.SPAN_EXCLUSIVE_INCLUSIVE, +                            ) +                        }                      } -                } -            entry.sbn.notification.extras.putCharSequence( -                EXTRA_SUMMARIZED_CONTENT, -                decoratedSummary, -            ) +                entry.sbn.notification.extras.putCharSequence( +                    EXTRA_SUMMARIZED_CONTENT, +                    decoratedSummary, +                ) +            } else { +                entry.sbn.notification.extras.putCharSequence(EXTRA_SUMMARIZED_CONTENT, null) +            }          }          messagingStyle.unreadMessageCount = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS index ba4001014681..e19ffb4f2018 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS @@ -17,4 +17,4 @@ valiiftime@google.com  yurilin@google.com  per-file MediaNotificationProcessor.java = ethibodeau@google.com -per-file MagicActionBackgroundDrawable.kt = dupin@google.com +per-file AnimatedActionBackgroundDrawable.kt = dupin@google.com diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt index 68c13afe82dc..7b0d90ca3b60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt @@ -24,14 +24,19 @@ import com.android.internal.dynamicanimation.animation.SpringForce  import com.android.systemui.res.R  import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator.Companion.createDefaultSpring  import com.android.systemui.statusbar.notification.stack.AnimationProperties +import kotlin.math.sign  /**   * A physically animatable property of a view.   *   * @param tag the view tag to safe this property in   * @param property the property to animate. + * @param avoidDoubleOvershoot should this property avoid double overshoot when animated   */ -data class PhysicsProperty(val tag: Int, val property: Property<View, Float>) { +data class PhysicsProperty +@JvmOverloads constructor( +    val tag: Int, val property: Property<View, Float>, val avoidDoubleOvershoot: Boolean = true +) {      val offsetProperty =          object : FloatProperty<View>(property.name) {              override fun get(view: View): Float { @@ -61,6 +66,8 @@ data class PropertyData(      var offset: Float = 0f,      var animator: SpringAnimation? = null,      var delayRunnable: Runnable? = null, +    var startOffset: Float = 0f, +    var doubleOvershootAvoidingListener: DynamicAnimation.OnAnimationUpdateListener? = null  )  /** @@ -140,30 +147,67 @@ private fun startAnimation(      if (animator == null) {          animator = SpringAnimation(view, animatableProperty.offsetProperty)          propertyData.animator = animator -        animator.setSpring(createDefaultSpring())          val listener = properties?.getAnimationEndListener(animatableProperty.property)          if (listener != null) {              animator.addEndListener(listener) -            // We always notify things as started even if we have a delay -            properties.getAnimationStartListener(animatableProperty.property)?.accept(animator)          } +        // We always notify things as started even if we have a delay +        properties?.getAnimationStartListener(animatableProperty.property)?.accept(animator)          // remove the tag when the animation is finished -        animator.addEndListener { _, _, _, _ -> propertyData.animator = null } +        animator.addEndListener { _, _, _, _ -> +            propertyData.animator = null +            propertyData.doubleOvershootAvoidingListener = null +            // Let's make sure we never get stuck with an offset even when canceling +            // We never actually cancel running animations but keep it around, so this only +            // triggers if things really should end. +            propertyData.offset = 0f +        } +    } +    if (animatableProperty.avoidDoubleOvershoot +        && propertyData.doubleOvershootAvoidingListener == null) { +        propertyData.doubleOvershootAvoidingListener = +            DynamicAnimation.OnAnimationUpdateListener { _, offset: Float, velocity: Float -> +                val isOscillatingBackwards = velocity.sign == propertyData.startOffset.sign +                val didAlreadyRemoveBounciness = +                    animator.spring.dampingRatio == SpringForce.DAMPING_RATIO_NO_BOUNCY +                val isOvershooting = offset.sign != propertyData.startOffset.sign +                if (isOvershooting && isOscillatingBackwards && !didAlreadyRemoveBounciness) { +                    // our offset is starting to decrease, let's remove all overshoot +                    animator.spring.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) +                } else if (!isOvershooting +                    && (didAlreadyRemoveBounciness || isOscillatingBackwards)) { +                    // we already did overshoot, let's skip to the end to avoid oscillations. +                    // Usually we shouldn't hit this as setting the damping ratio avoid overshoots +                    // but it may still happen if we see jank +                    animator.skipToEnd(); +                } +            } +        animator.addUpdateListener(propertyData.doubleOvershootAvoidingListener) +    } else if (!animatableProperty.avoidDoubleOvershoot +        && propertyData.doubleOvershootAvoidingListener != null) { +        animator.removeUpdateListener(propertyData.doubleOvershootAvoidingListener)      } +    // reset a new spring as it may have been modified +    animator.setSpring(createDefaultSpring().setFinalPosition(0f))      // TODO(b/393581344): look at custom spring      endListener?.let { animator.addEndListener(it) } -    val newOffset = previousEndValue - newEndValue + propertyData.offset -    // Immedialely set the new offset that compensates for the immediate end value change -    propertyData.offset = newOffset -    property.set(view, newEndValue + newOffset) +    val startOffset = previousEndValue - newEndValue + propertyData.offset +    // Immediately set the new offset that compensates for the immediate end value change +    propertyData.offset = startOffset +    propertyData.startOffset = startOffset +    property.set(view, newEndValue + startOffset)      // cancel previous starters still pending      view.removeCallbacks(propertyData.delayRunnable) -    animator.setStartValue(newOffset) +    animator.setStartValue(startOffset)      val startRunnable = Runnable {          animator.animateToFinalPosition(0f)          propertyData.delayRunnable = null +        // When setting a new spring on a running animation it doesn't properly set the finish +        // conditions and will never actually end them only calling start explicitly does that, +        // so let's start them again! +        animator.start()      }      if (properties != null && properties.delay > 0 && !animator.isRunning) {          propertyData.delayRunnable = startRunnable diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 87733725e133..a40a2285d8a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator  import android.app.Notification  import android.app.Notification.GROUP_ALERT_SUMMARY +import android.app.NotificationChannel.SYSTEM_RESERVED_IDS  import android.util.ArrayMap  import android.util.ArraySet  import com.android.internal.annotations.VisibleForTesting @@ -48,7 +49,10 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager  import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener  import com.android.systemui.statusbar.notification.headsup.PinnedStatus  import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionLogger  import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProviderImpl.DecisionImpl +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType  import com.android.systemui.statusbar.notification.logKey  import com.android.systemui.statusbar.notification.row.NotificationActionClickManager  import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix @@ -81,6 +85,7 @@ class HeadsUpCoordinator  constructor(      @Application private val applicationScope: CoroutineScope,      private val mLogger: HeadsUpCoordinatorLogger, +    private val mInterruptLogger: VisualInterruptionDecisionLogger,      private val mSystemClock: SystemClock,      private val notifCollection: NotifCollection,      private val mHeadsUpManager: HeadsUpManager, @@ -290,6 +295,19 @@ constructor(                      return@forEach                  } +                if (isDisqualifiedChild(childToReceiveParentHeadsUp)) { +                    mInterruptLogger.logDecision( +                        VisualInterruptionType.PEEK.name, +                        childToReceiveParentHeadsUp, +                        DecisionImpl(shouldInterrupt = false, +                            logReason = "disqualified-transfer-target")) +                    postedEntries.forEach { +                        it.shouldHeadsUpEver = false +                        it.shouldHeadsUpAgain = false +                        handlePostedEntry(it, hunMutator, scenario = "disqualified-transfer-target") +                    } +                    return@forEach +                }                  // At this point we just need to initiate the transfer                  val summaryUpdate = mPostedEntries[logicalSummary.key] @@ -392,6 +410,14 @@ constructor(              cleanUpEntryTimes()          } +    private fun isDisqualifiedChild(entry: NotificationEntry): Boolean  { +        if (entry.channel == null || entry.channel.id == null) { +            return false +        } +        return entry.channel.id in SYSTEM_RESERVED_IDS +    } + +      /**       * Find the posted child with the newest when, and return it if it is isolated and has       * GROUP_ALERT_SUMMARY so that it can be heads uped. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt index 8240a0abaa9d..9f67e50ef920 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt @@ -87,7 +87,7 @@ constructor(          val eventLogData: EventLogData?      } -    private class DecisionImpl( +    class DecisionImpl(          override val shouldInterrupt: Boolean,          override val logReason: String,      ) : Decision diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java index 175512336b8e..bd06a375d5e6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java @@ -33,6 +33,7 @@ import android.service.notification.StatusBarNotification;  import com.android.internal.logging.UiEvent;  import com.android.internal.logging.UiEventLogger; +import com.android.systemui.statusbar.notification.collection.EntryAdapter;  import com.android.systemui.statusbar.notification.collection.NotificationEntry;  import com.android.systemui.statusbar.notification.logging.nano.Notifications;  import com.android.systemui.statusbar.notification.stack.PriorityBucket; @@ -64,6 +65,8 @@ public interface NotificationPanelLogger {       */      void logNotificationDrag(NotificationEntry draggedNotification); +    void logNotificationDrag(EntryAdapter draggedNotification); +      enum NotificationPanelEvent implements UiEventLogger.UiEventEnum {          @UiEvent(doc = "Notification panel shown from status bar.")          NOTIFICATION_PANEL_OPEN_STATUS_BAR(200), @@ -123,6 +126,42 @@ public interface NotificationPanelLogger {      }      /** +     * Composes a NotificationsList proto from the list of visible notifications. +     * @param visibleNotifications as provided by NotificationEntryManager.getVisibleNotifications() +     * @return NotificationList proto suitable for SysUiStatsLog.write(NOTIFICATION_PANEL_REPORTED) +     */ +    static Notifications.NotificationList adapterToNotificationProto( +            @Nullable List<EntryAdapter> visibleNotifications) { +        Notifications.NotificationList notificationList = new Notifications.NotificationList(); +        if (visibleNotifications == null) { +            return notificationList; +        } +        final Notifications.Notification[] proto_array = +                new Notifications.Notification[visibleNotifications.size()]; +        int i = 0; +        for (EntryAdapter ne : visibleNotifications) { +            final StatusBarNotification n = ne.getSbn(); +            if (n != null) { +                final Notifications.Notification proto = new Notifications.Notification(); +                proto.uid = n.getUid(); +                proto.packageName = n.getPackageName(); +                if (n.getInstanceId() != null) { +                    proto.instanceId = n.getInstanceId().getId(); +                } +                // TODO set np.groupInstanceId +                if (n.getNotification() != null) { +                    proto.isGroupSummary = n.getNotification().isGroupSummary(); +                } +                proto.section = toNotificationSection(ne.getSectionBucket()); +                proto_array[i] = proto; +            } +            ++i; +        } +        notificationList.notifications = proto_array; +        return notificationList; +    } + +    /**       * Maps PriorityBucket enum to Notification.SECTION constant. The two lists should generally       * use matching names, but the values may differ, because PriorityBucket order changes from       * time to time, while logs need to have stable meanings. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java index d7f7b760dd04..7eb74a4bf83d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.logging;  import static com.android.systemui.statusbar.notification.logging.NotificationPanelLogger.NotificationPanelEvent.NOTIFICATION_DRAG;  import com.android.systemui.shared.system.SysUiStatsLog; +import com.android.systemui.statusbar.notification.collection.EntryAdapter;  import com.android.systemui.statusbar.notification.collection.NotificationEntry;  import com.android.systemui.statusbar.notification.logging.nano.Notifications;  import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor; @@ -62,4 +63,15 @@ public class NotificationPanelLoggerImpl implements NotificationPanelLogger {                  /* num_notifications = */ proto.notifications.length,                  /* notifications = */ MessageNano.toByteArray(proto));      } + +    @Override +    public void logNotificationDrag(EntryAdapter draggedNotification) { +        final Notifications.NotificationList proto = +                NotificationPanelLogger.adapterToNotificationProto( +                Collections.singletonList(draggedNotification)); +        SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED, +                /* event_id = */ NOTIFICATION_DRAG.getId(), +                /* num_notifications = */ proto.notifications.length, +                /* notifications = */ MessageNano.toByteArray(proto)); +    }  } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt index fdbd75bd33ca..9282e166f605 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt @@ -16,13 +16,13 @@  package com.android.systemui.statusbar.notification.promoted -import android.app.Flags  import android.app.Flags.notificationsRedesignTemplates  import android.app.Notification  import android.content.Context  import android.graphics.PorterDuff  import android.util.Log  import android.view.LayoutInflater +import android.view.NotificationTopLineView  import android.view.View  import android.view.View.GONE  import android.view.View.MeasureSpec.AT_MOST @@ -202,7 +202,7 @@ private class FrameLayoutWithMaxHeight(maxHeight: Int, context: Context) : Frame  private val PromotedNotificationContentModel.layoutResource: Int?      get() { -        return if (Flags.notificationsRedesignTemplates()) { +        return if (notificationsRedesignTemplates()) {              when (style) {                  Style.Base -> R.layout.notification_2025_template_expanded_base                  Style.BigPicture -> R.layout.notification_2025_template_expanded_big_picture @@ -258,6 +258,7 @@ private class AODPromotedNotificationViewUpdater(root: View) {      private val time: DateTimeView? = root.findViewById(R.id.time)      private val timeDivider: TextView? = root.findViewById(R.id.time_divider)      private val title: TextView? = root.findViewById(R.id.title) +    private val topLine: NotificationTopLineView? = root.findViewById(R.id.notification_top_line)      private val verificationDivider: TextView? = root.findViewById(R.id.verification_divider)      private val verificationIcon: ImageView? = root.findViewById(R.id.verification_icon)      private val verificationText: TextView? = root.findViewById(R.id.verification_text) @@ -266,6 +267,29 @@ private class AODPromotedNotificationViewUpdater(root: View) {      private var oldProgressBar: ProgressBar? = null      private val newProgressBar = root.findViewById<View>(R.id.progress) as? NotificationProgressBar +    private val largeIconSizePx: Int = +        root.context.resources.getDimensionPixelSize(R.dimen.notification_right_icon_size) + +    private val endMarginPx: Int = +        if (notificationsRedesignTemplates()) { +            root.context.resources.getDimensionPixelSize(R.dimen.notification_2025_margin) +        } else { +            root.context.resources.getDimensionPixelSize( +                systemuiR.dimen.notification_shade_content_margin_horizontal +            ) +        } + +    private val imageEndMarginPx: Int +        get() = largeIconSizePx + 2 * endMarginPx + +    private val PromotedNotificationContentModel.imageEndMarginPxIfHasLargeIcon: Int +        get() = +            if (!skeletonLargeIcon.isNullOrEmpty()) { +                imageEndMarginPx +            } else { +                0 +            } +      init {          // Hide views that are never visible in the skeleton promoted notification.          alternateExpandTarget?.visibility = GONE @@ -283,13 +307,20 @@ private class AODPromotedNotificationViewUpdater(root: View) {              ?.mutate()              ?.setColorFilter(SecondaryText.colorInt, PorterDuff.Mode.SRC_IN) +        (rightIcon?.layoutParams as? MarginLayoutParams)?.let { +            it.marginEnd = endMarginPx +            rightIcon.layoutParams = it +        } +        bigText?.setImageEndMargin(largeIconSizePx + endMarginPx) +        text?.setImageEndMargin(largeIconSizePx + endMarginPx) +          setTextViewColor(appNameDivider, SecondaryText)          setTextViewColor(headerTextDivider, SecondaryText)          setTextViewColor(headerTextSecondaryDivider, SecondaryText)          setTextViewColor(timeDivider, SecondaryText)          setTextViewColor(verificationDivider, SecondaryText) -        if (Flags.notificationsRedesignTemplates()) { +        if (notificationsRedesignTemplates()) {              (mainColumn?.layoutParams as? MarginLayoutParams)?.let { mainColumnMargins ->                  mainColumnMargins.topMargin =                      Notification.Builder.getContentMarginTop( @@ -315,19 +346,15 @@ private class AODPromotedNotificationViewUpdater(root: View) {      private fun updateBase(          content: PromotedNotificationContentModel, -        textView: ImageFloatingTextView? = null, -        showOldProgress: Boolean = true, +        textView: ImageFloatingTextView? = text,      ) { -        updateHeader(content, hideTitle = true) +        updateHeader(content)          updateTitle(title, content) -        updateText(textView ?: text, content) +        updateText(textView, content)          updateSmallIcon(icon, content)          updateImageView(rightIcon, content.skeletonLargeIcon) - -        if (showOldProgress) { -            updateOldProgressBar(content) -        } +        updateOldProgressBar(content)      }      private fun updateBigPictureStyle(content: PromotedNotificationContentModel) { @@ -345,14 +372,15 @@ private class AODPromotedNotificationViewUpdater(root: View) {      }      private fun updateProgressStyle(content: PromotedNotificationContentModel) { -        updateBase(content, showOldProgress = false) +        updateBase(content)          updateNewProgressBar(content)      }      private fun updateOldProgressBar(content: PromotedNotificationContentModel) {          if ( -            content.oldProgress == null || +            content.style == Style.Progress || +                content.oldProgress == null ||                  content.oldProgress.max == 0 ||                  content.oldProgress.isIndeterminate          ) { @@ -381,27 +409,24 @@ private class AODPromotedNotificationViewUpdater(root: View) {          }      } -    private fun updateHeader( -        content: PromotedNotificationContentModel, -        hideTitle: Boolean = false, -    ) { +    private fun updateHeader(content: PromotedNotificationContentModel) {          updateAppName(content)          updateTextView(headerTextSecondary, content.subText) -        if (!hideTitle) { -            updateTitle(headerText, content) -        } +        // Not calling updateTitle(headerText, content) because the title is always a separate +        // element in the expanded layout used for AOD RONs.          updateTimeAndChronometer(content) -        updateHeaderDividers(content, hideTitle = hideTitle) +        updateHeaderDividers(content) + +        updateTopLine(content)      } -    private fun updateHeaderDividers( -        content: PromotedNotificationContentModel, -        hideTitle: Boolean = false, -    ) { -        val hasAppName = content.appName != null && content.appName.isNotEmpty() -        val hasSubText = content.subText != null && content.subText.isNotEmpty() -        val hasHeader = content.title != null && content.title.isNotEmpty() && !hideTitle +    private fun updateHeaderDividers(content: PromotedNotificationContentModel) { +        val hasAppName = content.appName != null +        val hasSubText = content.subText != null +        // Not setting hasHeader = content.title because the title is always a separate element in +        // the expanded layout used for AOD RONs. +        val hasHeader = false          val hasTimeOrChronometer = content.time != null          val hasTextBeforeSubText = hasAppName @@ -418,26 +443,27 @@ private class AODPromotedNotificationViewUpdater(root: View) {      }      private fun updateConversationHeader(content: PromotedNotificationContentModel) { -        updateTitle(conversationText, content)          updateAppName(content)          updateTimeAndChronometer(content) -        updateConversationHeaderDividers(content, hideTitle = true) -          updateImageView(verificationIcon, content.verificationIcon)          updateTextView(verificationText, content.verificationText) +        updateConversationHeaderDividers(content) + +        updateTopLine(content) +          updateSmallIcon(conversationIcon, content) +        updateTitle(conversationText, content)      } -    private fun updateConversationHeaderDividers( -        content: PromotedNotificationContentModel, -        hideTitle: Boolean = false, -    ) { -        val hasTitle = content.title != null && !hideTitle +    private fun updateConversationHeaderDividers(content: PromotedNotificationContentModel) { +        // Not setting hasTitle = content.title because the title is always a separate element in +        // the expanded layout used for AOD RONs. +        val hasTitle = false          val hasAppName = content.appName != null          val hasTimeOrChronometer = content.time != null          val hasVerification = -            !content.verificationIcon.isNullOrEmpty() || !content.verificationText.isNullOrEmpty() +            !content.verificationIcon.isNullOrEmpty() || content.verificationText != null          val hasTextBeforeAppName = hasTitle          val hasTextBeforeTime = hasTitle || hasAppName @@ -457,6 +483,10 @@ private class AODPromotedNotificationViewUpdater(root: View) {      }      private fun updateTitle(titleView: TextView?, content: PromotedNotificationContentModel) { +        (titleView?.layoutParams as? MarginLayoutParams)?.let { +            it.marginEnd = content.imageEndMarginPxIfHasLargeIcon +            titleView.layoutParams = it +        }          updateTextView(titleView, content.title, color = PrimaryText)      } @@ -505,6 +535,10 @@ private class AODPromotedNotificationViewUpdater(root: View) {          chronometer?.appendFontFeatureSetting("tnum")      } +    private fun updateTopLine(content: PromotedNotificationContentModel) { +        topLine?.headerTextMarginEnd = content.imageEndMarginPxIfHasLargeIcon +    } +      private fun inflateOldProgressBar() {          if (oldProgressBar != null) {              return @@ -518,7 +552,8 @@ private class AODPromotedNotificationViewUpdater(root: View) {          view: ImageFloatingTextView?,          content: PromotedNotificationContentModel,      ) { -        view?.setHasImage(false) +        view?.setHasImage(!content.skeletonLargeIcon.isNullOrEmpty()) +        view?.setNumIndentLines(if (content.title != null) 0 else 1)          updateTextView(view, content.text)      } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt index a8a7e885d1f7..d9bdfbc81145 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt @@ -183,9 +183,10 @@ constructor(      private fun Notification.smallIconModel(imageModelProvider: ImageModelProvider): ImageModel? =          imageModelProvider.getImageModel(smallIcon, SmallSquare) -    private fun Notification.title(): CharSequence? = extras?.getCharSequence(EXTRA_TITLE) +    private fun Notification.title(): CharSequence? = getCharSequenceExtraUnlessEmpty(EXTRA_TITLE) -    private fun Notification.bigTitle(): CharSequence? = extras?.getCharSequence(EXTRA_TITLE_BIG) +    private fun Notification.bigTitle(): CharSequence? = +        getCharSequenceExtraUnlessEmpty(EXTRA_TITLE_BIG)      private fun Notification.callPerson(): Person? =          extras?.getParcelable(EXTRA_CALL_PERSON, Person::class.java) @@ -200,9 +201,10 @@ constructor(          } ?: title()      } -    private fun Notification.text(): CharSequence? = extras?.getCharSequence(EXTRA_TEXT) +    private fun Notification.text(): CharSequence? = getCharSequenceExtraUnlessEmpty(EXTRA_TEXT) -    private fun Notification.bigText(): CharSequence? = extras?.getCharSequence(EXTRA_BIG_TEXT) +    private fun Notification.bigText(): CharSequence? = +        getCharSequenceExtraUnlessEmpty(EXTRA_BIG_TEXT)      private fun Notification.text(style: Notification.Style?): CharSequence? {          return when (style) { @@ -211,17 +213,17 @@ constructor(          } ?: text()      } -    private fun Notification.subText(): String? = extras?.getString(EXTRA_SUB_TEXT) +    private fun Notification.subText(): String? = getStringExtraUnlessEmpty(EXTRA_SUB_TEXT)      private fun Notification.shortCriticalText(): String? {          if (!android.app.Flags.apiRichOngoing()) {              return null          } -        if (this.shortCriticalText != null) { -            return this.shortCriticalText +        if (shortCriticalText != null) { +            return shortCriticalText          }          if (Flags.promoteNotificationsAutomatically()) { -            return this.extras?.getString(EXTRA_AUTOMATICALLY_EXTRACTED_SHORT_CRITICAL_TEXT) +            return getStringExtraUnlessEmpty(EXTRA_AUTOMATICALLY_EXTRACTED_SHORT_CRITICAL_TEXT)          }          return null      } @@ -277,7 +279,7 @@ constructor(          }      private fun Notification.verificationText(): CharSequence? = -        extras.getCharSequence(EXTRA_VERIFICATION_TEXT) +        getCharSequenceExtraUnlessEmpty(EXTRA_VERIFICATION_TEXT)      private fun Notification.Builder.extractStyleContent(          notification: Notification, @@ -344,3 +346,11 @@ constructor(          contentBuilder.newProgress = createProgressModel(0xffffffff.toInt(), 0xff000000.toInt())      }  } + +private fun Notification.getCharSequenceExtraUnlessEmpty(key: String): CharSequence? = +    extras?.getCharSequence(key)?.takeUnlessEmpty() + +private fun Notification.getStringExtraUnlessEmpty(key: String): String? = +    extras?.getString(key)?.takeUnlessEmpty() + +private fun <T : CharSequence> T.takeUnlessEmpty(): T? = takeUnless { it.isEmpty() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AnimatedActionBackgroundDrawable.kt index a9ca6359b570..514e16557b65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AnimatedActionBackgroundDrawable.kt @@ -36,7 +36,7 @@ import com.android.wm.shell.shared.animation.Interpolators  import android.graphics.drawable.RippleDrawable  import androidx.core.content.ContextCompat -class MagicActionBackgroundDrawable( +class AnimatedActionBackgroundDrawable(      context: Context,  ) : RippleDrawable(      ContextCompat.getColorStateList( @@ -56,13 +56,13 @@ class BaseBackgroundDrawable(      context: Context,  ) : Drawable() { -    private val cornerRadius = context.resources.getDimension(R.dimen.magic_action_button_corner_radius) -    private val outlineStrokeWidth = context.resources.getDimension(R.dimen.magic_action_button_outline_stroke_width) +    private val cornerRadius = context.resources.getDimension(R.dimen.animated_action_button_corner_radius) +    private val outlineStrokeWidth = context.resources.getDimension(R.dimen.animated_action_button_outline_stroke_width)      private val insetVertical = 8 * context.resources.displayMetrics.density      private val buttonShape = Path()      // Color and style -    private val outlineStaticColor = context.getColor(R.color.magic_action_button_stroke_color) +    private val outlineStaticColor = context.getColor(R.color.animated_action_button_stroke_color)      private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {          val bgColor =              context.getColor( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionButton.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AnimatedActionButton.kt index d735360f1d4e..a1d75c69bf5b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionButton.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AnimatedActionButton.kt @@ -22,15 +22,15 @@ import android.util.AttributeSet  import android.widget.Button  /** - * Custom Button for Magic Action Button, which includes the custom background and foreground. + * Custom Button for Animated Action Button, which includes the custom background and foreground.   */  @SuppressLint("AppCompatCustomView") -class MagicActionButton @JvmOverloads constructor( +class AnimatedActionButton @JvmOverloads constructor(      context: Context,      attrs: AttributeSet? = null,      defStyleAttr: Int = 0,  ) : Button(context, attrs, defStyleAttr) {      init { -        background = MagicActionBackgroundDrawable(context) +        background = AnimatedActionBackgroundDrawable(context)      }  } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java index 20b826a3ca92..c04613af452a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java @@ -145,7 +145,11 @@ public class ExpandableNotificationRowDragController {                  | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);          if (result) {              // Log notification drag only if it succeeds -            mNotificationPanelLogger.logNotificationDrag(enr.getEntry()); +            if (NotificationBundleUi.isEnabled()) { +                mNotificationPanelLogger.logNotificationDrag(enr.getEntryAdapter()); +            } else { +                mNotificationPanelLogger.logNotificationDrag(enr.getEntryLegacy()); +            }              view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);              if (enr.isPinned()) {                  mHeadsUpManager.releaseAllImmediately(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java index 8cf9dd365b60..d1e6c6f335c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java @@ -30,6 +30,7 @@ import android.view.View;  import androidx.annotation.NonNull;  import com.android.app.animation.Interpolators; +import com.android.internal.dynamicanimation.animation.DynamicAnimation;  import com.android.systemui.res.R;  import com.android.systemui.statusbar.notification.PhysicsProperty;  import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator; @@ -199,15 +200,19 @@ public class ExpandableViewState extends ViewState {                  if (animateHeight) {                      expandableView.setActualHeightAnimating(true);                  } +                DynamicAnimation.OnAnimationEndListener endListener = null; +                if (!ViewState.isAnimating(expandableView, HEIGHT_PROPERTY)) { +                    // only Add the end listener if we haven't already +                    endListener = (animation, canceled, value, velocity) -> { +                        expandableView.setActualHeightAnimating(false); +                        if (!canceled && child instanceof ExpandableNotificationRow row) { +                            row.setGroupExpansionChanging(false /* isExpansionChanging */); +                        } +                    }; +                }                  PhysicsPropertyAnimator.setProperty(child, HEIGHT_PROPERTY, this.height, properties,                          animateHeight, -                        (animation, canceled, value, velocity) -> { -                            expandableView.setActualHeightAnimating(false); -                            if (!canceled && child instanceof ExpandableNotificationRow) { -                                ((ExpandableNotificationRow) child).setGroupExpansionChanging( -                                        false /* isExpansionChanging */); -                            } -                        }); +                        endListener);              } else {                  startHeightAnimationInterpolator(expandableView, properties);              } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java index 5414318b29bd..f0c03016a604 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java @@ -405,7 +405,7 @@ public class StackStateAnimator {              public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,                      float velocity) {                  mAnimatorSet.remove(animation); -                if (mAnimatorSet.isEmpty() && !canceled) { +                if (mAnimatorSet.isEmpty()) {                      onAnimationFinished();                  }                  mAnimationEndPool.push(this); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java index 29dbeb2c8ee4..2dc5a8127412 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java @@ -398,6 +398,14 @@ public class ViewState implements Dumpable {          return childTag != null;      } +    public static boolean isAnimating(View view, PhysicsProperty property) { +        Object childTag = getChildTag(view, property.getTag()); +        if (childTag instanceof PropertyData propertyData) { +            return propertyData.getAnimator() != null; +        } +        return childTag != null; +    } +      /**       * Start an animation to this viewstate       * @@ -680,13 +688,18 @@ public class ViewState implements Dumpable {      private void startYTranslationAnimation(final View child, AnimationProperties properties) {          if (mUsePhysicsForMovement) {              // Y Translation does some extra calls when it ends, so lets add a listener -            DynamicAnimation.OnAnimationEndListener endListener = -                    (animation, canceled, value, velocity) -> { -                        if (!canceled) { -                            HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(child, false); -                            onYTranslationAnimationFinished(child); -                        } -                    }; +            DynamicAnimation.OnAnimationEndListener endListener = null; +            if (!isAnimatingY(child)) { +                // Only add a listener if we're not animating yet +                endListener = +                        (animation, canceled, value, velocity) -> { +                            if (!canceled) { +                                HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(child, +                                        false); +                                onYTranslationAnimationFinished(child); +                            } +                        }; +            }              PhysicsPropertyAnimator.setProperty(child, Y_TRANSLATION, this.mYTranslation,                      properties, properties.getAnimationFilter().animateY, endListener);          } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 9d55e1d9d592..0ea9509f0c13 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -245,7 +245,7 @@ constructor(      val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =          if (SceneContainerFlag.isEnabled) {                  combine( -                    shadeInteractor.isShadeLayoutWide, +                    shadeModeInteractor.isShadeLayoutWide,                      shadeModeInteractor.shadeMode,                      configurationInteractor.onAnyConfigurationChange,                  ) { isShadeLayoutWide, shadeMode, _ -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt index 6d959be1c5f4..b33c2005479e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt @@ -397,13 +397,13 @@ constructor(          delayOnClickListener: Boolean,          packageContext: Context,      ): Button { -        val isMagicAction = -            Flags.notificationMagicActionsTreatment() && +        val isAnimatedAction = +            Flags.notificationAnimatedActionsTreatment() &&                  smartActions.fromAssistant && -                action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false) +                action.extras.getBoolean(Notification.Action.EXTRA_IS_ANIMATED, false)          val layoutRes = -            if (isMagicAction) { -                R.layout.magic_action_button +            if (isAnimatedAction) { +                R.layout.animated_action_button              } else {                  if (notificationsRedesignTemplates()) {                      R.layout.notification_2025_smart_action_button diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index bd3feadf4459..f36f765bb78b 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -537,8 +537,10 @@ public class ThemeOverlayController implements CoreStartable, Dumpable {              try {                  JSONObject object = new JSONObject(overlayPackageJson);                  String seedColorStr = Integer.toHexString(defaultSettings.seedColor.toArgb()); -                object.put(OVERLAY_CATEGORY_SYSTEM_PALETTE, seedColorStr); -                object.put(OVERLAY_CATEGORY_ACCENT_COLOR, seedColorStr); +                if(defaultSettings.colorSource == COLOR_SOURCE_PRESET){ +                    object.put(OVERLAY_CATEGORY_SYSTEM_PALETTE, seedColorStr); +                    object.put(OVERLAY_CATEGORY_ACCENT_COLOR, seedColorStr); +                }                  object.put(OVERLAY_COLOR_SOURCE, defaultSettings.colorSource);                  object.put(OVERLAY_CATEGORY_THEME_STYLE, Style.toString(mThemeStyle)); diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/Slider.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/Slider.kt index 54d2f79509c3..0b11da362a82 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/Slider.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/Slider.kt @@ -18,9 +18,9 @@  package com.android.systemui.volume.ui.compose.slider -import androidx.compose.animation.core.Animatable  import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring  import androidx.compose.foundation.gestures.Orientation  import androidx.compose.foundation.interaction.MutableInteractionSource  import androidx.compose.material3.ExperimentalMaterial3Api @@ -32,11 +32,11 @@ import androidx.compose.material3.SliderState  import androidx.compose.material3.VerticalSlider  import androidx.compose.runtime.Composable  import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State  import androidx.compose.runtime.getValue  import androidx.compose.runtime.mutableFloatStateOf  import androidx.compose.runtime.mutableStateOf  import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope  import androidx.compose.runtime.setValue  import androidx.compose.runtime.snapshotFlow  import androidx.compose.ui.Modifier @@ -53,14 +53,9 @@ import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel  import com.android.systemui.lifecycle.rememberViewModel  import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider  import kotlin.math.round -import kotlinx.coroutines.Job  import kotlinx.coroutines.flow.distinctUntilChanged  import kotlinx.coroutines.flow.filter  import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch - -private val defaultSpring = -    SpringSpec<Float>(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessHigh)  @Composable  fun Slider( @@ -87,55 +82,31 @@ fun Slider(      },  ) {      require(stepDistance >= 0) { "stepDistance must not be negative" } -    val coroutineScope = rememberCoroutineScope() -    val snappedValue = snapValue(value, valueRange, stepDistance) +    val snappedValue by valueState(value, isEnabled)      val hapticsViewModel = haptics.createViewModel(snappedValue, valueRange, interactionSource) -    val animatable = remember { Animatable(snappedValue) } -    var animationJob: Job? by remember { mutableStateOf(null) }      val sliderState =          remember(valueRange) { SliderState(value = snappedValue, valueRange = valueRange) }      val valueChange: (Float) -> Unit = { newValue ->          hapticsViewModel?.onValueChange(newValue) -        val snappedNewValue = snapValue(newValue, valueRange, stepDistance) -        if (animatable.targetValue != snappedNewValue) { -            onValueChanged(snappedNewValue) -            animationJob?.cancel() -            animationJob = -                coroutineScope.launch { -                    animatable.animateTo( -                        targetValue = snappedNewValue, -                        animationSpec = defaultSpring, -                    ) -                } -        } +        onValueChanged(newValue)      }      val semantics =          createSemantics(              accessibilityParams, -            animatable.targetValue, +            snappedValue,              valueRange,              valueChange,              isEnabled,              stepDistance,          ) -    LaunchedEffect(snappedValue) { -        if (!animatable.isRunning && animatable.targetValue != snappedValue) { -            animationJob?.cancel() -            animationJob = -                coroutineScope.launch { -                    animatable.animateTo(targetValue = snappedValue, animationSpec = defaultSpring) -                } -        } -    } -      sliderState.onValueChangeFinished = {          hapticsViewModel?.onValueChangeEnded() -        onValueChangeFinished?.invoke(animatable.targetValue) +        onValueChangeFinished?.invoke(snappedValue)      }      sliderState.onValueChange = valueChange -    sliderState.value = animatable.value +    sliderState.value = snappedValue      if (isVertical) {          VerticalSlider( @@ -161,16 +132,26 @@ fun Slider(      }  } -private fun snapValue( -    value: Float, -    valueRange: ClosedFloatingPointRange<Float>, -    stepDistance: Float, -): Float { -    if (stepDistance == 0f) { -        return value -    } -    val coercedValue = value.coerceIn(valueRange) -    return Math.round(coercedValue / stepDistance) * stepDistance +@Composable +private fun valueState(targetValue: Float, isEnabled: Boolean): State<Float> { +    var prevValue by remember { mutableFloatStateOf(targetValue) } +    var prevEnabled by remember { mutableStateOf(isEnabled) } +    // Don't animate slider value when receive the first value and when changing isEnabled state +    val value = +        if (prevEnabled != isEnabled) mutableFloatStateOf(targetValue) +        else +            animateFloatAsState( +                targetValue = targetValue, +                animationSpec = +                    spring( +                        dampingRatio = Spring.DampingRatioNoBouncy, +                        stiffness = Spring.StiffnessMedium, +                    ), +                label = "VolumeSliderValueAnimation", +            ) +    prevValue = targetValue +    prevEnabled = isEnabled +    return value  }  private fun createSemantics( diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java index 061f7984d44b..2898a02a1da8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -618,6 +618,8 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase {          when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);          mGlobalActionsDialogLite.showOrHideDialog(true, true, null /* view */,                  Display.DEFAULT_DISPLAY); +        // Clear the dismiss override so we don't have behavior after dismissing the dialog +        mGlobalActionsDialogLite.mDialog.setDismissOverride(null);          // Then smart lock will be disabled          verify(mLockPatternUtils).requireCredentialEntry(eq(expectedUser)); @@ -739,6 +741,8 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase {          // Show dialog with keyguard showing          mGlobalActionsDialogLite.showOrHideDialog(true, true, null, Display.DEFAULT_DISPLAY); +        // Clear the dismiss override so we don't have behavior after dismissing the dialog +        mGlobalActionsDialogLite.mDialog.setDismissOverride(null);          assertOneItemOfType(mGlobalActionsDialogLite.mItems,                  GlobalActionsDialogLite.SystemUpdateAction.class); @@ -764,12 +768,15 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase {          // Show dialog with keyguard showing          mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY); +        // Clear the dismiss override so we don't have behavior after dismissing the dialog +        mGlobalActionsDialogLite.mDialog.setDismissOverride(null);          assertNoItemsOfType(mGlobalActionsDialogLite.mItems,                  GlobalActionsDialogLite.SystemUpdateAction.class);          // Hide dialog          mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY); +      }      @Test @@ -783,6 +790,8 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase {          mGlobalActionsDialogLite.showOrHideDialog(false, true, null, Display.DEFAULT_DISPLAY);          mTestableLooper.processAllMessages(); +        // Clear the dismiss override so we don't have behavior after dismissing the dialog +        mGlobalActionsDialogLite.mDialog.setDismissOverride(null);          assertThat(mGlobalActionsDialogLite.mDialog.isShowing()).isTrue(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/cursorposition/domain/data/repository/TestCursorPositionRepositoryInstanceProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/cursorposition/domain/data/repository/TestCursorPositionRepositoryInstanceProvider.kt new file mode 100644 index 000000000000..9f998f3a4e62 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/cursorposition/domain/data/repository/TestCursorPositionRepositoryInstanceProvider.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.cursorposition.domain.data.repository + +import android.os.Handler +import com.android.app.displaylib.PerDisplayInstanceProviderWithTeardown +import com.android.systemui.cursorposition.data.repository.InputEventListenerBuilder +import com.android.systemui.cursorposition.data.repository.InputMonitorBuilder +import com.android.systemui.cursorposition.data.repository.SingleDisplayCursorPositionRepository +import com.android.systemui.cursorposition.data.repository.SingleDisplayCursorPositionRepositoryImpl + +class TestCursorPositionRepositoryInstanceProvider( +    private val handler: Handler, +    private val listenerBuilder: InputEventListenerBuilder, +    private val inputMonitorBuilder: InputMonitorBuilder, +) : PerDisplayInstanceProviderWithTeardown<SingleDisplayCursorPositionRepository> { + +    override fun destroyInstance(instance: SingleDisplayCursorPositionRepository) { +        instance.destroy() +    } + +    override fun createInstance(displayId: Int): SingleDisplayCursorPositionRepository { +        return SingleDisplayCursorPositionRepositoryImpl( +            displayId, +            handler, +            listenerBuilder, +            inputMonitorBuilder, +        ) +    } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt index 318e5c716ca7..8465345a0bdd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt @@ -20,8 +20,10 @@ import android.app.role.mockRoleManager  import android.content.applicationContext  import android.content.res.mainResources  import android.hardware.input.fakeInputManager +import android.os.fakeExecutorHandler  import android.view.windowManager  import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.concurrency.fakeExecutor  import com.android.systemui.keyboard.shortcut.data.repository.AppLaunchDataRepository  import com.android.systemui.keyboard.shortcut.data.repository.CustomInputGesturesRepository  import com.android.systemui.keyboard.shortcut.data.repository.CustomShortcutCategoriesRepository @@ -33,6 +35,7 @@ import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCust  import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperInputDeviceRepository  import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository  import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper +import com.android.systemui.keyboard.shortcut.data.repository.UserVisibleAppsRepository  import com.android.systemui.keyboard.shortcut.data.source.AccessibilityShortcutsSource  import com.android.systemui.keyboard.shortcut.data.source.AppCategoriesShortcutsSource  import com.android.systemui.keyboard.shortcut.data.source.CurrentAppShortcutsSource @@ -44,6 +47,7 @@ import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutCustomiz  import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor  import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCustomizationModeInteractor  import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor +import com.android.systemui.keyboard.shortcut.fakes.FakeLauncherApps  import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperExclusions  import com.android.systemui.keyboard.shortcut.ui.ShortcutCustomizationDialogStarter  import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutCustomizationViewModel @@ -247,3 +251,15 @@ val Kosmos.shortcutCustomizationViewModelFactory by              }          }      } + +val Kosmos.fakeLauncherApps by Kosmos.Fixture { FakeLauncherApps() } + +val Kosmos.userVisibleAppsRepository by +    Kosmos.Fixture { +        UserVisibleAppsRepository( +            userTracker, +            fakeExecutor, +            fakeExecutorHandler, +            fakeLauncherApps.launcherApps, +        ) +    } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/fakes/FakeLauncherApps.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/fakes/FakeLauncherApps.kt new file mode 100644 index 000000000000..f0c4a357b974 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/fakes/FakeLauncherApps.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.fakes + +import android.content.ComponentName +import android.content.pm.LauncherActivityInfo +import android.content.pm.LauncherApps +import android.os.UserHandle +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock + +class FakeLauncherApps { + +    private val activityListPerUser: MutableMap<Int, MutableList<LauncherActivityInfo>> = +        mutableMapOf() +    private val callbacks: MutableList<LauncherApps.Callback> = mutableListOf() + +    val launcherApps: LauncherApps = mock { +        on { getActivityList(anyOrNull(), any()) } +            .then { +                val userHandle = it.getArgument<UserHandle>(1) + +                activityListPerUser.getOrDefault(userHandle.identifier, emptyList()) +            } +        on { registerCallback(any(), any()) } +            .then { +                val callback = it.getArgument<LauncherApps.Callback>(0) + +                callbacks.add(callback) +            } +        on { unregisterCallback(any()) } +            .then { +                val callback = it.getArgument<LauncherApps.Callback>(0) + +                callbacks.remove(callback) +            } +    } + +    fun installPackageForUser(packageName: String, className: String, userHandle: UserHandle) { +        val launcherActivityInfo: LauncherActivityInfo = mock { +            on { componentName } +                .thenReturn(ComponentName(/* pkg= */ packageName, /* cls= */ className)) +        } + +        if (!activityListPerUser.containsKey(userHandle.identifier)) { +            activityListPerUser[userHandle.identifier] = mutableListOf() +        } + +        activityListPerUser[userHandle.identifier]!!.add(launcherActivityInfo) + +        callbacks.forEach { it.onPackageAdded(/* pkg= */ packageName, userHandle) } +    } + +    fun uninstallPackageForUser(packageName: String, className: String, userHandle: UserHandle) { +        activityListPerUser[userHandle.identifier]?.removeIf { +            it.componentName.packageName == packageName && it.componentName.className == className +        } + +        callbacks.forEach { it.onPackageRemoved(/* pkg= */ packageName, userHandle) } +    } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt index 406b5cb11bb4..19aaa285b6a7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt @@ -22,14 +22,14 @@ import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository  import com.android.systemui.keyguard.data.repository.keyguardSmartspaceSection  import com.android.systemui.kosmos.Kosmos  import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor  val Kosmos.keyguardBlueprintInteractor by      Kosmos.Fixture {          KeyguardBlueprintInteractor(              keyguardBlueprintRepository = keyguardBlueprintRepository,              applicationScope = applicationCoroutineScope, -            shadeInteractor = shadeInteractor, +            shadeModeInteractor = shadeModeInteractor,              configurationInteractor = configurationInteractor,              fingerprintPropertyInteractor = fingerprintPropertyInteractor,              smartspaceSection = keyguardSmartspaceSection, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelKosmos.kt index 76e2cc8b7bd0..384a071b5155 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelKosmos.kt @@ -19,7 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel  import com.android.systemui.keyguard.domain.interactor.keyguardSmartspaceInteractor  import com.android.systemui.kosmos.Kosmos  import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor  import com.android.systemui.util.mockito.mock  val Kosmos.keyguardSmartspaceViewModel by @@ -29,6 +29,6 @@ val Kosmos.keyguardSmartspaceViewModel by              smartspaceController = mock(),              keyguardClockViewModel = keyguardClockViewModel,              smartspaceInteractor = keyguardSmartspaceInteractor, -            shadeInteractor = shadeInteractor, +            shadeModeInteractor = shadeModeInteractor,          )      } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt index 32a30502a370..d06d4ca5597d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt @@ -69,7 +69,6 @@ val Kosmos.shadeInteractorImpl by              userSetupRepository = userSetupRepository,              userSwitcherInteractor = userSwitcherInteractor,              baseShadeInteractor = baseShadeInteractor, -            shadeModeInteractor = shadeModeInteractor,          )      }  var Kosmos.notificationElement: NotificationShadeElement by diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt index 55e35f2b2703..23251d27cff9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarou  import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel  import com.android.systemui.scene.domain.interactor.sceneInteractor  import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor  import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor  import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor  import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory @@ -33,6 +34,7 @@ val Kosmos.notificationsShadeOverlayContentViewModel:          notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory,          sceneInteractor = sceneInteractor,          shadeInteractor = shadeInteractor, +        shadeModeInteractor = shadeModeInteractor,          disableFlagsInteractor = disableFlagsInteractor,          mediaCarouselInteractor = mediaCarouselInteractor,          activeNotificationsInteractor = activeNotificationsInteractor, diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java index 05fc6bc869ca..c2500c8ae7fa 100644 --- a/services/core/java/com/android/server/camera/CameraServiceProxy.java +++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java @@ -559,6 +559,9 @@ public class CameraServiceProxy extends SystemService          @Override          public void onKeepClearAreasChanged(int displayId, List<Rect> restricted,                  List<Rect> unrestricted) { } + +        @Override +        public void onDesktopModeEligibleChanged(int displayId) { }      } diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index bbf7732c9596..7059c83d60b2 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -44,6 +44,7 @@ import android.util.Log;  import android.util.SparseArray;  import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting;  import java.util.Collection;  import java.util.HashSet; @@ -53,6 +54,9 @@ import java.util.List;  import java.util.Map;  import java.util.Optional;  import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit;  import java.util.function.Consumer;  /** @@ -71,6 +75,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub      /** The duration of wakelocks acquired during HAL callbacks */      private static final long WAKELOCK_TIMEOUT_MILLIS = 5 * 1000; +    /** The timeout of open session request */ +    @VisibleForTesting static final long OPEN_SESSION_REQUEST_TIMEOUT_SECONDS = 10; +      /*       * Internal interface used to invoke client callbacks.       */ @@ -81,6 +88,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub      /** The context of the service. */      private final Context mContext; +    /** The shared executor service for handling session operation timeout. */ +    private final ScheduledExecutorService mSessionTimeoutExecutor; +      /** The proxy to talk to the Context Hub HAL for endpoint communication. */      private final IEndpointCommunication mHubInterface; @@ -119,6 +129,8 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub          private SessionState mSessionState = SessionState.PENDING; +        private ScheduledFuture<?> mSessionOpenTimeoutFuture; +          private final boolean mRemoteInitiated;          /** @@ -151,6 +163,17 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub              mSessionState = state;          } +        public void setSessionOpenTimeoutFuture(ScheduledFuture<?> future) { +            mSessionOpenTimeoutFuture = future; +        } + +        public void cancelSessionOpenTimeoutFuture() { +            if (mSessionOpenTimeoutFuture != null) { +                mSessionOpenTimeoutFuture.cancel(false); +            } +            mSessionOpenTimeoutFuture = null; +        } +          public boolean isActive() {              return mSessionState == SessionState.ACTIVE;          } @@ -240,7 +263,8 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub              @NonNull IContextHubEndpointCallback callback,              String packageName,              String attributionTag, -            ContextHubTransactionManager transactionManager) { +            ContextHubTransactionManager transactionManager, +            ScheduledExecutorService sessionTimeoutExecutor) {          mContext = context;          mHubInterface = hubInterface;          mEndpointManager = endpointManager; @@ -250,6 +274,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub          mPackageName = packageName;          mAttributionTag = attributionTag;          mTransactionManager = transactionManager; +        mSessionTimeoutExecutor = sessionTimeoutExecutor;          mPid = Binder.getCallingPid();          mUid = Binder.getCallingUid(); @@ -352,6 +377,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub              }              try {                  mHubInterface.endpointSessionOpenComplete(sessionId); +                info.cancelSessionOpenTimeoutFuture();                  info.setSessionState(Session.SessionState.ACTIVE);              } catch (RemoteException | IllegalArgumentException | UnsupportedOperationException e) {                  Log.e(TAG, "Exception while calling endpointSessionOpenComplete", e); @@ -636,9 +662,10 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub          }          // Check & handle error cases for duplicated session id. -        final boolean existingSession; -        final boolean existingSessionActive;          synchronized (mOpenSessionLock) { +            final boolean existingSession; +            final boolean existingSessionActive; +              if (hasSessionId(sessionId)) {                  existingSession = true;                  existingSessionActive = mSessionMap.get(sessionId).isActive(); @@ -652,19 +679,23 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub              } else {                  existingSession = false;                  existingSessionActive = false; -                mSessionMap.put(sessionId, new Session(initiator, true)); +                Session pendingSession = new Session(initiator, true); +                pendingSession.setSessionOpenTimeoutFuture( +                        mSessionTimeoutExecutor.schedule( +                                () -> onEndpointSessionOpenRequestTimeout(sessionId), +                                OPEN_SESSION_REQUEST_TIMEOUT_SECONDS, +                                TimeUnit.SECONDS)); +                mSessionMap.put(sessionId, pendingSession);              } -        } -        if (existingSession) { -            if (existingSessionActive) { -                // Existing session is already active, call onSessionOpenComplete. -                openSessionRequestComplete(sessionId); +            if (existingSession) { +                if (existingSessionActive) { +                    // Existing session is already active, call onSessionOpenComplete. +                    openSessionRequestComplete(sessionId); +                } +                // Silence this request. The session open timeout future will handle clean up.                  return Optional.empty();              } -            // Reject the session open request for now. Consider invalidating previous pending -            // session open request based on timeout. -            return Optional.of(Reason.OPEN_ENDPOINT_SESSION_REQUEST_REJECTED);          }          boolean success = @@ -679,6 +710,20 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub          return success ? Optional.empty() : Optional.of(reason);      } +    private void onEndpointSessionOpenRequestTimeout(int sessionId) { +        synchronized (mOpenSessionLock) { +            Session s = mSessionMap.get(sessionId); +            if (s == null || s.isActive()) { +                return; +            } +            Log.w( +                    TAG, +                    "onEndpointSessionOpenRequestTimeout: " + "clean up session, id: " + sessionId); +            cleanupSessionResources(sessionId); +            mEndpointManager.halCloseEndpointSessionNoThrow(sessionId, Reason.TIMEOUT); +        } +    } +      private byte onMessageReceivedInternal(int sessionId, HubMessage message) {          synchronized (mOpenSessionLock) {              if (!isSessionActive(sessionId)) { diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java index e1561599517d..0dc1b832f5a4 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java @@ -46,6 +46,8 @@ import java.util.Map;  import java.util.Optional;  import java.util.Set;  import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor;  import java.util.function.Consumer;  /** @@ -112,6 +114,9 @@ import java.util.function.Consumer;      /** The interface for endpoint communication (retrieved from HAL in init()) */      private IEndpointCommunication mHubInterface = null; +    /** Thread pool executor for handling timeout */ +    private final ScheduledExecutorService mSessionTimeoutExecutor; +      /*       * The list of previous registration records.       */ @@ -154,15 +159,31 @@ import java.util.function.Consumer;          }      } -    /* package */ ContextHubEndpointManager( +    @VisibleForTesting +    ContextHubEndpointManager(              Context context,              IContextHubWrapper contextHubProxy,              HubInfoRegistry hubInfoRegistry, -            ContextHubTransactionManager transactionManager) { +            ContextHubTransactionManager transactionManager, +            ScheduledExecutorService scheduledExecutorService) {          mContext = context;          mContextHubProxy = contextHubProxy;          mHubInfoRegistry = hubInfoRegistry;          mTransactionManager = transactionManager; +        mSessionTimeoutExecutor = scheduledExecutorService; +    } + +    /* package */ ContextHubEndpointManager( +            Context context, +            IContextHubWrapper contextHubProxy, +            HubInfoRegistry hubInfoRegistry, +            ContextHubTransactionManager transactionManager) { +        this( +                context, +                contextHubProxy, +                hubInfoRegistry, +                transactionManager, +                new ScheduledThreadPoolExecutor(1));      }      /** @@ -264,7 +285,8 @@ import java.util.function.Consumer;                          callback,                          packageName,                          attributionTag, -                        mTransactionManager); +                        mTransactionManager, +                        mSessionTimeoutExecutor);          broker.register();          mEndpointMap.put(endpointId, broker); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 6ce1746ed3f6..c1e46a68b733 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1877,73 +1877,35 @@ public class NotificationManagerService extends SystemService {          }      }; -    private void unclassifyNotificationsForUser(final int userId) { -        if (DBG) { -            Slog.v(TAG, "unclassifyForUser: " + userId); -        } -        unclassifyNotificationsFiltered((r) -> r.getUserId() == userId); +    private void applyNotificationUpdateForUser(final int userId, +            NotificationUpdate notificationUpdate) { +        applyUpdateForNotificationsFiltered((r) -> r.getUserId() == userId, +                notificationUpdate);      } -    private void unclassifyNotificationsForUid(final int userId, @NonNull final String pkg) { -        if (DBG) { -            Slog.v(TAG, "unclassifyForUid userId: " + userId + " pkg: " + pkg); -        } -        unclassifyNotificationsFiltered((r) -> +    private void applyNotificationUpdateForUid(final int userId, @NonNull final String pkg, +            NotificationUpdate notificationUpdate) { +        applyUpdateForNotificationsFiltered((r) ->                  r.getUserId() == userId -                && Objects.equals(r.getSbn().getPackageName(), pkg)); +                && Objects.equals(r.getSbn().getPackageName(), pkg), +                notificationUpdate);      } -    private void unclassifyNotificationsForUserAndType(final int userId, -            final @Types int bundleType) { -        if (DBG) { -            Slog.v(TAG, -                    "unclassifyForUserAndType userId: " + userId + " bundleType: " + bundleType); -        } +    private void applyNotificationUpdateForUserAndChannelType(final int userId, +            final @Types int bundleType, NotificationUpdate notificationUpdate) {          final String bundleChannelId = NotificationChannel.getChannelIdForBundleType(bundleType); -        unclassifyNotificationsFiltered((r) -> +        applyUpdateForNotificationsFiltered((r) ->                  r.getUserId() == userId                  && r.getChannel() != null -                && Objects.equals(bundleChannelId, r.getChannel().getId())); +                && Objects.equals(bundleChannelId, r.getChannel().getId()), +                notificationUpdate);      } -    private void unclassifyNotificationsFiltered(Predicate<NotificationRecord> filter) { -        if (!(notificationClassificationUi() && notificationRegroupOnClassification())) { -            return; -        } -        synchronized (mNotificationLock) { -            for (int i = 0; i < mEnqueuedNotifications.size(); i++) { -                final NotificationRecord r = mEnqueuedNotifications.get(i); -                if (filter.test(r)) { -                    unclassifyNotificationLocked(r); -                } -            } - -            for (int i = 0; i < mNotificationList.size(); i++) { -                final NotificationRecord r = mNotificationList.get(i); -                if (filter.test(r)) { -                    unclassifyNotificationLocked(r); -                } -            } -        } -    } - -    @GuardedBy("mNotificationLock") -    private void unclassifyNotificationLocked(@NonNull final NotificationRecord r) { -        if (DBG) { -            Slog.v(TAG, "unclassifyNotification: " + r); -        } -        // Only NotificationRecord's mChannel is updated when bundled, the Notification -        // mChannelId will always be the original channel. -        String origChannelId = r.getNotification().getChannelId(); -        NotificationChannel originalChannel = mPreferencesHelper.getNotificationChannel( -                r.getSbn().getPackageName(), r.getUid(), origChannelId, false); -        String currChannelId = r.getChannel().getId(); -        boolean isClassified = NotificationChannel.SYSTEM_RESERVED_IDS.contains(currChannelId); -        if (originalChannel != null && !origChannelId.equals(currChannelId) && isClassified) { -            r.updateNotificationChannel(originalChannel); -            mGroupHelper.onNotificationUnbundled(r, -                    GroupHelper.isOriginalGroupSummaryPresent(r, mSummaryByGroupKey)); -        } +    private void applyNotificationUpdateForUserAndType(final int userId, +            final @Types int bundleType, NotificationUpdate notificationUpdate) { +        applyUpdateForNotificationsFiltered( +                (r) -> r.getUserId() == userId && r.getBundleType() == bundleType, +                notificationUpdate);      }      @VisibleForTesting @@ -1956,7 +1918,7 @@ public class NotificationManagerService extends SystemService {              if (r == null) {                  return;              } -            unclassifyNotificationLocked(r); +            unclassifyNotificationLocked(r, true);          }      } @@ -1974,50 +1936,36 @@ public class NotificationManagerService extends SystemService {          }      } -    private void reclassifyNotificationsFiltered(Predicate<NotificationRecord> filter) { -        if (!(notificationClassificationUi() && notificationRegroupOnClassification())) { -            return; -        } -        synchronized (mNotificationLock) { -            for (int i = 0; i < mEnqueuedNotifications.size(); i++) { -                final NotificationRecord r = mEnqueuedNotifications.get(i); -                if (filter.test(r)) { -                    reclassifyNotificationLocked(r, false); -                } -            } - -            for (int i = 0; i < mNotificationList.size(); i++) { -                final NotificationRecord r = mNotificationList.get(i); -                if (filter.test(r)) { -                    reclassifyNotificationLocked(r, true); -                } -            } -        } -    } - -    private void reclassifyNotificationsForUserAndType(final int userId, -            final @Types int bundleType) { +    @GuardedBy("mNotificationLock") +    private void unclassifyNotificationLocked(@NonNull final NotificationRecord r, +            boolean isPosted) {          if (DBG) { -            Slog.v(TAG, "reclassifyNotificationsForUserAndType userId: " + userId + " bundleType: " -                    + bundleType); +            Slog.v(TAG, "unclassifyNotification: " + r);          } -        reclassifyNotificationsFiltered( -                (r) -> r.getUserId() == userId && r.getBundleType() == bundleType); -    } - -    private void reclassifyNotificationsForUid(final int userId, final String pkg) { -        if (DBG) { -            Slog.v(TAG, "reclassifyNotificationsForUid userId: " + userId + " pkg: " + pkg); +        // Only NotificationRecord's mChannel is updated when bundled, the Notification +        // mChannelId will always be the original channel. +        String origChannelId = r.getNotification().getChannelId(); +        NotificationChannel originalChannel = mPreferencesHelper.getNotificationChannel( +                r.getSbn().getPackageName(), r.getUid(), origChannelId, false); +        String currChannelId = r.getChannel().getId(); +        boolean isClassified = NotificationChannel.SYSTEM_RESERVED_IDS.contains(currChannelId); +        if (originalChannel != null && !origChannelId.equals(currChannelId) && isClassified) { +            r.updateNotificationChannel(originalChannel); +            mGroupHelper.onNotificationUnbundled(r, +                    GroupHelper.isOriginalGroupSummaryPresent(r, mSummaryByGroupKey));          } -        reclassifyNotificationsFiltered((r) -> -                r.getUserId() == userId && Objects.equals(r.getSbn().getPackageName(), pkg));      } -    private void reclassifyNotificationsForUser(final int userId) { -        if (DBG) { -            Slog.v(TAG, "reclassifyAllNotificationsForUser: " + userId); -        } -        reclassifyNotificationsFiltered((r) -> r.getUserId() == userId); +    @GuardedBy("mNotificationLock") +    private void unsummarizeNotificationLocked(@NonNull final NotificationRecord r, +            boolean isPosted) { +        Bundle signals = new Bundle(); +        signals.putString(KEY_SUMMARIZATION, null); +        Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, "", +                r.getSbn().getUserId()); +        r.addAdjustment(adjustment); +        mRankingHandler.requestSort(); +      }      @GuardedBy("mNotificationLock") @@ -2043,6 +1991,33 @@ public class NotificationManagerService extends SystemService {          }      } +    /** +     * Given a filter and a function to update a notification record, runs that function on all +     * enqueued and posted notifications that match the filter +     */ +    private void applyUpdateForNotificationsFiltered(Predicate<NotificationRecord> filter, +            NotificationUpdate notificationUpdate) { +        synchronized (mNotificationLock) { +            for (int i = 0; i < mEnqueuedNotifications.size(); i++) { +                final NotificationRecord r = mEnqueuedNotifications.get(i); +                if (filter.test(r)) { +                    notificationUpdate.apply(r, false); +                } +            } + +            for (int i = 0; i < mNotificationList.size(); i++) { +                final NotificationRecord r = mNotificationList.get(i); +                if (filter.test(r)) { +                    notificationUpdate.apply(r, true); +                } +            } +        } +    } + +    private interface NotificationUpdate { +        void apply(NotificationRecord r, boolean isPosted); +    } +      NotificationManagerPrivate mNotificationManagerPrivate = new NotificationManagerPrivate() {          @Nullable          @Override @@ -4475,9 +4450,11 @@ public class NotificationManagerService extends SystemService {          public void allowAssistantAdjustment(String adjustmentType) {              checkCallerIsSystemOrSystemUiOrShell();              mAssistants.allowAdjustmentType(adjustmentType); +            int userId = UserHandle.getUserId(Binder.getCallingUid());              if ((notificationClassificationUi() && notificationRegroupOnClassification())) {                  if (KEY_TYPE.equals(adjustmentType)) { -                    reclassifyNotificationsForUser(UserHandle.getUserId(Binder.getCallingUid())); +                    applyNotificationUpdateForUser(userId, +                            NotificationManagerService.this::reclassifyNotificationLocked);                  }              }              handleSavePolicyFile(); @@ -4488,9 +4465,17 @@ public class NotificationManagerService extends SystemService {          public void disallowAssistantAdjustment(String adjustmentType) {              checkCallerIsSystemOrSystemUiOrShell();              mAssistants.disallowAdjustmentType(adjustmentType); +            int userId = UserHandle.getUserId(Binder.getCallingUid());              if ((notificationClassificationUi() && notificationRegroupOnClassification())) {                  if (KEY_TYPE.equals(adjustmentType)) { -                    unclassifyNotificationsForUser(UserHandle.getUserId(Binder.getCallingUid())); +                    applyNotificationUpdateForUser(userId, +                            NotificationManagerService.this::unclassifyNotificationLocked); +                } +            } +            if (nmSummarizationUi() || nmSummarization()) { +                if (KEY_SUMMARIZATION.equals(adjustmentType)) { +                    applyNotificationUpdateForUser(userId, +                            NotificationManagerService.this::unsummarizeNotificationLocked);                  }              }              handleSavePolicyFile(); @@ -4539,11 +4524,13 @@ public class NotificationManagerService extends SystemService {              mAssistants.setAssistantAdjustmentKeyTypeState(type, enabled);              if ((notificationClassificationUi() && notificationRegroupOnClassification())) {                  if (enabled) { -                    reclassifyNotificationsForUserAndType( -                            UserHandle.getUserId(Binder.getCallingUid()), type); +                    applyNotificationUpdateForUserAndType( +                            UserHandle.getUserId(Binder.getCallingUid()), type, +                            NotificationManagerService.this::reclassifyNotificationLocked);                  } else { -                    unclassifyNotificationsForUserAndType( -                            UserHandle.getUserId(Binder.getCallingUid()), type); +                    applyNotificationUpdateForUserAndChannelType( +                            UserHandle.getUserId(Binder.getCallingUid()), type, +                            NotificationManagerService.this::unclassifyNotificationLocked);                  }              }              handleSavePolicyFile(); @@ -4569,11 +4556,17 @@ public class NotificationManagerService extends SystemService {              if (notificationClassificationUi() && notificationRegroupOnClassification()                      && key.equals(KEY_TYPE)) {                  if (enabled) { -                    reclassifyNotificationsForUid(UserHandle.getUserId(Binder.getCallingUid()), -                            pkg); +                    applyNotificationUpdateForUid(UserHandle.getUserId(Binder.getCallingUid()), +                            pkg, NotificationManagerService.this::reclassifyNotificationLocked);                  } else { -                    unclassifyNotificationsForUid(UserHandle.getUserId(Binder.getCallingUid()), -                            pkg); +                    applyNotificationUpdateForUid(UserHandle.getUserId(Binder.getCallingUid()), +                            pkg, NotificationManagerService.this::unclassifyNotificationLocked); +                } +            } +            if (nmSummarization() || nmSummarizationUi()) { +                if (KEY_SUMMARIZATION.equals(key) && !enabled) { +                    applyNotificationUpdateForUid(UserHandle.getUserId(Binder.getCallingUid()), +                            pkg, NotificationManagerService.this::unsummarizeNotificationLocked);                  }              }              handleSavePolicyFile(); diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index cec5a93a2a15..700f6fafe2d7 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -999,7 +999,7 @@ public final class NotificationRecord {          return null;      } -    public String getSummarization() { +    public @Nullable String getSummarization() {          if ((android.app.Flags.nmSummarizationUi() || android.app.Flags.nmSummarization())) {              return mSummarization;          } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index af788ea6ccdb..e00d80f860b0 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -3107,8 +3107,17 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {                          mInternalProgress = 0.5f;                          computeProgressLocked(true);                      } +                    final File libDir = new File(stageDir, NativeLibraryHelper.LIB_DIR_NAME); +                    if (!mayInheritNativeLibs()) { +                        // Start from a clean slate +                        NativeLibraryHelper.removeNativeBinariesFromDirLI(libDir, true); +                    } +                    // Skip native libraries processing for archival installation. +                    if (isArchivedInstallation()) { +                        return; +                    }                      extractNativeLibraries( -                            mPackageLite, stageDir, params.abiOverride, mayInheritNativeLibs()); +                            mPackageLite, libDir, params.abiOverride);                  }              }          } @@ -4505,21 +4514,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {          Slog.d(TAG, "Copied " + fromFiles.size() + " files into " + toDir);      } -    private void extractNativeLibraries(PackageLite packageLite, File packageDir, -            String abiOverride, boolean inherit) +    private void extractNativeLibraries(PackageLite packageLite, File libDir, +            String abiOverride)              throws PackageManagerException {          Objects.requireNonNull(packageLite); -        final File libDir = new File(packageDir, NativeLibraryHelper.LIB_DIR_NAME); -        if (!inherit) { -            // Start from a clean slate -            NativeLibraryHelper.removeNativeBinariesFromDirLI(libDir, true); -        } - -        // Skip native libraries processing for archival installation. -        if (isArchivedInstallation()) { -            return; -        } -          NativeLibraryHelper.Handle handle = null;          try {              handle = NativeLibraryHelper.Handle.create(packageLite); diff --git a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java index 7cd9bdbc662c..b4ca7845ffee 100644 --- a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java +++ b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java @@ -505,7 +505,9 @@ public class BatteryHistoryDirectory implements BatteryStatsHistory.BatteryHisto                  for (int i = 0; i < mHistoryFiles.size(); i++) {                      size += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length();                  } -                while (size > mMaxHistorySize) { +                // Trim until the directory size is within the limit or there is just one most +                // recent file left in the directory +                while (size > mMaxHistorySize && mHistoryFiles.size() > 1) {                      BatteryHistoryFile oldest = mHistoryFiles.get(0);                      int length = (int) oldest.atomicFile.getBaseFile().length();                      oldest.atomicFile.delete(); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 20b0f58d4fd9..1d7247330b7a 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -98,7 +98,6 @@ import static android.os.Build.VERSION_CODES.HONEYCOMB;  import static android.os.Build.VERSION_CODES.O;  import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS;  import static android.os.Process.SYSTEM_UID; -import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;  import static android.view.Display.INVALID_DISPLAY;  import static android.view.WindowManager.ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15;  import static android.view.WindowManager.ENABLE_ACTIVITY_EMBEDDING_FOR_ANDROID_15; @@ -109,7 +108,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;  import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;  import static android.view.WindowManager.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED;  import static android.view.WindowManager.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING; -import static android.view.WindowManager.TRANSIT_OLD_UNSET;  import static android.view.WindowManager.TRANSIT_RELAUNCH;  import static android.view.WindowManager.hasWindowExtensionsEnabled;  import static android.window.TransitionInfo.FLAGS_IS_OCCLUDED_NO_ANIMATION; @@ -117,9 +115,7 @@ import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;  import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;  import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ADD_REMOVE; -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM;  import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS; -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS_ANIM;  import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_CONFIGURATION;  import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_CONTAINERS;  import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_FOCUS; @@ -135,8 +131,6 @@ import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANG  import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;  import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED;  import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE; -import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM; -import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;  import static com.android.server.wm.ActivityRecord.State.DESTROYED;  import static com.android.server.wm.ActivityRecord.State.DESTROYING;  import static com.android.server.wm.ActivityRecord.State.FINISHING; @@ -364,7 +358,6 @@ import com.android.server.uri.GrantUri;  import com.android.server.uri.NeededUriGrants;  import com.android.server.uri.UriPermissionOwner;  import com.android.server.wm.ActivityMetricsLogger.TransitionInfoSnapshot; -import com.android.server.wm.SurfaceAnimator.AnimationType;  import com.android.server.wm.WindowManagerService.H;  import com.android.window.flags.Flags; @@ -7208,40 +7201,6 @@ final class ActivityRecord extends WindowToken {          return candidate;      } -    @Override -    public SurfaceControl getAnimationLeashParent() { -        // For transitions in the root pinned task (menu activity) we just let them occur as a child -        // of the root pinned task. -        // All normal app transitions take place in an animation layer which is below the root -        // pinned task but may be above the parent tasks of the given animating apps by default. -        // When a new hierarchical animation is enabled, we just let them occur as a child of the -        // parent task, i.e. the hierarchy of the surfaces is unchanged. -        if (inPinnedWindowingMode()) { -            return getRootTask().getSurfaceControl(); -        } else { -            return super.getAnimationLeashParent(); -        } -    } - -    @VisibleForTesting -    boolean shouldAnimate() { -        return task == null || task.shouldAnimate(); -    } - -    /** -     * Creates a layer to apply crop to an animation. -     */ -    private SurfaceControl createAnimationBoundsLayer(Transaction t) { -        ProtoLog.i(WM_DEBUG_APP_TRANSITIONS_ANIM, "Creating animation bounds layer"); -        final SurfaceControl.Builder builder = makeAnimationLeash() -                .setParent(getAnimationLeashParent()) -                .setName(getSurfaceControl() + " - animation-bounds") -                .setCallsite("ActivityRecord.createAnimationBoundsLayer"); -        final SurfaceControl boundsLayer = builder.build(); -        t.show(boundsLayer); -        return boundsLayer; -    } -      boolean isTransitionForward() {          return (mStartingData != null && mStartingData.mIsTransitionForward)                  || mDisplayContent.isNextTransitionForward(); @@ -7253,25 +7212,6 @@ final class ActivityRecord extends WindowToken {      }      @Override -    public void onLeashAnimationStarting(Transaction t, SurfaceControl leash) { -        // If the animation needs to be cropped then an animation bounds layer is created as a -        // child of the root pinned task or animation layer. The leash is then reparented to this -        // new layer. -        if (mNeedsAnimationBoundsLayer) { -            mTmpRect.setEmpty(); -            task.getBounds(mTmpRect); -            mAnimationBoundsLayer = createAnimationBoundsLayer(t); - -            // Crop to root task bounds. -            t.setLayer(leash, 0); -            t.setLayer(mAnimationBoundsLayer, getLastLayer()); - -            // Reparent leash to animation bounds layer. -            t.reparent(leash, mAnimationBoundsLayer); -        } -    } - -    @Override      boolean showSurfaceOnCreation() {          return false;      } @@ -7310,74 +7250,6 @@ final class ActivityRecord extends WindowToken {          return mLastSurfaceShowing;      } -    @Override -    public void onAnimationLeashLost(Transaction t) { -        super.onAnimationLeashLost(t); -        if (mAnimationBoundsLayer != null) { -            t.remove(mAnimationBoundsLayer); -            mAnimationBoundsLayer = null; -        } - -        mNeedsAnimationBoundsLayer = false; -    } - -    @Override -    protected void onAnimationFinished(@AnimationType int type, AnimationAdapter anim) { -        super.onAnimationFinished(type, anim); - -        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "AR#onAnimationFinished"); -        mTransit = TRANSIT_OLD_UNSET; -        mTransitFlags = 0; - -        setAppLayoutChanges(FINISH_LAYOUT_REDO_ANIM | FINISH_LAYOUT_REDO_WALLPAPER, -                "ActivityRecord"); - -        setClientVisible(isVisible() || mVisibleRequested); - -        getDisplayContent().computeImeTargetIfNeeded(this); - -        ProtoLog.v(WM_DEBUG_ANIM, "Animation done in %s" -                + ": reportedVisible=%b okToDisplay=%b okToAnimate=%b startingDisplayed=%b", -                this, reportedVisible, okToDisplay(), okToAnimate(), -                isStartingWindowDisplayed()); - -        // WindowState.onExitAnimationDone might modify the children list, so make a copy and then -        // traverse the copy. -        final ArrayList<WindowState> children = new ArrayList<>(mChildren); -        children.forEach(WindowState::onExitAnimationDone); -        // The starting window could transfer to another activity after app transition started, in -        // that case the latest top activity might not receive exit animation done callback if the -        // starting window didn't applied exit animation success. Notify animation finish to the -        // starting window if needed. -        if (task != null && startingMoved) { -            final WindowState transferredStarting = task.getWindow(w -> -                    w.mAttrs.type == TYPE_APPLICATION_STARTING); -            if (transferredStarting != null && transferredStarting.mAnimatingExit -                    && !transferredStarting.isSelfAnimating(0 /* flags */, -                    ANIMATION_TYPE_WINDOW_ANIMATION)) { -                transferredStarting.onExitAnimationDone(); -            } -        } - -        scheduleAnimation(); - -        // Schedule to handle the stopping and finishing activities which the animation is done -        // because the activities which were animating have not been stopped yet. -        mTaskSupervisor.scheduleProcessStoppingAndFinishingActivitiesIfNeeded(); -        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); -    } - -    void clearAnimatingFlags() { -        boolean wallpaperMightChange = false; -        for (int i = mChildren.size() - 1; i >= 0; i--) { -            final WindowState win = mChildren.get(i); -            wallpaperMightChange |= win.clearAnimatingFlags(); -        } -        if (wallpaperMightChange) { -            requestUpdateWallpaperIfNeeded(); -        } -    } -      public @TransitionOldType int getTransit() {          return mTransit;      } diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java index 03ba1a51ad7b..61e8e09bc035 100644 --- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java @@ -21,7 +21,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;  import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;  import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;  import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; -import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;  import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;  import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK;  import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK; @@ -242,20 +241,18 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {              @NonNull Task launchingTask) {          if (existingTaskActivity == null || launchingActivity == null) return false;          return (existingTaskActivity.packageName == launchingActivity.packageName) -                && isLaunchingNewTask(launchingActivity.launchMode, -                    launchingTask.getBaseIntent().getFlags()) +                && isLaunchingNewSingleTask(launchingActivity.launchMode)                  && isClosingExitingInstance(launchingTask.getBaseIntent().getFlags());      }      /** -     * Returns true if the launch mode or intent will result in a new task being created for the +     * Returns true if the launch mode will result in a single new task being created for the       * activity.       */ -    private boolean isLaunchingNewTask(int launchMode, int intentFlags) { +    private boolean isLaunchingNewSingleTask(int launchMode) {          return launchMode == LAUNCH_SINGLE_TASK                  || launchMode == LAUNCH_SINGLE_INSTANCE -                || launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK -                || (intentFlags & FLAG_ACTIVITY_NEW_TASK) != 0; +                || launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK;      }      /** diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 2f9242fbdfc9..16caec81f5f8 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4982,22 +4982,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp          return win != null;      } -    /** -     * Callbacks when the given type of {@link WindowContainer} animation finished running in the -     * hierarchy. -     */ -    void onWindowAnimationFinished(@NonNull WindowContainer wc, int type) { -        if (mImeScreenshot != null) { -            ProtoLog.i(WM_DEBUG_IME, -                    "onWindowAnimationFinished, wc=%s, type=%s, imeSnapshot=%s, target=%s", -                    wc, SurfaceAnimator.animationTypeToString(type), mImeScreenshot, -                    mImeScreenshot.getImeTarget()); -        } -        if ((type & WindowState.EXIT_ANIMATING_TYPES) != 0) { -            removeImeSurfaceByTarget(wc); -        } -    } -      // TODO: Super unexpected long method that should be broken down...      void applySurfaceChangesTransaction() {          final WindowSurfacePlacer surfacePlacer = mWmService.mWindowPlacerLocked; diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index f9eb0574d87c..dbae9c4b3a0f 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1882,6 +1882,9 @@ public class DisplayPolicy {              final boolean isSystemDecorationsSupported =                      mDisplayContent.isSystemDecorationsSupported();              final boolean isHomeSupported = mDisplayContent.isHomeSupported(); +            final boolean eligibleForDesktopMode = +                    isSystemDecorationsSupported && (mDisplayContent.isDefaultDisplay +                            || mDisplayContent.allowContentModeSwitch());              mHandler.post(() -> {                  if (isSystemDecorationsSupported) {                      StatusBarManagerInternal statusBar = getStatusBarManagerInternal(); @@ -1896,6 +1899,10 @@ public class DisplayPolicy {                          wpMgr.onDisplayAddSystemDecorations(displayId);                      }                  } +                if (eligibleForDesktopMode) { +                    mService.mDisplayNotificationController.dispatchDesktopModeEligibleChanged( +                            displayId); +                }              });          } else {              mHandler.post(() -> { @@ -1926,6 +1933,8 @@ public class DisplayPolicy {                      if (wpMgr != null) {                          wpMgr.onDisplayRemoveSystemDecorations(displayId);                      } +                    mService.mDisplayNotificationController.dispatchDesktopModeEligibleChanged( +                            displayId);                      final NotificationManagerInternal notificationManager =                              LocalServices.getService(NotificationManagerInternal.class);                      if (notificationManager != null) { diff --git a/services/core/java/com/android/server/wm/DisplayWindowListenerController.java b/services/core/java/com/android/server/wm/DisplayWindowListenerController.java index d90fff229cd9..d705274b62e7 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowListenerController.java +++ b/services/core/java/com/android/server/wm/DisplayWindowListenerController.java @@ -133,4 +133,15 @@ class DisplayWindowListenerController {          }          mDisplayListeners.finishBroadcast();      } + +    void dispatchDesktopModeEligibleChanged(int displayId) { +        int count = mDisplayListeners.beginBroadcast(); +        for (int i = 0; i < count; ++i) { +            try { +                mDisplayListeners.getBroadcastItem(i).onDesktopModeEligibleChanged(displayId); +            } catch (RemoteException e) { +            } +        } +        mDisplayListeners.finishBroadcast(); +    }  } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index ec17d131958b..3cce17242648 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5764,15 +5764,16 @@ class Task extends TaskFragment {              return false;          } -        // If we have a watcher, preflight the move before committing to it.  First check -        // for *other* available tasks, but if none are available, then try again allowing the -        // current task to be selected. +        // If we have a watcher, preflight the move before committing to it. +        // Checks for other available tasks; however, if none are available, skips because this +        // is the bottommost task.          if (mAtmService.mController != null && isTopRootTaskInDisplayArea()) { -            ActivityRecord next = topRunningActivity(null, task.mTaskId); -            if (next == null) { -                next = topRunningActivity(null, INVALID_TASK_ID); -            } +            final ActivityRecord next = getDisplayArea().getActivity( +                    a -> isTopRunning(a, task.mTaskId, null /* notTop */));              if (next != null) { +                if (next.isState(RESUMED)) { +                    return true; +                }                  // ask watcher if this is allowed                  boolean moveOK = true;                  try { diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 5b4870b0c0c7..b1422c20e516 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -55,14 +55,12 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME  import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;  import android.annotation.CallSuper; -import android.annotation.ColorInt;  import android.annotation.IntDef;  import android.annotation.NonNull;  import android.annotation.Nullable;  import android.content.pm.ActivityInfo;  import android.content.pm.ActivityInfo.ScreenOrientation;  import android.content.res.Configuration; -import android.graphics.Color;  import android.graphics.Point;  import android.graphics.Rect;  import android.os.Debug; @@ -105,7 +103,6 @@ import java.util.ArrayList;  import java.util.Comparator;  import java.util.LinkedList;  import java.util.List; -import java.util.concurrent.atomic.AtomicInteger;  import java.util.function.BiFunction;  import java.util.function.Consumer;  import java.util.function.Function; @@ -218,14 +215,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<      protected final WindowManagerService mWmService;      final TransitionController mTransitionController; -    /** -     * Sources which triggered a surface animation on this container. An animation target can be -     * promoted to higher level, for example, from a set of {@link ActivityRecord}s to -     * {@link Task}. In this case, {@link ActivityRecord}s are set on this variable while -     * the animation is running, and reset after finishing it. -     */ -    private final ArraySet<WindowContainer> mSurfaceAnimationSources = new ArraySet<>(); -      private final Point mTmpPos = new Point();      protected final Point mLastSurfacePosition = new Point();      protected @Surface.Rotation int mLastDeltaRotation = Surface.ROTATION_0; @@ -279,17 +268,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<       */      int mTransitFlags; -    /** Layer used to constrain the animation to a container's stack bounds. */ -    SurfaceControl mAnimationBoundsLayer; - -    /** Whether this container needs to create mAnimationBoundsLayer for cropping animations. */ -    boolean mNeedsAnimationBoundsLayer; - -    /** -     * This gets used during some open/close transitions as well as during a change transition -     * where it represents the starting-state snapshot. -     */ -    final Point mTmpPoint = new Point();      protected final Rect mTmpRect = new Rect();      final Rect mTmpPrevBounds = new Rect(); @@ -2961,7 +2939,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<      }      void cancelAnimation() { -        doAnimationFinished(mSurfaceAnimator.getAnimationType(), mSurfaceAnimator.getAnimation());          mSurfaceAnimator.cancelAnimation();      } @@ -2992,10 +2969,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<                  || (getParent() != null && getParent().inPinnedWindowingMode());      } -    ArraySet<WindowContainer> getAnimationSources() { -        return mSurfaceAnimationSources; -    } -      @Override      public Builder makeAnimationLeash() {          return makeSurface().setContainerLayer(); @@ -3094,21 +3067,10 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<          return mAnimationLeash;      } -    private void doAnimationFinished(@AnimationType int type, AnimationAdapter anim) { -        for (int i = 0; i < mSurfaceAnimationSources.size(); ++i) { -            mSurfaceAnimationSources.valueAt(i).onAnimationFinished(type, anim); -        } -        mSurfaceAnimationSources.clear(); -        if (mDisplayContent != null) { -            mDisplayContent.onWindowAnimationFinished(this, type); -        } -    } -      /**       * Called when an animation has finished running.       */      protected void onAnimationFinished(@AnimationType int type, AnimationAdapter anim) { -        doAnimationFinished(type, anim);          mWmService.onAnimationFinished();      } @@ -3821,50 +3783,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<          return true;      } -    private class AnimationRunnerBuilder { -        /** -         * Runs when the surface stops animating -         */ -        private final List<Runnable> mOnAnimationFinished = new LinkedList<>(); -        /** -         * Runs when the animation is cancelled but the surface is still animating -         */ -        private final List<Runnable> mOnAnimationCancelled = new LinkedList<>(); - -        private void setTaskBackgroundColor(@ColorInt int backgroundColor) { -            TaskDisplayArea taskDisplayArea = getTaskDisplayArea(); - -            if (taskDisplayArea != null && backgroundColor != Color.TRANSPARENT) { -                taskDisplayArea.setBackgroundColor(backgroundColor); - -                // Atomic counter to make sure the clearColor callback is only called one. -                // It will be called twice in the case we cancel the animation without restart -                // (in that case it will run as the cancel and finished callbacks). -                final AtomicInteger callbackCounter = new AtomicInteger(0); -                final Runnable clearBackgroundColorHandler = () -> { -                    if (callbackCounter.getAndIncrement() == 0) { -                        taskDisplayArea.clearBackgroundColor(); -                    } -                }; - -                // We want to make sure this is called both when the surface stops animating and -                // also when an animation is cancelled (i.e. animation is replaced by another -                // animation but and so the surface is still animating) -                mOnAnimationFinished.add(clearBackgroundColorHandler); -                mOnAnimationCancelled.add(clearBackgroundColorHandler); -            } -        } - -        private IAnimationStarter build() { -            return (Transaction t, AnimationAdapter adapter, boolean hidden, -                    @AnimationType int type, @Nullable AnimationAdapter snapshotAnim) -> { -                startAnimation(getPendingTransaction(), adapter, !isVisible(), type, -                        (animType, anim) -> mOnAnimationFinished.forEach(Runnable::run), -                        () -> mOnAnimationCancelled.forEach(Runnable::run), snapshotAnim); -            }; -        } -    } -      private interface IAnimationStarter {          void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,                  @AnimationType int type, @Nullable AnimationAdapter snapshotAnim); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index d43aba0d218d..a03b765cae6a 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -4697,40 +4697,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP          return super.handleCompleteDeferredRemoval();      } -    boolean clearAnimatingFlags() { -        boolean didSomething = false; -        // We also don't clear the mAnimatingExit flag for windows which have the -        // mRemoveOnExit flag. This indicates an explicit remove request has been issued -        // by the client. We should let animation proceed and not clear this flag or -        // they won't eventually be removed by WindowStateAnimator#finishExit. -        if (!mRemoveOnExit) { -            // Clear mAnimating flag together with mAnimatingExit. When animation -            // changes from exiting to entering, we need to clear this flag until the -            // new animation gets applied, so that isAnimationStarting() becomes true -            // until then. -            // Otherwise applySurfaceChangesTransaction will fail to skip surface -            // placement for this window during this period, one or more frame will -            // show up with wrong position or scale. -            if (mAnimatingExit) { -                mAnimatingExit = false; -                ProtoLog.d(WM_DEBUG_ANIM, "Clear animatingExit: reason=clearAnimatingFlags win=%s", -                        this); -                didSomething = true; -            } -            if (mDestroying) { -                mDestroying = false; -                mWmService.mDestroySurface.remove(this); -                didSomething = true; -            } -        } - -        for (int i = mChildren.size() - 1; i >= 0; --i) { -            didSomething |= (mChildren.get(i)).clearAnimatingFlags(); -        } - -        return didSomething; -    } -      public boolean isRtl() {          return getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;      } diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java index 87cd1560509c..992c1183d0c0 100644 --- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java +++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;  import static org.mockito.ArgumentMatchers.any;  import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong;  import static org.mockito.ArgumentMatchers.eq;  import static org.mockito.Mockito.doThrow;  import static org.mockito.Mockito.never; @@ -62,6 +63,7 @@ import org.mockito.junit.MockitoRule;  import java.util.Collections;  import java.util.List; +import java.util.concurrent.ScheduledExecutorService;  @RunWith(AndroidJUnit4.class)  @Presubmit @@ -97,6 +99,7 @@ public class ContextHubEndpointTest {      private HubInfoRegistry mHubInfoRegistry;      private ContextHubTransactionManager mTransactionManager;      private Context mContext; +    @Mock private ScheduledExecutorService mMockTimeoutExecutorService;      @Mock private IEndpointCommunication mMockEndpointCommunications;      @Mock private IContextHubWrapper mMockContextHubWrapper;      @Mock private IContextHubEndpointCallback mMockCallback; @@ -120,7 +123,11 @@ public class ContextHubEndpointTest {                          mMockContextHubWrapper, mClientManager, new NanoAppStateManager());          mEndpointManager =                  new ContextHubEndpointManager( -                        mContext, mMockContextHubWrapper, mHubInfoRegistry, mTransactionManager); +                        mContext, +                        mMockContextHubWrapper, +                        mHubInfoRegistry, +                        mTransactionManager, +                        mMockTimeoutExecutorService);          mEndpointManager.init();      } @@ -248,14 +255,20 @@ public class ContextHubEndpointTest {                  endpoint.getAssignedHubEndpointInfo().getIdentifier(),                  targetInfo.getIdentifier(),                  ENDPOINT_SERVICE_DESCRIPTOR); -          verify(mMockCallback)                  .onSessionOpenRequest(                          SESSION_ID_FOR_OPEN_REQUEST, targetInfo, ENDPOINT_SERVICE_DESCRIPTOR);          // Accept          endpoint.openSessionRequestComplete(SESSION_ID_FOR_OPEN_REQUEST); -        verify(mMockEndpointCommunications) + +        // Even when timeout happens, there should be no effect on this session +        ArgumentCaptor<Runnable> runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); +        verify(mMockTimeoutExecutorService) +                .schedule(runnableArgumentCaptor.capture(), anyLong(), any()); +        runnableArgumentCaptor.getValue().run(); + +        verify(mMockEndpointCommunications, times(1))                  .endpointSessionOpenComplete(SESSION_ID_FOR_OPEN_REQUEST);          unregisterExampleEndpoint(endpoint); @@ -331,6 +344,87 @@ public class ContextHubEndpointTest {      }      @Test +    public void testEndpointSessionOpenRequest_rejectAfterTimeout() throws RemoteException { +        assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); +        IContextHubEndpoint endpoint = registerExampleEndpoint(); + +        HubEndpointInfo targetInfo = +                new HubEndpointInfo( +                        TARGET_ENDPOINT_NAME, +                        TARGET_ENDPOINT_ID, +                        ENDPOINT_PACKAGE_NAME, +                        Collections.emptyList()); +        mHubInfoRegistry.onEndpointStarted(new HubEndpointInfo[] {targetInfo}); +        mEndpointManager.onEndpointSessionOpenRequest( +                SESSION_ID_FOR_OPEN_REQUEST, +                endpoint.getAssignedHubEndpointInfo().getIdentifier(), +                targetInfo.getIdentifier(), +                ENDPOINT_SERVICE_DESCRIPTOR); + +        // Immediately timeout +        ArgumentCaptor<Runnable> runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); +        verify(mMockTimeoutExecutorService) +                .schedule(runnableArgumentCaptor.capture(), anyLong(), any()); +        runnableArgumentCaptor.getValue().run(); + +        // Client's callback shouldn't matter after timeout +        try { +            endpoint.openSessionRequestComplete(SESSION_ID_FOR_OPEN_REQUEST); +        } catch (IllegalArgumentException ignore) { +            // This will throw because the session is no longer valid +        } + +        // HAL will receive closeEndpointSession with Timeout as reason +        verify(mMockEndpointCommunications, times(1)) +                .closeEndpointSession(SESSION_ID_FOR_OPEN_REQUEST, Reason.TIMEOUT); +        // HAL will not receives open complete notifications +        verify(mMockEndpointCommunications, never()) +                .endpointSessionOpenComplete(SESSION_ID_FOR_OPEN_REQUEST); + +        unregisterExampleEndpoint(endpoint); +    } + +    @Test +    public void testEndpointSessionOpenRequest_duplicatedSessionId_noopWithinTimeout() +            throws RemoteException { +        assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); +        IContextHubEndpoint endpoint = registerExampleEndpoint(); + +        HubEndpointInfo targetInfo = +                new HubEndpointInfo( +                        TARGET_ENDPOINT_NAME, +                        TARGET_ENDPOINT_ID, +                        ENDPOINT_PACKAGE_NAME, +                        Collections.emptyList()); +        mHubInfoRegistry.onEndpointStarted(new HubEndpointInfo[] {targetInfo}); +        mEndpointManager.onEndpointSessionOpenRequest( +                SESSION_ID_FOR_OPEN_REQUEST, +                endpoint.getAssignedHubEndpointInfo().getIdentifier(), +                targetInfo.getIdentifier(), +                ENDPOINT_SERVICE_DESCRIPTOR); + +        // Duplicated session open request +        mEndpointManager.onEndpointSessionOpenRequest( +                SESSION_ID_FOR_OPEN_REQUEST, +                endpoint.getAssignedHubEndpointInfo().getIdentifier(), +                targetInfo.getIdentifier(), +                ENDPOINT_SERVICE_DESCRIPTOR); + +        // Finally, endpoint approved the session open request +        endpoint.openSessionRequestComplete(SESSION_ID_FOR_OPEN_REQUEST); + +        // Client API is only invoked once +        verify(mMockCallback, times(1)) +                .onSessionOpenRequest( +                        SESSION_ID_FOR_OPEN_REQUEST, targetInfo, ENDPOINT_SERVICE_DESCRIPTOR); +        // HAL still receives two open complete notifications +        verify(mMockEndpointCommunications, times(1)) +                .endpointSessionOpenComplete(SESSION_ID_FOR_OPEN_REQUEST); + +        unregisterExampleEndpoint(endpoint); +    } + +    @Test      public void testMessageTransaction() throws RemoteException {          IContextHubEndpoint endpoint = registerExampleEndpoint();          testMessageTransactionInternal(endpoint, /* deliverMessageStatus= */ true); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 8c9b9bd03b9f..159b3fd7b5c3 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -24,6 +24,7 @@ import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.NOT_  import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.SHOW_IMMEDIATELY;  import static android.app.ActivityTaskManager.INVALID_TASK_ID;  import static android.app.Flags.FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS; +import static android.app.Flags.FLAG_NM_SUMMARIZATION;  import static android.app.Flags.FLAG_SORT_SECTION_BY_TIME;  import static android.app.Notification.EXTRA_ALLOW_DURING_SETUP;  import static android.app.Notification.EXTRA_PICTURE; @@ -105,6 +106,7 @@ import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;  import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;  import static android.service.notification.Adjustment.KEY_CONTEXTUAL_ACTIONS;  import static android.service.notification.Adjustment.KEY_IMPORTANCE; +import static android.service.notification.Adjustment.KEY_SUMMARIZATION;  import static android.service.notification.Adjustment.KEY_TEXT_REPLIES;  import static android.service.notification.Adjustment.KEY_TYPE;  import static android.service.notification.Adjustment.KEY_USER_SENTIMENT; @@ -18308,9 +18310,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {          // Post some notifications and classify in different bundles          final int numNotifications = NotificationChannel.SYSTEM_RESERVED_IDS.size();          final int numNewsNotifications = 1; +        List<String> postedNotificationKeys = new ArrayList();          for (int i = 0; i < numNotifications; i++) {              NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, i, mUserId);              mService.addNotification(r); +            postedNotificationKeys.add(r.getKey());              Bundle signals = new Bundle();              final int adjustmentType = i + 1;              signals.putInt(Adjustment.KEY_TYPE, adjustmentType); @@ -18330,7 +18334,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {          waitForIdle();          //Check that all notifications classified as TYPE_NEWS have been unbundled -        for (NotificationRecord record : mService.mNotificationList) { +        for (String key : postedNotificationKeys) { +            NotificationRecord record= mService.mNotificationsByKey.get(key);              // Check that the original channel was restored              // for notifications classified as TYPE_NEWS              if (record.getBundleType() == TYPE_NEWS) { @@ -18355,7 +18360,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {          // Check that the bundle channel was restored          verify(mRankingHandler, times(numNewsNotifications)).requestSort(); -        for (NotificationRecord record : mService.mNotificationList) { +        for (String key : postedNotificationKeys) { +            NotificationRecord record= mService.mNotificationsByKey.get(key);              assertThat(record.getChannel().getId()).isIn(NotificationChannel.SYSTEM_RESERVED_IDS);          }      } @@ -18425,6 +18431,36 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {      }      @Test +    @EnableFlags({FLAG_NM_SUMMARIZATION}) +    public void testDisableBundleAdjustmentByPkg_unsummarizesNotifications() throws Exception { +        NotificationManagerService.WorkerHandler handler = mock( +                NotificationManagerService.WorkerHandler.class); +        mService.setHandler(handler); +        when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); +        when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); +        when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true); +        when(mAssistants.isAdjustmentAllowedForPackage(anyString(), anyString())).thenReturn(true); + +        NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, mUserId); +        mService.addNotification(r); +        Bundle signals = new Bundle(); +        signals.putCharSequence(Adjustment.KEY_SUMMARIZATION, "hello"); +        Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, +                "", r.getUser().getIdentifier()); +        mBinderService.applyAdjustmentFromAssistant(null, adjustment); +        waitForIdle(); +        r.applyAdjustments(); +        Mockito.clearInvocations(mRankingHandler); + +        // Disable summarization for package +        mBinderService.setAdjustmentSupportedForPackage(KEY_SUMMARIZATION, mPkg, false); +        verify(mRankingHandler).requestSort(); +        mService.handleRankingSort(); + +        assertThat(mService.mNotificationsByKey.get(r.getKey()).getSummarization()).isNull(); +    } + +    @Test      @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION,              FLAG_NOTIFICATION_FORCE_GROUPING,              FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION, @@ -18627,6 +18663,36 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {      }      @Test +    @EnableFlags({FLAG_NM_SUMMARIZATION}) +    public void testDisableBundleAdjustment_unsummarizesNotifications() throws Exception { +        NotificationManagerService.WorkerHandler handler = mock( +                NotificationManagerService.WorkerHandler.class); +        mService.setHandler(handler); +        when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); +        when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); +        when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true); +        when(mAssistants.isAdjustmentAllowedForPackage(anyString(), anyString())).thenReturn(true); + +        NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, mUserId); +        mService.addNotification(r); +        Bundle signals = new Bundle(); +        signals.putCharSequence(Adjustment.KEY_SUMMARIZATION, "hello"); +        Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, +                "", r.getUser().getIdentifier()); +        mBinderService.applyAdjustmentFromAssistant(null, adjustment); +        waitForIdle(); +        r.applyAdjustments(); +        Mockito.clearInvocations(mRankingHandler); + +        // Disable summarization for package +        mBinderService.disallowAssistantAdjustment(KEY_SUMMARIZATION); +        verify(mRankingHandler).requestSort(); +        mService.handleRankingSort(); + +        assertThat(mService.mNotificationsByKey.get(r.getKey()).getSummarization()).isNull(); +    } + +    @Test      @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)      public void clearAll_fromUser_willSendDeleteIntentForCachedSummaries() throws Exception {          NotificationRecord n = generateNotificationRecord( diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java index 1cb1e3cae413..ec264034871a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java @@ -25,6 +25,7 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT;  import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;  import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;  import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.display.feature.flags.Flags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT;  import static com.android.server.wm.ActivityInterceptorCallback.MAINLINE_FIRST_ORDERED_ID;  import static com.android.server.wm.ActivityInterceptorCallback.SYSTEM_FIRST_ORDERED_ID;  import static com.android.server.wm.ActivityInterceptorCallback.SYSTEM_LAST_ORDERED_ID; @@ -67,6 +68,7 @@ import android.os.LocaleList;  import android.os.PowerManagerInternal;  import android.os.RemoteException;  import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags;  import android.platform.test.annotations.Presubmit;  import android.view.Display;  import android.view.DisplayInfo; @@ -75,6 +77,8 @@ import android.view.WindowManager;  import androidx.test.filters.MediumTest; +import com.android.server.UiThread; +  import org.junit.Before;  import org.junit.Test;  import org.junit.runner.RunWith; @@ -164,11 +168,13 @@ public class ActivityTaskManagerServiceTests extends WindowTestsBase {          verify(mClientLifecycleManager, never()).scheduleTransactionItem(any(), any());      } +    @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT)      @Test      public void testDisplayWindowListener() {          final ArrayList<Integer> added = new ArrayList<>();          final ArrayList<Integer> changed = new ArrayList<>();          final ArrayList<Integer> removed = new ArrayList<>(); +        final ArrayList<Integer> desktopModeEligibleChanged = new ArrayList<>();          IDisplayWindowListener listener = new IDisplayWindowListener.Stub() {              @Override              public void onDisplayAdded(int displayId) { @@ -194,6 +200,11 @@ public class ActivityTaskManagerServiceTests extends WindowTestsBase {              @Override              public void onKeepClearAreasChanged(int displayId, List<Rect> restricted,                      List<Rect> unrestricted) {} + +            @Override +            public void onDesktopModeEligibleChanged(int displayId) { +                desktopModeEligibleChanged.add(displayId); +            }          };          int[] displayIds = mAtm.mWindowManager.registerDisplayWindowListener(listener);          for (int i = 0; i < displayIds.length; i++) { @@ -218,7 +229,25 @@ public class ActivityTaskManagerServiceTests extends WindowTestsBase {          assertEquals(1, changed.size());          assertEquals(0, removed.size());          changed.clear(); + +        // Check adding decoration +        doReturn(true).when(newDisp1).allowContentModeSwitch(); +        doReturn(true).when(newDisp1).isSystemDecorationsSupported(); +        mAtm.mWindowManager.setShouldShowSystemDecors(newDisp1.mDisplayId, true); +        waitHandlerIdle(UiThread.getHandler()); +        assertEquals(1, desktopModeEligibleChanged.size()); +        assertEquals(newDisp1.mDisplayId, (int) desktopModeEligibleChanged.get(0)); +        desktopModeEligibleChanged.clear(); +        // Check removing decoration +        doReturn(false).when(newDisp1).isSystemDecorationsSupported(); +        mAtm.mWindowManager.setShouldShowSystemDecors(newDisp1.mDisplayId, false); +        waitHandlerIdle(UiThread.getHandler()); +        assertEquals(1, desktopModeEligibleChanged.size()); +        assertEquals(newDisp1.mDisplayId, (int) desktopModeEligibleChanged.get(0)); +        desktopModeEligibleChanged.clear(); +          // Check that removal is reported +        changed.clear();          newDisp1.remove();          assertEquals(0, added.size());          assertEquals(0, changed.size()); diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index f587d6e8c346..678230564b25 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -21,7 +21,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;  import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;  import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;  import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;  import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE;  import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_MEDIUM_VALUE;  import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_SMALL_VALUE; @@ -282,6 +282,7 @@ public class DesktopModeLaunchParamsModifierTests extends          final DisplayContent dc = spy(createNewDisplay());          final Task existingFreeformTask = new TaskBuilder(mSupervisor).setCreateActivity(true)                  .setWindowingMode(WINDOWING_MODE_FREEFORM).setPackage(packageName).build(); +        existingFreeformTask.topRunningActivity().launchMode = LAUNCH_SINGLE_INSTANCE;          existingFreeformTask.setBounds(                  /* left */ 0,                  /* top */ 0, @@ -293,8 +294,8 @@ public class DesktopModeLaunchParamsModifierTests extends          // so first instance will close.          final Task launchingTask = new TaskBuilder(mSupervisor).setPackage(packageName)                  .setCreateActivity(true).build(); +        launchingTask.topRunningActivity().launchMode = LAUNCH_SINGLE_INSTANCE;          launchingTask.onDisplayChanged(dc); -        launchingTask.intent.addFlags(FLAG_ACTIVITY_NEW_TASK);          // New instance should inherit task bounds of old instance.          assertEquals(RESULT_DONE, diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index 5624677779a2..fa77e42611e1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -562,26 +562,6 @@ public class WindowStateTests extends WindowTestsBase {      }      @Test -    public void testDeferredRemovalByAnimating() { -        final WindowState appWindow = newWindowBuilder("appWindow", TYPE_APPLICATION).build(); -        makeWindowVisible(appWindow); -        spyOn(appWindow.mWinAnimator); -        doReturn(true).when(appWindow.mWinAnimator).getShown(); -        final AnimationAdapter animation = mock(AnimationAdapter.class); -        final ActivityRecord activity = appWindow.mActivityRecord; -        activity.startAnimation(appWindow.getPendingTransaction(), -                animation, false /* hidden */, SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION); - -        appWindow.removeIfPossible(); -        assertTrue(appWindow.mAnimatingExit); -        assertFalse(appWindow.mRemoved); - -        activity.cancelAnimation(); -        assertFalse(appWindow.mAnimatingExit); -        assertTrue(appWindow.mRemoved); -    } - -    @Test      public void testOnExitAnimationDone() {          final WindowState parent = newWindowBuilder("parent", TYPE_APPLICATION).build();          final WindowState child = newWindowBuilder("child", TYPE_APPLICATION_PANEL).setParent( diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index d7f80a94081a..2095ee83b77d 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3135,6 +3135,16 @@ interface ITelephony {      boolean setSatelliteIgnoreCellularServiceState(in boolean enabled);      /** +     * This API can be used by only CTS to control the feature +     * {@code config_support_disable_satellite_while_enable_in_progress}. +     * +     * @param reset Whether to reset the override. +     * @param supported Whether to support the feature. +     * @return {@code true} if the value is set successfully, {@code false} otherwise. +     */ +    boolean setSupportDisableSatelliteWhileEnableInProgress(boolean reset, boolean supported); + +    /**       * This API can be used by only CTS to update satellite pointing UI app package and class names.       *       * @param packageName The package name of the satellite pointing UI app. diff --git a/vendor/google_testing/integration/tests/scenarios/screenshots/cuttlefish/flexiglass/six_digits_pin_delete.png b/vendor/google_testing/integration/tests/scenarios/screenshots/cuttlefish/flexiglass/six_digits_pin_delete.png Binary files differnew file mode 100644 index 000000000000..d4e72fbd6e2f --- /dev/null +++ b/vendor/google_testing/integration/tests/scenarios/screenshots/cuttlefish/flexiglass/six_digits_pin_delete.png  |