diff options
135 files changed, 3932 insertions, 1367 deletions
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 70a2e5382f60..aa947eb21b10 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -3882,7 +3882,9 @@ public class Activity extends ContextThemeWrapper * it will set up the dispatch to call {@link #onKeyUp} where the action * will be performed; for earlier applications, it will perform the * action immediately in on-down, as those versions of the platform - * behaved. + * behaved. This implementation will also take care of {@link KeyEvent#KEYCODE_ESCAPE} + * by finishing the activity if it would be closed by touching outside + * of it. * * <p>Other additional default key handling may be performed * if configured with {@link #setDefaultKeyMode}. @@ -3904,6 +3906,11 @@ public class Activity extends ContextThemeWrapper return true; } + if (keyCode == KeyEvent.KEYCODE_ESCAPE && mWindow.shouldCloseOnTouchOutside()) { + event.startTracking(); + return true; + } + if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) { return false; } else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) { @@ -3999,6 +4006,15 @@ public class Activity extends ContextThemeWrapper return true; } } + + if (keyCode == KeyEvent.KEYCODE_ESCAPE + && mWindow.shouldCloseOnTouchOutside() + && event.isTracking() + && !event.isCanceled()) { + finish(); + return true; + } + return false; } diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index 411d157fa927..4851279eea97 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -694,12 +694,22 @@ public class Dialog implements DialogInterface, Window.Callback, */ @Override public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { - if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) - && event.isTracking() - && !event.isCanceled() - && !WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) { - onBackPressed(); - return true; + if (event.isTracking() && !event.isCanceled()) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) { + onBackPressed(); + return true; + } + break; + case KeyEvent.KEYCODE_ESCAPE: + if (mCancelable) { + cancel(); + } else { + dismiss(); + } + return true; + } } return false; } diff --git a/core/java/android/app/servertransaction/TransactionExecutorHelper.java b/core/java/android/app/servertransaction/TransactionExecutorHelper.java index 5311b09e609d..baf2a4722dff 100644 --- a/core/java/android/app/servertransaction/TransactionExecutorHelper.java +++ b/core/java/android/app/servertransaction/TransactionExecutorHelper.java @@ -193,8 +193,8 @@ public class TransactionExecutorHelper { switch (prevState) { // TODO(lifecycler): Extend to support all possible states. case ON_START: - lifecycleItem = StartActivityItem.obtain(null /* activityOptions */); - break; + // Fall through to return the PAUSE item to ensure the activity is properly + // resumed while relaunching. case ON_PAUSE: lifecycleItem = PauseActivityItem.obtain(); break; diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index d695c0cb3760..d425bf8ae557 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7486,6 +7486,14 @@ public final class Settings { public static final String STYLUS_BUTTONS_ENABLED = "stylus_buttons_enabled"; /** + * Preferred default user profile to use with the notes task button shortcut. + * + * @hide + */ + @SuppressLint("NoSettingsProvider") + public static final String DEFAULT_NOTE_TASK_PROFILE = "default_note_task_profile"; + + /** * Host name and port for global http proxy. Uses ':' seperator for * between host and port. * @@ -17979,6 +17987,15 @@ public final class Settings { "review_permissions_notification_state"; /** + * Whether repair mode is active on the device. + * <p> + * Set to 1 for true and 0 for false. + * + * @hide + */ + public static final String REPAIR_MODE_ACTIVE = "repair_mode_active"; + + /** * Settings migrated from Wear OS settings provider. * @hide */ diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index d4578475e9c3..01a99b92a055 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -2043,7 +2043,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final float x = event.getXDispatchLocation(pointerIndex); final float y = event.getYDispatchLocation(pointerIndex); if (isOnScrollbarThumb(x, y) || isDraggingScrollBar()) { - return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_ARROW); + // Return null here so that it fallbacks to the default PointerIcon for the source + // device. For mouse, the default PointerIcon is PointerIcon.TYPE_ARROW. + // For stylus, the default PointerIcon is PointerIcon.TYPE_NULL. + return null; } // Check what the child under the pointer says about the pointer. final int childrenCount = mChildrenCount; diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 21fe87f42e1b..7596459b130b 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -1482,6 +1482,11 @@ public abstract class Window { } /** @hide */ + public boolean shouldCloseOnTouchOutside() { + return mCloseOnTouchOutside; + } + + /** @hide */ @SuppressWarnings("HiddenAbstractMethod") @UnsupportedAppUsage public abstract void alwaysReadCloseOnTouchAttr(); diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index adeb88909d38..6ad1960cbda9 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -4703,7 +4703,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { - if (mFastScroll != null) { + if (mFastScroll != null && event.isFromSource(InputDevice.SOURCE_MOUSE)) { PointerIcon pointerIcon = mFastScroll.onResolvePointerIcon(event, pointerIndex); if (pointerIcon != null) { return pointerIcon; diff --git a/core/java/android/widget/Button.java b/core/java/android/widget/Button.java index 634cbe323d86..405099d6a260 100644 --- a/core/java/android/widget/Button.java +++ b/core/java/android/widget/Button.java @@ -18,6 +18,7 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; +import android.view.InputDevice; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.PointerIcon; @@ -173,7 +174,8 @@ public class Button extends TextView { @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { - if (getPointerIcon() == null && isClickable() && isEnabled()) { + if (getPointerIcon() == null && isClickable() && isEnabled() + && event.isFromSource(InputDevice.SOURCE_MOUSE)) { return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND); } return super.onResolvePointerIcon(event, pointerIndex); diff --git a/core/java/android/widget/ImageButton.java b/core/java/android/widget/ImageButton.java index e1b0c915c684..b6c5396ca176 100644 --- a/core/java/android/widget/ImageButton.java +++ b/core/java/android/widget/ImageButton.java @@ -18,6 +18,7 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; +import android.view.InputDevice; import android.view.MotionEvent; import android.view.PointerIcon; import android.widget.RemoteViews.RemoteView; @@ -99,7 +100,8 @@ public class ImageButton extends ImageView { @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { - if (getPointerIcon() == null && isClickable() && isEnabled()) { + if (getPointerIcon() == null && isClickable() && isEnabled() + && event.isFromSource(InputDevice.SOURCE_MOUSE)) { return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND); } return super.onResolvePointerIcon(event, pointerIndex); diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java index f3600b0de22b..edf0f48c3577 100644 --- a/core/java/android/widget/RadialTimePickerView.java +++ b/core/java/android/widget/RadialTimePickerView.java @@ -38,6 +38,7 @@ import android.util.MathUtils; import android.util.StateSet; import android.util.TypedValue; import android.view.HapticFeedbackConstants; +import android.view.InputDevice; import android.view.MotionEvent; import android.view.PointerIcon; import android.view.View; @@ -1060,9 +1061,11 @@ public class RadialTimePickerView extends View { if (!isEnabled()) { return null; } - final int degrees = getDegreesFromXY(event.getX(), event.getY(), false); - if (degrees != -1) { - return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND); + if (event.isFromSource(InputDevice.SOURCE_MOUSE)) { + final int degrees = getDegreesFromXY(event.getX(), event.getY(), false); + if (degrees != -1) { + return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND); + } } return super.onResolvePointerIcon(event, pointerIndex); } diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java index 6c53a44c79fa..1317b51f7bfa 100644 --- a/core/java/android/widget/SimpleMonthView.java +++ b/core/java/android/widget/SimpleMonthView.java @@ -39,6 +39,7 @@ import android.util.AttributeSet; import android.util.IntArray; import android.util.MathUtils; import android.util.StateSet; +import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.PointerIcon; @@ -1041,12 +1042,15 @@ class SimpleMonthView extends View { if (!isEnabled()) { return null; } - // Add 0.5f to event coordinates to match the logic in onTouchEvent. - final int x = (int) (event.getX() + 0.5f); - final int y = (int) (event.getY() + 0.5f); - final int dayUnderPointer = getDayAtLocation(x, y); - if (dayUnderPointer >= 0) { - return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND); + + if (event.isFromSource(InputDevice.SOURCE_MOUSE)) { + // Add 0.5f to event coordinates to match the logic in onTouchEvent. + final int x = (int) (event.getX() + 0.5f); + final int y = (int) (event.getY() + 0.5f); + final int dayUnderPointer = getDayAtLocation(x, y); + if (dayUnderPointer >= 0) { + return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND); + } } return super.onResolvePointerIcon(event, pointerIndex); } diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java index ad431efc0bd2..ecc41a5ec6c9 100644 --- a/core/java/android/widget/Spinner.java +++ b/core/java/android/widget/Spinner.java @@ -38,6 +38,7 @@ import android.util.AttributeSet; import android.util.Log; import android.view.ContextThemeWrapper; import android.view.Gravity; +import android.view.InputDevice; import android.view.MotionEvent; import android.view.PointerIcon; import android.view.View; @@ -935,7 +936,8 @@ public class Spinner extends AbsSpinner implements OnClickListener { @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { - if (getPointerIcon() == null && isClickable() && isEnabled()) { + if (getPointerIcon() == null && isClickable() && isEnabled() + && event.isFromSource(InputDevice.SOURCE_MOUSE)) { return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND); } return super.onResolvePointerIcon(event, pointerIndex); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index db7d48471d9d..7e1e52dd0707 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -9223,18 +9223,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { - if (mSpannable != null && mLinksClickable) { - final float x = event.getX(pointerIndex); - final float y = event.getY(pointerIndex); - final int offset = getOffsetForPosition(x, y); - final ClickableSpan[] clickables = mSpannable.getSpans(offset, offset, - ClickableSpan.class); - if (clickables.length > 0) { - return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HAND); - } - } - if (isTextSelectable() || isTextEditable()) { - return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_TEXT); + if (event.isFromSource(InputDevice.SOURCE_MOUSE)) { + if (mSpannable != null && mLinksClickable) { + final float x = event.getX(pointerIndex); + final float y = event.getY(pointerIndex); + final int offset = getOffsetForPosition(x, y); + final ClickableSpan[] clickables = mSpannable.getSpans(offset, offset, + ClickableSpan.class); + if (clickables.length > 0) { + return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HAND); + } + } + if (isTextSelectable() || isTextEditable()) { + return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_TEXT); + } } return super.onResolvePointerIcon(event, pointerIndex); } diff --git a/core/java/com/android/internal/statusbar/IAppClipsService.aidl b/core/java/com/android/internal/statusbar/IAppClipsService.aidl index 013d0d32e7a2..d6ab8bcdde20 100644 --- a/core/java/com/android/internal/statusbar/IAppClipsService.aidl +++ b/core/java/com/android/internal/statusbar/IAppClipsService.aidl @@ -23,4 +23,6 @@ package com.android.internal.statusbar; */ interface IAppClipsService { boolean canLaunchCaptureContentActivityForNote(in int taskId); -}
\ No newline at end of file + + int canLaunchCaptureContentActivityForNoteInternal(in int taskId); +} diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 4ef9efddac79..4f905fc29f58 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6198,7 +6198,7 @@ <!-- Flag indicating whether the show Stylus pointer icon. If set, a pointer icon will be shown over the location of a stylus pointer.--> - <bool name="config_enableStylusPointerIcon">false</bool> + <bool name="config_enableStylusPointerIcon">true</bool> <!-- Determines whether SafetyCenter feature is enabled. --> <bool name="config_enableSafetyCenter">true</bool> @@ -6459,4 +6459,9 @@ <!-- Whether the AOSP support for app cloning building blocks is to be enabled for the device. --> <bool name="config_enableAppCloningBuildingBlocks">true</bool> + + <!-- Enables or disables support for repair mode. The feature creates a secure + environment to protect the user's privacy when the device is being repaired. + Off by default, since OEMs may have had a similar feature on their devices. --> + <bool name="config_repairModeSupported">false</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index d374bddddc73..c446c3e17288 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4912,6 +4912,8 @@ <java-symbol type="bool" name="config_safetyProtectionEnabled" /> + <java-symbol type="bool" name="config_repairModeSupported" /> + <java-symbol type="string" name="config_devicePolicyManagementUpdater" /> <java-symbol type="string" name="config_deviceSpecificDeviceStatePolicyProvider" /> diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index c1deba3288e5..129de649a4b5 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -1716,6 +1716,17 @@ <meta-data android:name="android.view.im" android:resource="@xml/ime_meta_handwriting"/> </service> + + <activity android:name="android.widget.PointerIconTestActivity" + android:label="PointerIconTestActivity" + android:screenOrientation="portrait" + android:exported="true" + android:theme="@android:style/Theme.Material.Light"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> + </intent-filter> + </activity> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/core/tests/coretests/res/layout/pointer_icon_test.xml b/core/tests/coretests/res/layout/pointer_icon_test.xml new file mode 100644 index 000000000000..a2a64473ae23 --- /dev/null +++ b/core/tests/coretests/res/layout/pointer_icon_test.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 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:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <TextView + android:id="@+id/textview" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Test"/> + + <EditText + android:id="@+id/edittext" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Test"/> + + <Button + android:id="@+id/button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Test"/> + + <ImageButton + android:id="@+id/imagebutton" + android:layout_width="50dp" + android:layout_height="50dp"/> + + <Spinner + android:id="@+id/spinner" + android:layout_width="50dp" + android:layout_height="50dp"/> + + <RadialTimePickerView + android:id="@+id/timepicker" + android:layout_width="200dp" + android:layout_height="200dp"/> + + <CalendarView + android:id="@+id/calendar" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> +</LinearLayout>
\ No newline at end of file diff --git a/core/tests/coretests/src/android/widget/PointerIconTest.java b/core/tests/coretests/src/android/widget/PointerIconTest.java new file mode 100644 index 000000000000..8e9e1a50d8c9 --- /dev/null +++ b/core/tests/coretests/src/android/widget/PointerIconTest.java @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2023 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 android.widget; + +import static com.google.common.truth.Truth.assertThat; + +import android.annotation.UiThread; +import android.app.Instrumentation; +import android.graphics.Rect; +import android.os.SystemClock; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.PointerIcon; +import android.view.View; + +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.frameworks.coretests.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class PointerIconTest { + private Instrumentation mInstrumentation; + + @Before + public void setup() { + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + } + + @Rule + public ActivityScenarioRule<PointerIconTestActivity> mActivityScenarioRule = + new ActivityScenarioRule<>(PointerIconTestActivity.class); + + @Test + @UiThread + public void button_mouse_onResolvePointerIcon_returnsTypeHand() { + assertOnResolvePointerIconForMouseEvent(R.id.button, PointerIcon.TYPE_HAND); + } + + @Test + @UiThread + public void button_mouse_disabled_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.button, /* enabled */ false, /* clickable */ true, + /* isMouse */ true); + } + + @Test + @UiThread + public void button_mouse_unclickable_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.button, /* enabled */ true, /* clickable */ false, + /* isMouse */ true); + } + + @Test + @UiThread + public void button_stylus_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.button, /* enabled */ true, /* clickable */ true, + /* isMouse */ false); + } + + @Test + @UiThread + public void imageButton_mouse_onResolvePointerIconreturnsTypeHand() { + assertOnResolvePointerIconForMouseEvent(R.id.imagebutton, PointerIcon.TYPE_HAND); + } + + @Test + @UiThread + public void imageButton_mouse_diabled_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.imagebutton, /* enabled */ false, + /* clickable */ true, /* isMouse */ true); + } + + @Test + @UiThread + public void imageButton_mouse_unclickable_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.imagebutton, /* enabled */ true, + /* clickable */ false, /* isMouse */ true); + } + + @Test + @UiThread + public void imageButton_stylus_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.imagebutton, /* enabled */ true, + /* clickable */ true, /* isMouse */ false); + } + + @Test + @UiThread + public void textView_mouse_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.textview, /* enabled */ true, + /* clickable */ true, /* isMouse */ true); + } + + @Test + @UiThread + public void textView_stylus_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.textview, /* enabled */ true, + /* clickable */ true, /* isMouse */ false); + } + + @Test + @UiThread + public void editText_mouse_onResolvePointerIcon_returnsTypeText() { + assertOnResolvePointerIconForMouseEvent(R.id.edittext, PointerIcon.TYPE_TEXT); + } + + @Test + @UiThread + public void editText_stylus_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.edittext, /* enabled */ true, + /* clickable */ true, /* isMouse */ false); + } + + @Test + @UiThread + public void spinner_mouse_onResolvePointerIcon_returnsTypeHand() { + assertOnResolvePointerIconForMouseEvent(R.id.spinner, PointerIcon.TYPE_HAND); + } + + @Test + @UiThread + public void spinner_mouse_disabled_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.spinner, /* enabled */ false, + /* clickable */ true, /* isMouse */ true); + } + + @Test + @UiThread + public void spinner_mouse_unclickable_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.spinner, /* enabled */ true, + /* clickable */ false, /* isMouse */ true); + } + + @Test + @UiThread + public void spinner_stylus_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.spinner, /* enabled */ true, /* clickable */ true, + /* isMouse */ false); + } + + @Test + @UiThread + public void radialTimePickerView_mouse_onResolvePointerIcon_returnsTypeHand() { + assertOnResolvePointerIconForMouseEvent(R.id.timepicker, PointerIcon.TYPE_HAND); + + } + + @Test + @UiThread + public void radialTimePickerView_mouse_disabled_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.timepicker, /* enabled */ false, + /* clickable */ true, /* isMouse */ true); + } + + @Test + @UiThread + public void radialTimePickerView_stylus_onResolvePointerIcon_returnsNull() { + assertOnResolvePointerIconReturnNull(R.id.timepicker, /* enabled */ true, + /* clickable */ true, /* isMouse */ false); + } + + @Test + @UiThread + public void calendarView_mouse_onResolvePointerIcon_returnsTypeHand() { + assertPointerIconForCalendarView(/* pointerType */ PointerIcon.TYPE_HAND, + /* isMouse */ true); + } + + @Test + @UiThread + public void calendarView_stylus_onResolvePointerIcon_returnsNull() { + assertPointerIconForCalendarView(/* pointerType */ Integer.MIN_VALUE, /* isMouse */ false); + } + + /** + * Assert {@link View#onResolvePointerIcon} method for {@link CalendarView}. + * + * @param pointerType the expected type of the {@link PointerIcon}. + * When {@link Integer#MIN_VALUE} is passed, it will verify that the + * returned {@link PointerIcon} is null. + * @param isMouse if true, mouse events are used to test the given view. Otherwise, it uses + * stylus events to test the view. + */ + void assertPointerIconForCalendarView(int pointerType, boolean isMouse) { + Calendar calendar = new GregorianCalendar(); + calendar.set(2023, 0, 1); + long time = calendar.getTimeInMillis(); + mActivityScenarioRule.getScenario().onActivity(activity -> { + CalendarView calendarView = activity.findViewById(R.id.calendar); + calendarView.setDate(time, /* animate */ false, /* center */true); + }); + + // Wait for setDate to finish and then verify. + mInstrumentation.waitForIdleSync(); + mActivityScenarioRule.getScenario().onActivity(activity -> { + CalendarView calendarView = activity.findViewById(R.id.calendar); + Rect bounds = new Rect(); + calendarView.getBoundsForDate(time, bounds); + MotionEvent event = createHoverEvent(isMouse, bounds.centerX(), bounds.centerY()); + PointerIcon icon = calendarView.onResolvePointerIcon(event, /* pointerIndex */ 0); + if (pointerType != Integer.MIN_VALUE) { + assertThat(icon.getType()).isEqualTo(pointerType); + } else { + assertThat(icon).isNull(); + } + }); + } + + /** + * Assert that the given view's {@link View#onResolvePointerIcon(MotionEvent, int)} method + * returns a {@link PointerIcon} with the specified pointer type. The passed {@link MotionEvent} + * locates at the center of the view. + * + * @param resId the resource id of the view to be tested. + * @param pointerType the expected pointer type. When {@link Integer#MIN_VALUE} is passed, it + * will verify that the returned {@link PointerIcon} is null. + */ + public void assertOnResolvePointerIconForMouseEvent(int resId, int pointerType) { + mActivityScenarioRule.getScenario().onActivity(activity -> { + View view = activity.findViewById(resId); + MotionEvent event = createHoverEvent(/* isMouse */ true, /* x */ 0, /* y */ 0); + PointerIcon icon = view.onResolvePointerIcon(event, /* pointerIndex */ 0); + if (pointerType != Integer.MIN_VALUE) { + assertThat(icon.getType()).isEqualTo(pointerType); + } else { + assertThat(icon).isNull(); + } + }); + } + + /** + * Assert that the given view's {@link View#onResolvePointerIcon(MotionEvent, int)} method + * returns a {@link PointerIcon} with the specified pointer type. The passed {@link MotionEvent} + * locates at the center of the view. + * + * @param resId the resource id of the view to be tested. + * @param enabled whether the tested view is enabled. + * @param clickable whether the tested view is clickable. + * @param isMouse if true, mouse events are used to test the given view. Otherwise, it uses + * stylus events to test the view. + */ + public void assertOnResolvePointerIconReturnNull(int resId, boolean enabled, boolean clickable, + boolean isMouse) { + mActivityScenarioRule.getScenario().onActivity(activity -> { + View view = activity.findViewById(resId); + view.setEnabled(enabled); + view.setClickable(clickable); + MotionEvent event = createHoverEvent(isMouse, /* x */ 0, /* y */ 0); + PointerIcon icon = view.onResolvePointerIcon(event, /* pointerIndex */ 0); + assertThat(icon).isNull(); + }); + } + + + /** + * Create a hover {@link MotionEvent} for testing. + * + * @param isMouse if true, a {@link MotionEvent} from mouse is returned. Otherwise, + * a {@link MotionEvent} from stylus is returned. + * @param x the x coordinate of the returned {@link MotionEvent} + * @param y the y coordinate of the returned {@link MotionEvent} + */ + private MotionEvent createHoverEvent(boolean isMouse, int x, int y) { + MotionEvent.PointerProperties[] properties = MotionEvent.PointerProperties.createArray(1); + properties[0].toolType = + isMouse ? MotionEvent.TOOL_TYPE_MOUSE : MotionEvent.TOOL_TYPE_STYLUS; + + MotionEvent.PointerCoords[] coords = MotionEvent.PointerCoords.createArray(1); + coords[0].x = x; + coords[0].y = y; + + int source = isMouse ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_STYLUS; + long eventTime = SystemClock.uptimeMillis(); + return MotionEvent.obtain(/* downTime */ 0, eventTime, MotionEvent.ACTION_HOVER_MOVE, + /* pointerCount */ 1, properties, coords, /* metaState */ 0, /* buttonState */ 0, + /* xPrecision */ 1, /* yPrecision */ 1, /* deviceId */ 0, /* edgeFlags */ 0, + source, /* flags */ 0); + } + +} diff --git a/core/tests/coretests/src/android/widget/PointerIconTestActivity.java b/core/tests/coretests/src/android/widget/PointerIconTestActivity.java new file mode 100644 index 000000000000..d491fb051e30 --- /dev/null +++ b/core/tests/coretests/src/android/widget/PointerIconTestActivity.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 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 android.widget; + +import android.app.Activity; +import android.os.Bundle; + +import com.android.frameworks.coretests.R; + +public class PointerIconTestActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.pointer_icon_test); + + RadialTimePickerView timePicker = findViewById(R.id.timepicker); + // Set the time of TimePicker to 0:00 so that the test is stable. + timePicker.setCurrentHour(0); + timePicker.setCurrentMinute(0); + } +} diff --git a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java index be2c27de637c..3e0e36d6f3b8 100644 --- a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java +++ b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java @@ -181,7 +181,7 @@ public class ActivityThreadClientTest { // Verify for ON_START state. Activity should be relaunched. getInstrumentation().runOnMainSync(() -> clientSession.startActivity(r)); - recreateAndVerifyRelaunched(activityThread, activity[0], r, ON_START); + recreateAndVerifyRelaunched(activityThread, activity[0], r, ON_PAUSE); // Verify for ON_RESUME state. Activity should be relaunched. getInstrumentation().runOnMainSync(() -> clientSession.resumeActivity(r)); diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml index 171a6b2fe5fb..6f8a7666e5a8 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -30,6 +30,8 @@ <color name="bubbles_light">#FFFFFF</color> <color name="bubbles_dark">@color/GM2_grey_800</color> <color name="bubbles_icon_tint">@color/GM2_grey_700</color> + <color name="bubble_bar_expanded_view_handle_light">#EBffffff</color> + <color name="bubble_bar_expanded_view_handle_dark">#99000000</color> <!-- PiP --> <color name="pip_custom_close_bg">#D93025</color> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 2be34c90a661..aa05179ed113 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -229,7 +229,11 @@ <!-- Size of the bubble bar (height), should match transient_taskbar_size in Launcher. --> <dimen name="bubblebar_size">72dp</dimen> <!-- The size of the drag handle / menu shown along with a bubble bar expanded view. --> - <dimen name="bubblebar_expanded_view_menu_size">16dp</dimen> + <dimen name="bubble_bar_expanded_view_handle_size">40dp</dimen> + <!-- The width of the drag handle shown along with a bubble bar expanded view. --> + <dimen name="bubble_bar_expanded_view_handle_width">128dp</dimen> + <!-- The height of the drag handle shown along with a bubble bar expanded view. --> + <dimen name="bubble_bar_expanded_view_handle_height">4dp</dimen> <!-- Bottom and end margin for compat buttons. --> <dimen name="compat_button_margin">24dp</dimen> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index b8f049becb6f..da1a5572e53b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles.bar; +import android.annotation.ColorInt; +import android.annotation.Nullable; import android.app.ActivityManager; import android.content.Context; import android.content.res.TypedArray; @@ -46,10 +48,10 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private BubbleController mController; private BubbleTaskViewHelper mBubbleTaskViewHelper; - private HandleView mMenuView; - private TaskView mTaskView; + private BubbleBarHandleView mHandleView = new BubbleBarHandleView(getContext()); + private @Nullable TaskView mTaskView; - private int mMenuHeight; + private int mHandleHeight; private int mBackgroundColor; private float mCornerRadius = 0f; @@ -83,11 +85,9 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView super.onFinishInflate(); Context context = getContext(); setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation)); - mMenuHeight = context.getResources().getDimensionPixelSize( - R.dimen.bubblebar_expanded_view_menu_size); - mMenuView = new HandleView(context); - addView(mMenuView); - + mHandleHeight = context.getResources().getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_handle_size); + addView(mHandleView); applyThemeAttrs(); setClipToOutline(true); setOutlineProvider(new ViewOutlineProvider() { @@ -114,7 +114,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView void applyThemeAttrs() { boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( mContext.getResources()); - final TypedArray ta = mContext.obtainStyledAttributes(new int[] { + final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ android.R.attr.dialogCornerRadius, android.R.attr.colorBackgroundFloating}); mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0; @@ -123,14 +123,12 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView ta.recycle(); - mMenuView.setCornerRadius(mCornerRadius); - mMenuHeight = getResources().getDimensionPixelSize( - R.dimen.bubblebar_expanded_view_menu_size); + mHandleHeight = getResources().getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_handle_size); if (mTaskView != null) { mTaskView.setCornerRadius(mCornerRadius); - mTaskView.setElevation(150); - updateMenuColor(); + updateHandleAndBackgroundColor(true /* animated */); } } @@ -138,10 +136,8 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); - - // Add corner radius here so that the menu extends behind the rounded corners of TaskView. - int menuViewHeight = Math.min((int) (mMenuHeight + mCornerRadius), height); - measureChild(mMenuView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight, + int menuViewHeight = Math.min(mHandleHeight, height); + measureChild(mHandleView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight, MeasureSpec.getMode(heightMeasureSpec))); if (mTaskView != null) { @@ -153,12 +149,12 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); // Drag handle above - final int dragHandleBottom = t + mMenuView.getMeasuredHeight(); - mMenuView.layout(l, t, r, dragHandleBottom); + final int dragHandleBottom = t + mHandleView.getMeasuredHeight(); + mHandleView.layout(l, t, r, dragHandleBottom); if (mTaskView != null) { - // Subtract radius so that the menu extends behind the rounded corners of TaskView. - mTaskView.layout(l, (int) (dragHandleBottom - mCornerRadius), r, + mTaskView.layout(l, dragHandleBottom, r, dragHandleBottom + mTaskView.getMeasuredHeight()); } } @@ -166,7 +162,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView @Override public void onTaskCreated() { setContentVisibility(true); - updateMenuColor(); + updateHandleAndBackgroundColor(false /* animated */); } @Override @@ -218,16 +214,33 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView } } - /** Updates the menu bar to be the status bar color specified by the app. */ - private void updateMenuColor() { + /** + * Updates the background color to match with task view status/bg color, and sets handle color + * to contrast with the background + */ + private void updateHandleAndBackgroundColor(boolean animated) { if (mTaskView == null) return; - ActivityManager.RunningTaskInfo info = mTaskView.getTaskInfo(); - final int taskBgColor = info.taskDescription.getStatusBarColor(); - final int color = Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb(); - if (color != -1) { - mMenuView.setBackgroundColor(color); + final int color = getTaskViewColor(); + final boolean isRegionDark = Color.luminance(color) <= 0.5; + mHandleView.updateHandleColor(isRegionDark, animated); + setBackgroundColor(color); + } + + /** + * Retrieves task view status/nav bar color or background if available + * + * TODO (b/283075226): Update with color sampling when + * RegionSamplingHelper or alternative is available + */ + private @ColorInt int getTaskViewColor() { + if (mTaskView == null || mTaskView.getTaskInfo() == null) return mBackgroundColor; + ActivityManager.TaskDescription taskDescription = mTaskView.getTaskInfo().taskDescription; + if (taskDescription.getStatusBarColor() != Color.TRANSPARENT) { + return taskDescription.getStatusBarColor(); + } else if (taskDescription.getBackgroundColor() != Color.TRANSPARENT) { + return taskDescription.getBackgroundColor(); } else { - mMenuView.setBackgroundColor(mBackgroundColor); + return mBackgroundColor; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java new file mode 100644 index 000000000000..e121aa45469d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2023 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.bar; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Outline; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewOutlineProvider; + +import androidx.annotation.ColorInt; +import androidx.core.content.ContextCompat; + +import com.android.wm.shell.R; + +/** + * Handle view to show at the top of a bubble bar expanded view. + */ +public class BubbleBarHandleView extends View { + private static final long COLOR_CHANGE_DURATION = 120; + + private final int mHandleWidth; + private final int mHandleHeight; + private final @ColorInt int mHandleLightColor; + private final @ColorInt int mHandleDarkColor; + private @Nullable ObjectAnimator mColorChangeAnim; + + public BubbleBarHandleView(Context context) { + this(context, null); + } + + public BubbleBarHandleView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + Resources resources = context.getResources(); + mHandleWidth = resources.getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_handle_width); + mHandleHeight = resources.getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_handle_height); + mHandleLightColor = ContextCompat.getColor(context, + R.color.bubble_bar_expanded_view_handle_light); + mHandleDarkColor = ContextCompat.getColor(context, + R.color.bubble_bar_expanded_view_handle_dark); + + setClipToOutline(true); + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + final int handleCenterX = view.getWidth() / 2; + final int handleCenterY = view.getHeight() / 2; + final float handleRadius = mHandleHeight / 2f; + Rect handleBounds = new Rect( + handleCenterX - mHandleWidth / 2, + handleCenterY - mHandleHeight / 2, + handleCenterX + mHandleWidth / 2, + handleCenterY + mHandleHeight / 2); + outline.setRoundRect(handleBounds, handleRadius); + } + }); + } + + /** + * Updates the handle color. + * + * @param isRegionDark Whether the background behind the handle is dark, and thus the handle + * should be light (and vice versa). + * @param animated Whether to animate the change, or apply it immediately. + */ + public void updateHandleColor(boolean isRegionDark, boolean animated) { + int newColor = isRegionDark ? mHandleLightColor : mHandleDarkColor; + if (mColorChangeAnim != null) { + mColorChangeAnim.cancel(); + } + if (animated) { + mColorChangeAnim = ObjectAnimator.ofArgb(this, "backgroundColor", newColor); + mColorChangeAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mColorChangeAnim = null; + } + }); + mColorChangeAnim.setDuration(COLOR_CHANGE_DURATION); + mColorChangeAnim.start(); + } else { + setBackgroundColor(newColor); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java deleted file mode 100644 index 9ee8a9d98aa1..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2023 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.bar; - -import android.content.Context; -import android.view.Gravity; -import android.widget.LinearLayout; - -/** - * Handle / menu view to show at the top of a bubble bar expanded view. - */ -public class HandleView extends LinearLayout { - - // TODO(b/273307221): implement the manage menu in this view. - public HandleView(Context context) { - super(context); - setOrientation(LinearLayout.HORIZONTAL); - setGravity(Gravity.CENTER); - } - - /** - * The menu extends past the top of the TaskView because of the rounded corners. This means - * to center content in the menu we must subtract the radius (i.e. the amount of space covered - * by TaskView). - */ - public void setCornerRadius(float radius) { - setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (int) radius); - } -} 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 4980e49a6fc3..bc0b71c97346 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 @@ -47,6 +47,7 @@ import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.annotations.ShellAnimationThread; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.desktopmode.DesktopModeController; @@ -264,8 +265,13 @@ public abstract class WMShellModule { static FreeformTaskTransitionHandler provideFreeformTaskTransitionHandler( ShellInit shellInit, Transitions transitions, - WindowDecorViewModel windowDecorViewModel) { - return new FreeformTaskTransitionHandler(shellInit, transitions, windowDecorViewModel); + Context context, + WindowDecorViewModel windowDecorViewModel, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor, + @ShellAnimationThread ShellExecutor animExecutor) { + return new FreeformTaskTransitionHandler(shellInit, transitions, context, + windowDecorViewModel, displayController, mainExecutor, animExecutor); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index b9d2be280efb..1169af9306b0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -508,5 +508,20 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll ); return result[0]; } + + @Override + public void stashDesktopApps(int displayId) throws RemoteException { + // Stashing of desktop apps not needed. Apps always launch on desktop + } + + @Override + public void hideStashedDesktopApps(int displayId) throws RemoteException { + // Stashing of desktop apps not needed. Apps always launch on desktop + } + + @Override + public void setTaskListener(IDesktopTaskListener listener) throws RemoteException { + // TODO(b/261234402): move visibility from sysui state to listener + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 3ab175d3b68a..402bb96dc0c0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -43,6 +43,7 @@ class DesktopModeTaskRepository { */ val activeTasks: ArraySet<Int> = ArraySet(), val visibleTasks: ArraySet<Int> = ArraySet(), + var stashed: Boolean = false ) // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0). @@ -312,6 +313,33 @@ class DesktopModeTaskRepository { } /** + * Update stashed status on display with id [displayId] + */ + fun setStashed(displayId: Int, stashed: Boolean) { + val data = displayData.getOrCreate(displayId) + val oldValue = data.stashed + data.stashed = stashed + if (oldValue != stashed) { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTaskRepo: mark stashed=%b displayId=%d", + stashed, + displayId + ) + visibleTasksListeners.forEach { (listener, executor) -> + executor.execute { listener.onStashedChanged(displayId, stashed) } + } + } + } + + /** + * Check if display with id [displayId] has desktop tasks stashed + */ + fun isStashed(displayId: Int): Boolean { + return displayData[displayId]?.stashed ?: false + } + + /** * Defines interface for classes that can listen to changes for active tasks in desktop mode. */ interface ActiveTasksListener { @@ -331,5 +359,11 @@ class DesktopModeTaskRepository { */ @JvmDefault fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {} + + /** + * Called when the desktop stashed status changes. + */ + @JvmDefault + fun onStashedChanged(displayId: Int, stashed: Boolean) {} } } 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 91bb155d9d01..de7d3adb0af3 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 @@ -46,6 +46,7 @@ import com.android.wm.shell.common.ExecutorUtils import com.android.wm.shell.common.ExternalInterfaceBinder import com.android.wm.shell.common.RemoteCallable import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.common.SingleInstanceRemoteListener import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.common.annotations.ExternalThread import com.android.wm.shell.common.annotations.ShellMainThread @@ -118,6 +119,30 @@ class DesktopTasksController( } } + /** + * Stash desktop tasks on display with id [displayId]. + * + * When desktop tasks are stashed, launcher home screen icons are fully visible. New apps + * launched in this state will be added to the desktop. Existing desktop tasks will be brought + * back to front during the launch. + */ + fun stashDesktopApps(displayId: Int) { + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: stashDesktopApps") + desktopModeTaskRepository.setStashed(displayId, true) + } + + /** + * Clear the stashed state for the given display + */ + fun hideStashedDesktopApps(displayId: Int) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: hideStashedApps displayId=%d", + displayId + ) + desktopModeTaskRepository.setStashed(displayId, false) + } + /** Get number of tasks that are marked as visible */ fun getVisibleTaskCount(displayId: Int): Int { return desktopModeTaskRepository.getVisibleTaskCount(displayId) @@ -397,6 +422,11 @@ class DesktopTasksController( transition: IBinder, request: TransitionRequestInfo ): WindowContainerTransaction? { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: handleRequest request=%s", + request + ) // Check if we should skip handling this transition val shouldHandleRequest = when { @@ -418,43 +448,63 @@ class DesktopTasksController( } val task: RunningTaskInfo = request.triggerTask - val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) - // Check if we should switch a fullscreen task to freeform - if (task.windowingMode == WINDOWING_MODE_FULLSCREEN) { - // If there are any visible desktop tasks, switch the task to freeform - if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { - KtProtoLog.d( + return when { + // If display has tasks stashed, handle as stashed launch + desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task) + // Check if fullscreen task should be updated + task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task) + // Check if freeform task should be updated + task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task) + else -> null + } + } + + private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) + if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) { + KtProtoLog.d( WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: switch fullscreen task to freeform on transition" + - " taskId=%d", + "DesktopTasksController: switch freeform task to fullscreen oon transition" + + " taskId=%d", task.taskId - ) - return WindowContainerTransaction().also { wct -> - addMoveToDesktopChanges(wct, task.token) - } + ) + return WindowContainerTransaction().also { wct -> + addMoveToFullscreenChanges(wct, task.token) } } + return null + } - // CHeck if we should switch a freeform task to fullscreen - if (task.windowingMode == WINDOWING_MODE_FREEFORM) { - // If no visible desktop tasks, switch this task to freeform as the transition came - // outside of this controller - if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) { - KtProtoLog.d( + private fun handleFullscreenTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) + if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { + KtProtoLog.d( WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: switch freeform task to fullscreen oon transition" + - " taskId=%d", + "DesktopTasksController: switch fullscreen task to freeform on transition" + + " taskId=%d", task.taskId - ) - return WindowContainerTransaction().also { wct -> - addMoveToFullscreenChanges(wct, task.token) - } + ) + return WindowContainerTransaction().also { wct -> + addMoveToDesktopChanges(wct, task.token) } } return null } + private fun handleStashedTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: launch apps with stashed on transition taskId=%d", + task.taskId + ) + val wct = WindowContainerTransaction() + bringDesktopAppsToFront(task.displayId, wct) + addMoveToDesktopChanges(wct, task.token) + desktopModeTaskRepository.setStashed(task.displayId, false) + return wct + } + private fun addMoveToDesktopChanges( wct: WindowContainerTransaction, token: WindowContainerToken @@ -658,8 +708,46 @@ class DesktopTasksController( @BinderThread private class IDesktopModeImpl(private var controller: DesktopTasksController?) : IDesktopMode.Stub(), ExternalInterfaceBinder { + + private lateinit var remoteListener: + SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener> + + private val listener: VisibleTasksListener = object : VisibleTasksListener { + override fun onVisibilityChanged(displayId: Int, visible: Boolean) { + // TODO(b/261234402): move visibility from sysui state to listener + remoteListener.call { l -> l.onVisibilityChanged(displayId, visible) } + } + + override fun onStashedChanged(displayId: Int, stashed: Boolean) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "IDesktopModeImpl: onStashedChanged stashed=%b display=%d", + stashed, + displayId + ) + remoteListener.call { l -> l.onStashedChanged(displayId, stashed) } + } + } + + init { + remoteListener = + SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>( + controller, + { c -> + c.desktopModeTaskRepository.addVisibleTasksListener( + listener, + c.mainExecutor + ) + }, + { c -> + c.desktopModeTaskRepository.removeVisibleTasksListener(listener) + } + ) + } + /** Invalidates this instance, preventing future calls from updating the controller. */ override fun invalidate() { + remoteListener.unregister() controller = null } @@ -670,6 +758,20 @@ class DesktopTasksController( ) { c -> c.showDesktopApps(displayId) } } + override fun stashDesktopApps(displayId: Int) { + ExecutorUtils.executeRemoteCallWithTaskPermission( + controller, + "stashDesktopApps" + ) { c -> c.stashDesktopApps(displayId) } + } + + override fun hideStashedDesktopApps(displayId: Int) { + ExecutorUtils.executeRemoteCallWithTaskPermission( + controller, + "hideStashedDesktopApps" + ) { c -> c.hideStashedDesktopApps(displayId) } + } + override fun getVisibleTaskCount(displayId: Int): Int { val result = IntArray(1) ExecutorUtils.executeRemoteCallWithTaskPermission( @@ -680,6 +782,18 @@ class DesktopTasksController( ) return result[0] } + + override fun setTaskListener(listener: IDesktopTaskListener?) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "IDesktopModeImpl: set task listener=%s", + listener ?: "null" + ) + ExecutorUtils.executeRemoteCallWithTaskPermission( + controller, + "setTaskListener" + ) { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() } + } } companion object { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl index 899d67267e69..05a6e33de37a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl @@ -16,6 +16,8 @@ package com.android.wm.shell.desktopmode; +import com.android.wm.shell.desktopmode.IDesktopTaskListener; + /** * Interface that is exposed to remote callers to manipulate desktop mode features. */ @@ -24,6 +26,15 @@ interface IDesktopMode { /** Show apps on the desktop on the given display */ void showDesktopApps(int displayId); + /** Stash apps on the desktop to allow launching another app from home screen */ + void stashDesktopApps(int displayId); + + /** Hide apps that may be stashed */ + void hideStashedDesktopApps(int displayId); + /** Get count of visible desktop tasks on the given display */ int getVisibleTaskCount(int displayId); + + /** Set listener that will receive callbacks about updates to desktop tasks */ + oneway void setTaskListener(IDesktopTaskListener listener); }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl new file mode 100644 index 000000000000..39128a863ec9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 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.desktopmode; + +/** + * Allows external processes to register a listener in WMShell to get updates about desktop task + * state. + */ +interface IDesktopTaskListener { + + /** Desktop task visibility has change. Visible if at least 1 task is visible. */ + oneway void onVisibilityChanged(int displayId, boolean visible); + + /** Desktop task stashed status has changed. */ + oneway void onStashedChanged(int displayId, boolean stashed); +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md index 99922fbc2d95..c795a0595087 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md @@ -26,6 +26,12 @@ building to check the log state (is enabled) before printing the print format st - Non-text ProtoLogs are not currently supported with the Shell library (you can't view them with traces in Winscope) +### Kotlin + +Protolog tool does not yet have support for Kotlin code (see [b/168581922](https://b.corp.google.com/issues/168581922)). +For logging in Kotlin, use the [KtProtoLog](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt) +class which has a similar API to the Java ProtoLog class. + ### Enabling ProtoLog command line logging Run these commands to enable protologs for both WM Core and WM Shell to print to logcat. ```shell diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java index 04fc79acadbd..55e34fe3d836 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java @@ -19,9 +19,15 @@ package com.android.wm.shell.freeform; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.app.ActivityManager; import android.app.WindowConfiguration; +import android.content.Context; +import android.graphics.Rect; import android.os.IBinder; +import android.util.ArrayMap; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -31,6 +37,8 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.WindowDecorViewModel; @@ -39,23 +47,37 @@ import java.util.ArrayList; import java.util.List; /** - * The {@link Transitions.TransitionHandler} that handles freeform task maximizing and restoring - * transitions. + * The {@link Transitions.TransitionHandler} that handles freeform task maximizing, closing, and + * restoring transitions. */ public class FreeformTaskTransitionHandler implements Transitions.TransitionHandler, FreeformTaskTransitionStarter { - + private static final int CLOSE_ANIM_DURATION = 400; + private final Context mContext; private final Transitions mTransitions; private final WindowDecorViewModel mWindowDecorViewModel; + private final DisplayController mDisplayController; + private final ShellExecutor mMainExecutor; + private final ShellExecutor mAnimExecutor; private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); + private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>(); + public FreeformTaskTransitionHandler( ShellInit shellInit, Transitions transitions, - WindowDecorViewModel windowDecorViewModel) { + Context context, + WindowDecorViewModel windowDecorViewModel, + DisplayController displayController, + ShellExecutor mainExecutor, + ShellExecutor animExecutor) { mTransitions = transitions; + mContext = context; mWindowDecorViewModel = windowDecorViewModel; + mDisplayController = displayController; + mMainExecutor = mainExecutor; + mAnimExecutor = animExecutor; if (Transitions.ENABLE_SHELL_TRANSITIONS) { shellInit.addInitCallback(this::onInit, this); } @@ -103,6 +125,14 @@ public class FreeformTaskTransitionHandler @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback) { boolean transitionHandled = false; + final ArrayList<Animator> animations = new ArrayList<>(); + final Runnable onAnimFinish = () -> { + if (!animations.isEmpty()) return; + mMainExecutor.execute(() -> { + mAnimations.remove(transition); + finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + }); + }; for (TransitionInfo.Change change : info.getChanges()) { if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) { continue; @@ -121,21 +151,45 @@ public class FreeformTaskTransitionHandler case WindowManager.TRANSIT_TO_BACK: transitionHandled |= startMinimizeTransition(transition); break; + case WindowManager.TRANSIT_CLOSE: + if (change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_FREEFORM) { + transitionHandled |= startCloseTransition(transition, change, + finishT, animations, onAnimFinish); + } + break; } } - - mPendingTransitionTokens.remove(transition); - if (!transitionHandled) { return false; } - + mAnimations.put(transition, animations); + // startT must be applied before animations start. startT.apply(); - mTransitions.getMainExecutor().execute( - () -> finishCallback.onTransitionFinished(null, null)); + mAnimExecutor.execute(() -> { + for (Animator anim : animations) { + anim.start(); + } + }); + // Run this here in case no animators are created. + onAnimFinish.run(); + mPendingTransitionTokens.remove(transition); return true; } + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + ArrayList<Animator> animations = mAnimations.get(mergeTarget); + if (animations == null) return; + mAnimExecutor.execute(() -> { + for (Animator anim : animations) { + anim.end(); + } + }); + + } + private boolean startChangeTransition( IBinder transition, int type, @@ -165,6 +219,36 @@ public class FreeformTaskTransitionHandler return mPendingTransitionTokens.contains(transition); } + private boolean startCloseTransition(IBinder transition, TransitionInfo.Change change, + SurfaceControl.Transaction finishT, ArrayList<Animator> animations, + Runnable onAnimFinish) { + if (!mPendingTransitionTokens.contains(transition)) return false; + int screenHeight = mDisplayController + .getDisplayLayout(change.getTaskInfo().displayId).height(); + ValueAnimator animator = new ValueAnimator(); + animator.setDuration(CLOSE_ANIM_DURATION) + .setFloatValues(0f, 1f); + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + SurfaceControl sc = change.getLeash(); + finishT.hide(sc); + Rect startBounds = new Rect(change.getTaskInfo().configuration.windowConfiguration + .getBounds()); + animator.addUpdateListener(animation -> { + t.setPosition(sc, startBounds.left, + startBounds.top + (animation.getAnimatedFraction() * screenHeight)); + t.apply(); + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animations.remove(animator); + onAnimFinish.run(); + } + }); + animations.add(animator); + return true; + } + @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, 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 39fb7936747e..2267c750a31b 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 @@ -268,7 +268,10 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { return false; } case MotionEvent.ACTION_MOVE: { - int dragPointerIdx = e.findPointerIndex(mDragPointerId); + if (e.findPointerIndex(mDragPointerId) == -1) { + mDragPointerId = e.getPointerId(0); + } + final int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragPositioningCallback.onDragPositioningMove( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); mIsDragging = true; @@ -276,7 +279,10 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { - int dragPointerIdx = e.findPointerIndex(mDragPointerId); + if (e.findPointerIndex(mDragPointerId) == -1) { + mDragPointerId = e.getPointerId(0); + } + final int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragPositioningCallback.onDragPositioningEnd( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); final boolean wasDragging = mIsDragging; 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 9fd57d7e1201..15abbf298b24 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 @@ -385,6 +385,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { case MotionEvent.ACTION_MOVE: { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + if (e.findPointerIndex(mDragPointerId) == -1) { + mDragPointerId = e.getPointerId(0); + } final int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDesktopTasksController.ifPresent(c -> c.onDragPositioningMove(taskInfo, decoration.mTaskSurface, e.getRawY(dragPointerIdx))); @@ -395,6 +398,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { + if (e.findPointerIndex(mDragPointerId) == -1) { + mDragPointerId = e.getPointerId(0); + } final int dragPointerIdx = e.findPointerIndex(mDragPointerId); // Position of the task is calculated by subtracting the raw location of the // motion event (the location of the motion relative to the display) by the diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java index 65b5a7a17afe..58644b23ce12 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java @@ -73,8 +73,11 @@ class DragDetector { return mResultOfDownAction; } case ACTION_MOVE: { + if (ev.findPointerIndex(mDragPointerId) == -1) { + mDragPointerId = ev.getPointerId(0); + } + final int dragPointerIndex = ev.findPointerIndex(mDragPointerId); if (!mIsDragEvent) { - int dragPointerIndex = ev.findPointerIndex(mDragPointerId); float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x; float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y; // Touches generate noisy moves, so only once the move is past the touch diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java new file mode 100644 index 000000000000..d38b848fbb4d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 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.bar; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.graphics.drawable.ColorDrawable; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.core.content.ContextCompat; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class BubbleBarHandleViewTest extends ShellTestCase { + private BubbleBarHandleView mHandleView; + + @Before + public void setup() { + mHandleView = new BubbleBarHandleView(mContext); + } + + @Test + public void testUpdateHandleColor_lightBg() { + mHandleView.updateHandleColor(false /* isRegionDark */, false /* animated */); + + assertTrue(mHandleView.getClipToOutline()); + assertTrue(mHandleView.getBackground() instanceof ColorDrawable); + ColorDrawable bgDrawable = (ColorDrawable) mHandleView.getBackground(); + assertEquals(bgDrawable.getColor(), + ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_handle_dark)); + } + + @Test + public void testUpdateHandleColor_darkBg() { + mHandleView.updateHandleColor(true /* isRegionDark */, false /* animated */); + + assertTrue(mHandleView.getClipToOutline()); + assertTrue(mHandleView.getBackground() instanceof ColorDrawable); + ColorDrawable bgDrawable = (ColorDrawable) mHandleView.getBackground(); + assertEquals(bgDrawable.getColor(), + ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_handle_light)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 3bc2f0e8674e..17c0463309d7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -313,6 +313,65 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { assertThat(tasks.first()).isEqualTo(6) } + @Test + fun setStashed_stateIsUpdatedForTheDisplay() { + repo.setStashed(DEFAULT_DISPLAY, true) + assertThat(repo.isStashed(DEFAULT_DISPLAY)).isTrue() + assertThat(repo.isStashed(SECOND_DISPLAY)).isFalse() + + repo.setStashed(DEFAULT_DISPLAY, false) + assertThat(repo.isStashed(DEFAULT_DISPLAY)).isFalse() + } + + @Test + fun setStashed_notifyListener() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + repo.setStashed(DEFAULT_DISPLAY, true) + executor.flushAll() + assertThat(listener.stashedOnDefaultDisplay).isTrue() + assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1) + + repo.setStashed(DEFAULT_DISPLAY, false) + executor.flushAll() + assertThat(listener.stashedOnDefaultDisplay).isFalse() + assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(2) + } + + @Test + fun setStashed_secondCallDoesNotNotify() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + repo.setStashed(DEFAULT_DISPLAY, true) + repo.setStashed(DEFAULT_DISPLAY, true) + executor.flushAll() + assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1) + } + + @Test + fun setStashed_tracksPerDisplay() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + + repo.setStashed(DEFAULT_DISPLAY, true) + executor.flushAll() + assertThat(listener.stashedOnDefaultDisplay).isTrue() + assertThat(listener.stashedOnSecondaryDisplay).isFalse() + + repo.setStashed(SECOND_DISPLAY, true) + executor.flushAll() + assertThat(listener.stashedOnDefaultDisplay).isTrue() + assertThat(listener.stashedOnSecondaryDisplay).isTrue() + + repo.setStashed(DEFAULT_DISPLAY, false) + executor.flushAll() + assertThat(listener.stashedOnDefaultDisplay).isFalse() + assertThat(listener.stashedOnSecondaryDisplay).isTrue() + } + class TestListener : DesktopModeTaskRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 @@ -332,6 +391,12 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { var visibleChangesOnDefaultDisplay = 0 var visibleChangesOnSecondaryDisplay = 0 + var stashedOnDefaultDisplay = false + var stashedOnSecondaryDisplay = false + + var stashedChangesOnDefaultDisplay = 0 + var stashedChangesOnSecondaryDisplay = 0 + override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) { when (displayId) { DEFAULT_DISPLAY -> { @@ -345,6 +410,20 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { else -> fail("Visible task listener received unexpected display id: $displayId") } } + + override fun onStashedChanged(displayId: Int, stashed: Boolean) { + when (displayId) { + DEFAULT_DISPLAY -> { + stashedOnDefaultDisplay = stashed + stashedChangesOnDefaultDisplay++ + } + SECOND_DISPLAY -> { + stashedOnSecondaryDisplay = stashed + stashedChangesOnDefaultDisplay++ + } + else -> fail("Visible task listener received unexpected display id: $displayId") + } + } } companion object { 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 1335ebf105a6..9ce18db9492d 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 @@ -451,6 +451,27 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY) + markTaskHidden(stashedFreeformTask) + + val fullscreenTask = createFullscreenTask(DEFAULT_DISPLAY) + + controller.stashDesktopApps(DEFAULT_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + assertThat(result).isNotNull() + result!!.assertReorderSequence(stashedFreeformTask, fullscreenTask) + assertThat(result.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + + // Stashed state should be cleared + assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() + } + + @Test fun handleRequest_freeformTask_freeformVisible_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -501,6 +522,25 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_freeformTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY) + markTaskHidden(stashedFreeformTask) + + val freeformTask = createFreeformTask(DEFAULT_DISPLAY) + + controller.stashDesktopApps(DEFAULT_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(freeformTask)) + assertThat(result).isNotNull() + result?.assertReorderSequence(stashedFreeformTask, freeformTask) + + // Stashed state should be cleared + assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() + } + + @Test fun handleRequest_notOpenOrToFrontTransition_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -539,6 +579,25 @@ class DesktopTasksControllerTest : ShellTestCase() { assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() } + @Test + fun stashDesktopApps_stateUpdates() { + controller.stashDesktopApps(DEFAULT_DISPLAY) + + assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isTrue() + assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isFalse() + } + + @Test + fun hideStashedDesktopApps_stateUpdates() { + desktopModeTaskRepository.setStashed(DEFAULT_DISPLAY, true) + desktopModeTaskRepository.setStashed(SECOND_DISPLAY, true) + controller.hideStashedDesktopApps(DEFAULT_DISPLAY) + + assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() + // Check that second display is not affected + assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isTrue() + } + private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createFreeformTask(displayId) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml index aae30dfe6223..a0b34690696f 100644 --- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml +++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml @@ -144,7 +144,7 @@ android:visibility="gone" android:duplicateParentState="true" android:clickable="false" - android:text="@string/consent_no" /> + android:text="@string/consent_cancel" /> </LinearLayout> diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml index 2502bbf7b40b..539857951ab1 100644 --- a/packages/CompanionDeviceManager/res/values/strings.xml +++ b/packages/CompanionDeviceManager/res/values/strings.xml @@ -28,13 +28,13 @@ <string name="profile_name_watch">watch</string> <!-- Title of the device selection dialog. --> - <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by <strong><xliff:g id="app_name" example="Android Wear">%2$s</xliff:g></strong></string> + <string name="chooser_title_non_profile">Choose a device to be managed by <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong></string> - <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] --> - <string name="summary_watch">This app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to sync info, like the name of someone calling, interact with your notifications and access your Phone, SMS, Contacts, Calendar, Call logs and Nearby devices permissions.</string> + <!-- Tile of the multiple devices' dialog. --> + <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to set up</string> - <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile for singleDevice(type) [CHAR LIMIT=NONE] --> - <string name="summary_watch_single_device">This app will be allowed to sync info, like the name of someone calling, and access these permissions on your <xliff:g id="device_type" example="phone">%1$s</xliff:g></string> + <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile [CHAR LIMIT=NONE] --> + <string name="summary_watch">This app will be allowed to sync info, like the name of someone calling, and access these permissions on your <xliff:g id="device_name" example="phone">%1$s</xliff:g></string> <!-- ================= DEVICE_PROFILE_GLASSES ================= --> @@ -42,13 +42,10 @@ <string name="confirmation_title_glasses">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to manage <strong><xliff:g id="device_name" example="Glasses">%2$s</xliff:g></strong>?</string> <!-- The name of the "glasses" device type [CHAR LIMIT=30] --> - <string name="profile_name_glasses">glasses</string> + <string name="profile_name_glasses">device</string> - <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile (type) [CHAR LIMIT=NONE] --> - <string name="summary_glasses_multi_device">This app is needed to manage <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string> - - <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile for singleDevice(type) [CHAR LIMIT=NONE] --> - <string name="summary_glasses_single_device">This app will be allowed to access these permissions on your <xliff:g id="device_type" example="phone">%1$s</xliff:g></string> + <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile [CHAR LIMIT=NONE] --> + <string name="summary_glasses">This app will be allowed to access these permissions on your <xliff:g id="device_name" example="phone">%1$s</xliff:g></string> <!-- ================= DEVICE_PROFILE_APP_STREAMING ================= --> @@ -97,9 +94,6 @@ <string name="profile_name_generic">device</string> <!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] --> - <string name="summary_generic_single_device">This app will be able to sync info, like the name of someone calling, between your phone and <xliff:g id="device_name" example="My Watch">%1$s</xliff:g></string> - - <!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] --> <string name="summary_generic">This app will be able to sync info, like the name of someone calling, between your phone and the chosen device</string> <!-- ================= Buttons ================= --> @@ -110,6 +104,9 @@ <!-- Negative button for the device-app association consent dialog [CHAR LIMIT=30] --> <string name="consent_no">Don\u2019t allow</string> + <!-- Cancel button for the device chooser dialog [CHAR LIMIT=30] --> + <string name="consent_cancel">Cancel</string> + <!-- Back button for the helper consent dialog [CHAR LIMIT=30] --> <string name="consent_back">Back</string> diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml index e85190be0e1e..222877bbe9e9 100644 --- a/packages/CompanionDeviceManager/res/values/styles.xml +++ b/packages/CompanionDeviceManager/res/values/styles.xml @@ -69,11 +69,13 @@ <style name="PositiveButton" parent="@android:style/Widget.Material.Button.Borderless.Colored"> - <item name="android:layout_width">300dp</item> - <item name="android:layout_height">56dp</item> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> <item name="android:layout_marginBottom">2dp</item> <item name="android:textAllCaps">false</item> <item name="android:textSize">14sp</item> + <item name="android:layout_marginStart">32dp</item> + <item name="android:layout_marginEnd">32dp</item> <item name="android:textColor">@android:color/system_neutral1_900</item> <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item> <item name="android:background">@drawable/btn_positive_bottom</item> @@ -81,11 +83,13 @@ <style name="NegativeButton" parent="@android:style/Widget.Material.Button.Borderless.Colored"> - <item name="android:layout_width">300dp</item> - <item name="android:layout_height">56dp</item> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> <item name="android:layout_marginTop">2dp</item> <item name="android:textAllCaps">false</item> <item name="android:textSize">14sp</item> + <item name="android:layout_marginStart">32dp</item> + <item name="android:layout_marginEnd">32dp</item> <item name="android:textColor">@android:color/system_neutral1_900</item> <item name="android:layout_marginTop">4dp</item> <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item> diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java index 4154029b6d41..97016f5384f6 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java @@ -27,10 +27,8 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState; import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState.FINISHED_TIMEOUT; -import static com.android.companiondevicemanager.CompanionDeviceResources.MULTI_DEVICES_SUMMARIES; import static com.android.companiondevicemanager.CompanionDeviceResources.PERMISSION_TYPES; import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME; -import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME_MULTI; import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_ICON; import static com.android.companiondevicemanager.CompanionDeviceResources.SUMMARIES; import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_PROFILES; @@ -121,6 +119,9 @@ public class CompanionDeviceActivity extends FragmentActivity implements private IAssociationRequestCallback mAppCallback; private ResultReceiver mCdmServiceReceiver; + // Present for application's name. + private CharSequence mAppLabel; + // Always present widgets. private TextView mTitle; private TextView mSummary; @@ -165,8 +166,7 @@ public class CompanionDeviceActivity extends FragmentActivity implements private @Nullable RecyclerView mDeviceListRecyclerView; private @Nullable DeviceListAdapter mDeviceAdapter; - - // The recycler view is only shown for selfManaged and singleDevice association request. + // The recycler view is shown for non-null profile association request. private @Nullable RecyclerView mPermissionListRecyclerView; private @Nullable PermissionListAdapter mPermissionListAdapter; @@ -178,8 +178,6 @@ public class CompanionDeviceActivity extends FragmentActivity implements // onActivityResult() after the association is created. private @Nullable DeviceFilterPair<?> mSelectedDevice; - private @Nullable List<Integer> mPermissionTypes; - private LinearLayoutManager mPermissionsLayoutManager = new LinearLayoutManager(this); @Override @@ -302,6 +300,8 @@ public class CompanionDeviceActivity extends FragmentActivity implements setContentView(R.layout.activity_confirmation); + mAppLabel = appLabel; + mConstraintList = findViewById(R.id.constraint_list); mAssociationConfirmationDialog = findViewById(R.id.association_confirmation); mVendorHeader = findViewById(R.id.vendor_header); @@ -322,7 +322,6 @@ public class CompanionDeviceActivity extends FragmentActivity implements mMultipleDeviceSpinner = findViewById(R.id.spinner_multiple_device); mSingleDeviceSpinner = findViewById(R.id.spinner_single_device); - mDeviceAdapter = new DeviceListAdapter(this, this::onListItemClick); mPermissionListRecyclerView = findViewById(R.id.permission_list); mPermissionListAdapter = new PermissionListAdapter(this); @@ -468,8 +467,6 @@ public class CompanionDeviceActivity extends FragmentActivity implements throw new RuntimeException("Unsupported profile " + deviceProfile); } - mPermissionTypes = new ArrayList<>(); - try { vendorIcon = getVendorHeaderIcon(this, packageName, userId); vendorName = getVendorHeaderName(this, packageName, userId); @@ -486,17 +483,13 @@ public class CompanionDeviceActivity extends FragmentActivity implements } title = getHtmlFromResources(this, TITLES.get(deviceProfile), deviceName); - mPermissionTypes.addAll(PERMISSION_TYPES.get(deviceProfile)); + setupPermissionList(deviceProfile); // Summary is not needed for selfManaged dialog. mSummary.setVisibility(View.GONE); - - setupPermissionList(); - mTitle.setText(title); mVendorHeaderName.setText(vendorName); mVendorHeader.setVisibility(View.VISIBLE); - mVendorHeader.setVisibility(View.VISIBLE); mProfileIcon.setVisibility(View.GONE); mDeviceListRecyclerView.setVisibility(View.GONE); // Top and bottom borders should be gone for selfManaged dialog. @@ -509,7 +502,9 @@ public class CompanionDeviceActivity extends FragmentActivity implements final String deviceProfile = mRequest.getDeviceProfile(); - mPermissionTypes = new ArrayList<>(); + if (!SUPPORTED_PROFILES.contains(deviceProfile)) { + throw new RuntimeException("Unsupported profile " + deviceProfile); + } CompanionDeviceDiscoveryService.getScanResult().observe(this, deviceFilterPairs -> updateSingleDeviceUi( @@ -529,75 +524,40 @@ public class CompanionDeviceActivity extends FragmentActivity implements if (deviceFilterPairs.isEmpty()) return; mSelectedDevice = requireNonNull(deviceFilterPairs.get(0)); - // No need to show user consent dialog if it is a singleDevice - // and isSkipPrompt(true) AssociationRequest. - // See AssociationRequestsProcessor#mayAssociateWithoutPrompt. - if (mRequest.isSkipPrompt()) { - mSingleDeviceSpinner.setVisibility(View.GONE); - onUserSelectedDevice(mSelectedDevice); - return; - } - - final String deviceName = mSelectedDevice.getDisplayName(); - final Spanned title; - final Spanned summary; - final Drawable profileIcon; - if (!SUPPORTED_PROFILES.contains(deviceProfile)) { - throw new RuntimeException("Unsupported profile " + deviceProfile); - } + final Drawable profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile)); - if (deviceProfile == null) { - summary = getHtmlFromResources(this, SUMMARIES.get(null), deviceName); - mConstraintList.setVisibility(View.GONE); - } else { - summary = getHtmlFromResources( - this, SUMMARIES.get(deviceProfile), getString(R.string.device_type)); - mPermissionTypes.addAll(PERMISSION_TYPES.get(deviceProfile)); - setupPermissionList(); - } - - title = getHtmlFromResources(this, TITLES.get(deviceProfile), appLabel, deviceName); - profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile)); + updatePermissionUi(); - mTitle.setText(title); - mSummary.setText(summary); mProfileIcon.setImageDrawable(profileIcon); - mSingleDeviceSpinner.setVisibility(View.GONE); mAssociationConfirmationDialog.setVisibility(View.VISIBLE); + mSingleDeviceSpinner.setVisibility(View.GONE); } private void initUiForMultipleDevices(CharSequence appLabel) { if (DEBUG) Log.i(TAG, "initUiFor_MultipleDevices()"); - final String deviceProfile = mRequest.getDeviceProfile(); - - final String profileName; - final String profileNameMulti; - final Spanned summary; final Drawable profileIcon; - final int summaryResourceId; + final Spanned title; + final String deviceProfile = mRequest.getDeviceProfile(); if (!SUPPORTED_PROFILES.contains(deviceProfile)) { throw new RuntimeException("Unsupported profile " + deviceProfile); } - profileName = getString(PROFILES_NAME.get(deviceProfile)); - profileNameMulti = getString(PROFILES_NAME_MULTI.get(deviceProfile)); profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile)); - summaryResourceId = MULTI_DEVICES_SUMMARIES.get(deviceProfile); if (deviceProfile == null) { - summary = getHtmlFromResources(this, summaryResourceId); + title = getHtmlFromResources(this, R.string.chooser_title_non_profile, appLabel); + mButtonNotAllowMultipleDevices.setText(R.string.consent_no); } else { - summary = getHtmlFromResources(this, summaryResourceId, profileName, appLabel); + title = getHtmlFromResources(this, + R.string.chooser_title, getString(PROFILES_NAME.get(deviceProfile))); } - final Spanned title = getHtmlFromResources( - this, R.string.chooser_title, profileNameMulti, appLabel); + mDeviceAdapter = new DeviceListAdapter(this, this::onDeviceClicked); mTitle.setText(title); - mSummary.setText(summary); mProfileIcon.setImageDrawable(profileIcon); mDeviceListRecyclerView.setAdapter(mDeviceAdapter); @@ -613,6 +573,7 @@ public class CompanionDeviceActivity extends FragmentActivity implements mDeviceAdapter.setDevices(deviceFilterPairs); }); + mSummary.setVisibility(View.GONE); // "Remove" consent button: users would need to click on the list item. mButtonAllow.setVisibility(View.GONE); mButtonNotAllow.setVisibility(View.GONE); @@ -623,11 +584,9 @@ public class CompanionDeviceActivity extends FragmentActivity implements mMultipleDeviceSpinner.setVisibility(View.VISIBLE); } - private void onListItemClick(int position) { - if (DEBUG) Log.d(TAG, "onListItemClick() " + position); - + private void onDeviceClicked(int position) { final DeviceFilterPair<?> selectedDevice = mDeviceAdapter.getItem(position); - + // To prevent double tap on the selected device. if (mSelectedDevice != null) { if (DEBUG) Log.w(TAG, "Already selected."); return; @@ -637,7 +596,47 @@ public class CompanionDeviceActivity extends FragmentActivity implements mSelectedDevice = requireNonNull(selectedDevice); - onUserSelectedDevice(selectedDevice); + Log.d(TAG, "onDeviceClicked(): " + mSelectedDevice.toShortString()); + + updatePermissionUi(); + + mSummary.setVisibility(View.VISIBLE); + mButtonAllow.setVisibility(View.VISIBLE); + mButtonNotAllow.setVisibility(View.VISIBLE); + mDeviceListRecyclerView.setVisibility(View.GONE); + mNotAllowMultipleDevicesLayout.setVisibility(View.GONE); + } + + private void updatePermissionUi() { + final String deviceProfile = mRequest.getDeviceProfile(); + final int summaryResourceId = SUMMARIES.get(deviceProfile); + final String remoteDeviceName = mSelectedDevice.getDisplayName(); + final Spanned title = getHtmlFromResources( + this, TITLES.get(deviceProfile), mAppLabel, remoteDeviceName); + final Spanned summary; + + // No need to show permission consent dialog if it is a isSkipPrompt(true) + // AssociationRequest. See AssociationRequestsProcessor#mayAssociateWithoutPrompt. + if (mRequest.isSkipPrompt()) { + mSingleDeviceSpinner.setVisibility(View.GONE); + onUserSelectedDevice(mSelectedDevice); + return; + } + + if (deviceProfile == null && mRequest.isSingleDevice()) { + summary = getHtmlFromResources(this, summaryResourceId, remoteDeviceName); + mConstraintList.setVisibility(View.GONE); + } else if (deviceProfile == null) { + onUserSelectedDevice(mSelectedDevice); + return; + } else { + summary = getHtmlFromResources( + this, summaryResourceId, getString(R.string.device_type)); + setupPermissionList(deviceProfile); + } + + mTitle.setText(title); + mSummary.setText(summary); } private void onPositiveButtonClick(View v) { @@ -680,8 +679,9 @@ public class CompanionDeviceActivity extends FragmentActivity implements // initiate the layoutManager for the recyclerview, add listeners for monitoring the scrolling // and when mPermissionListRecyclerView is fully populated. // Lastly, disable the Allow and Don't allow buttons. - private void setupPermissionList() { - mPermissionListAdapter.setPermissionType(mPermissionTypes); + private void setupPermissionList(String deviceProfile) { + final List<Integer> permissionTypes = new ArrayList<>(PERMISSION_TYPES.get(deviceProfile)); + mPermissionListAdapter.setPermissionType(permissionTypes); mPermissionListRecyclerView.setAdapter(mPermissionListAdapter); mPermissionListRecyclerView.setLayoutManager(mPermissionsLayoutManager); diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java index 7aed13960b08..060c03213bcd 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java @@ -86,21 +86,11 @@ final class CompanionDeviceResources { static final Map<String, Integer> SUMMARIES; static { final Map<String, Integer> map = new ArrayMap<>(); - map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch_single_device); - map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses_single_device); - map.put(null, R.string.summary_generic_single_device); - - SUMMARIES = unmodifiableMap(map); - } - - static final Map<String, Integer> MULTI_DEVICES_SUMMARIES; - static { - final Map<String, Integer> map = new ArrayMap<>(); map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch); - map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses_multi_device); + map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses); map.put(null, R.string.summary_generic); - MULTI_DEVICES_SUMMARIES = unmodifiableMap(map); + SUMMARIES = unmodifiableMap(map); } static final Map<String, Integer> PROFILES_NAME; diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 873b434aa4fd..dd8eb3b3f3fb 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -590,6 +590,7 @@ public class SettingsBackupTest { Settings.Global.APPOP_HISTORY_BASE_INTERVAL_MILLIS, Settings.Global.AUTO_REVOKE_PARAMETERS, Settings.Global.ENABLE_RADIO_BUG_DETECTION, + Settings.Global.REPAIR_MODE_ACTIVE, Settings.Global.RADIO_BUG_WAKELOCK_TIMEOUT_COUNT_THRESHOLD, Settings.Global.RADIO_BUG_SYSTEM_ERROR_COUNT_THRESHOLD, Settings.Global.ENABLED_SUBSCRIPTION_FOR_SLOT, @@ -724,6 +725,7 @@ public class SettingsBackupTest { Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, Settings.Secure.CONTENT_CAPTURE_ENABLED, Settings.Secure.DEFAULT_INPUT_METHOD, + Settings.Secure.DEFAULT_NOTE_TASK_PROFILE, Settings.Secure.DEVICE_PAIRED, Settings.Secure.DIALER_DEFAULT_APPLICATION, Settings.Secure.DISABLED_PRINT_SERVICES, diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index fe90caf2646c..b661ba485e73 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -451,12 +451,14 @@ android:noHistory="true" /> <service android:name=".screenshot.appclips.AppClipsScreenshotHelperService" - android:permission="com.android.systemui.permission.SELF" - android:exported="false" /> + android:exported="false" + android:singleUser="true" + android:permission="com.android.systemui.permission.SELF" /> <service android:name=".screenshot.appclips.AppClipsService" - android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE" - android:exported="true" /> + android:exported="true" + android:singleUser="true" + android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE" /> <service android:name=".screenrecord.RecordingService" android:foregroundServiceType="systemExempted"/> @@ -990,6 +992,11 @@ <service android:name=".notetask.NoteTaskControllerUpdateService" /> + <service android:name=".notetask.NoteTaskBubblesController$NoteTaskBubblesService" + android:exported="false" + android:singleUser="true" + android:permission="com.android.systemui.permission.SELF" /> + <activity android:name=".notetask.shortcut.LaunchNoteTaskActivity" android:exported="true" @@ -1003,16 +1010,6 @@ </intent-filter> </activity> - <!-- LaunchNoteTaskManagedProfileProxyActivity MUST NOT be exported because it allows caller - to specify an Android user when launching the default notes app. --> - <activity - android:name=".notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity" - android:exported="false" - android:enabled="true" - android:excludeFromRecents="true" - android:resizeableActivity="false" - android:theme="@android:style/Theme.NoDisplay" /> - <activity android:name=".notetask.LaunchNotesRoleSettingsTrampolineActivity" android:exported="true" diff --git a/packages/SystemUI/docs/device-entry/keyguard.md b/packages/SystemUI/docs/device-entry/keyguard.md index 8634c950f96c..1898b97ee1ca 100644 --- a/packages/SystemUI/docs/device-entry/keyguard.md +++ b/packages/SystemUI/docs/device-entry/keyguard.md @@ -20,6 +20,10 @@ Begins with the device in low power mode, with the display active for [AOD][3] o An indication to power off the device most likely comes from one of two signals: the user presses the power button or the screen timeout has passed. This may [lock the device](#How-the-device-locks) +#### Long-pressing on keyguard + +OEMs may choose to enable a long-press action that displays a button at the bottom of lockscreen. This button links to lockscreen customization. This can be achieved by overriding the `long_press_keyguard_customize_lockscreen_enabled` resource in `packages/SystemUI/res/values/config.xml`. + #### On Lockscreen #### On Lockscreen, occluded by an activity diff --git a/packages/SystemUI/docs/device-entry/quickaffordance.md b/packages/SystemUI/docs/device-entry/quickaffordance.md index d662649ac419..afcf846ba2e5 100644 --- a/packages/SystemUI/docs/device-entry/quickaffordance.md +++ b/packages/SystemUI/docs/device-entry/quickaffordance.md @@ -17,7 +17,9 @@ Tests belong in the `packages/SystemUI/tests/src/com/android/systemui/keyguard/d By default, AOSP ships with a "bottom right" and a "bottom left" slot, each with a slot capacity of `1`, allowing only one Quick Affordance on each side of the lock screen. ### Customizing Slots -OEMs may choose to override the IDs and number of slots and/or override the default capacities. This can be achieved by overridding the `config_keyguardQuickAffordanceSlots` resource in `packages/SystemUI/res/values/config.xml`. +OEMs may choose to enable customization of slots. An entry point in settings will appear when overriding the `custom_lockscreen_shortcuts_enabled` resource in `packages/SystemUI/res/values/config.xml`. + +OEMs may also choose to override the IDs and number of slots and/or override the default capacities. This can be achieved by overridding the `config_keyguardQuickAffordanceSlots` resource in `packages/SystemUI/res/values/config.xml`. ### Default Quick Affordances OEMs may also choose to predefine default Quick Affordances for each slot. To achieve this, a developer may override the `config_keyguardQuickAffordanceDefaults` resource in `packages/SystemUI/res/values/config.xml`. Note that defaults only work until the user of the device selects a different quick affordance for that slot, even if they select the "None" option. diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index b4e1b6695d0e..c3651cfa36e7 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -44,6 +44,12 @@ <!-- orientation of the dead zone when touches have recently occurred elsewhere on screen --> <integer name="navigation_bar_deadzone_orientation">0</integer> + <!-- Whether or not lockscreen shortcuts can be customized --> + <bool name="custom_lockscreen_shortcuts_enabled">false</bool> + + <!-- Whether or not long-pressing on keyguard will display to customize lockscreen --> + <bool name="long_press_keyguard_customize_lockscreen_enabled">false</bool> + <bool name="config_dead_zone_flash">false</bool> <!-- Whether to enable dimming navigation buttons when wallpaper is not visible, should be diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 67fdb4c0c213..e8b9f2a35659 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2390,6 +2390,8 @@ <string name="magnification_mode_switch_state_window">Magnify part of screen</string> <!-- Click action label for magnification settings panel. [CHAR LIMIT=NONE] --> <string name="magnification_open_settings_click_label">Open magnification settings</string> + <!-- Click action label for magnification settings panel. [CHAR LIMIT=NONE] --> + <string name="magnification_close_settings_click_label">Close magnification settings</string> <!-- Label of the corner of a rectangle that you can tap and drag to resize the magnification area. [CHAR LIMIT=NONE] --> <string name="magnification_drag_corner_to_resize">Drag corner to resize</string> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java index e057188851f6..7acfbf64ce02 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java @@ -140,6 +140,7 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey long elapsedRealtime = SystemClock.elapsedRealtime(); long secondsInFuture = (long) Math.ceil( (elapsedRealtimeDeadline - elapsedRealtime) / 1000.0); + getKeyguardSecurityCallback().onAttemptLockoutStart(secondsInFuture); mCountdownTimer = new CountDownTimer(secondsInFuture * 1000, 1000) { @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index e54d4739dc97..7cce75fa71c6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -240,6 +240,7 @@ public class KeyguardClockSwitch extends RelativeLayout { if (!animate) { out.setAlpha(0f); + out.setVisibility(INVISIBLE); in.setAlpha(1f); in.setVisibility(VISIBLE); mStatusArea.setTranslationY(statusAreaYTranslation); @@ -255,7 +256,10 @@ public class KeyguardClockSwitch extends RelativeLayout { direction * -mClockSwitchYAmount)); mClockOutAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mClockOutAnim = null; + if (mClockOutAnim == animation) { + out.setVisibility(INVISIBLE); + mClockOutAnim = null; + } } }); @@ -269,7 +273,9 @@ public class KeyguardClockSwitch extends RelativeLayout { mClockInAnim.setStartDelay(CLOCK_IN_START_DELAY_MILLIS); mClockInAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mClockInAnim = null; + if (mClockInAnim == animation) { + mClockInAnim = null; + } } }); @@ -283,7 +289,9 @@ public class KeyguardClockSwitch extends RelativeLayout { mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); mStatusAreaAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mStatusAreaAnim = null; + if (mStatusAreaAnim == animation) { + mStatusAreaAnim = null; + } } }); mStatusAreaAnim.start(); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index d8bf570954df..3defec72981f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -425,10 +425,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS int clockHeight = clock.getLargeClock().getView().getHeight(); return frameHeight / 2 + clockHeight / 2 + mKeyguardLargeClockTopMargin / -2; } else { - // This is only called if we've never shown the large clock as the frame is inflated - // with 'gone', but then the visibility is never set when it is animated away by - // KeyguardClockSwitch, instead it is removed from the view hierarchy. - // TODO(b/261755021): Cleanup Large Frame Visibility int clockHeight = clock.getSmallClock().getView().getHeight(); return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin; } @@ -446,15 +442,11 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS if (mLargeClockFrame.getVisibility() == View.VISIBLE) { return clock.getLargeClock().getView().getHeight(); } else { - // Is not called except in certain edge cases, see comment in getClockBottom - // TODO(b/261755021): Cleanup Large Frame Visibility return clock.getSmallClock().getView().getHeight(); } } boolean isClockTopAligned() { - // Returns false except certain edge cases, see comment in getClockBottom - // TODO(b/261755021): Cleanup Large Frame Visibility return mLargeClockFrame.getVisibility() != View.VISIBLE; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index 3c05299f7a95..b0a5d7c521cd 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -17,6 +17,7 @@ package com.android.keyguard; import android.annotation.CallSuper; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -33,6 +34,10 @@ import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor; +import com.android.systemui.keyguard.bouncer.ui.BouncerMessageView; +import com.android.systemui.keyguard.ui.binder.BouncerMessageViewBinder; +import com.android.systemui.log.BouncerLogger; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.util.ViewController; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -168,6 +173,24 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> /** Determines the message to show in the bouncer when it first appears. */ protected abstract int getInitialMessageResId(); + /** + * Binds the {@link KeyguardInputView#getBouncerMessageView()} view with the provided context. + */ + public void bindMessageView( + @NonNull BouncerMessageInteractor bouncerMessageInteractor, + KeyguardMessageAreaController.Factory messageAreaControllerFactory, + BouncerLogger bouncerLogger, + FeatureFlags featureFlags) { + BouncerMessageView bouncerMessageView = (BouncerMessageView) mView.getBouncerMessageView(); + if (bouncerMessageView != null) { + BouncerMessageViewBinder.bind(bouncerMessageView, + bouncerMessageInteractor, + messageAreaControllerFactory, + bouncerLogger, + featureFlags); + } + } + /** Factory for a {@link KeyguardInputViewController}. */ public static class Factory { private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index 64b1c502b2c3..bcf8e98a8106 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -365,6 +365,7 @@ public class KeyguardPatternViewController final long elapsedRealtime = SystemClock.elapsedRealtime(); final long secondsInFuture = (long) Math.ceil( (elapsedRealtimeDeadline - elapsedRealtime) / 1000.0); + getKeyguardSecurityCallback().onAttemptLockoutStart(secondsInFuture); mCountdownTimer = new CountDownTimer(secondsInFuture * 1000, 1000) { @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java index bf9c3bbddc30..2878df2fe03f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java @@ -87,6 +87,11 @@ public interface KeyguardSecurityCallback { default void onUserInput() { } + /** + * Invoked when the auth input is disabled for specified number of seconds. + * @param seconds Number of seconds for which the auth input is disabled. + */ + default void onAttemptLockoutStart(long seconds) {} /** * Dismisses keyguard and go to unlocked state. diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index b5e54209dab2..57df33e60b48 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -74,6 +74,7 @@ import com.android.systemui.classifier.FalsingA11yDelegate; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.ActivityStarter; @@ -116,6 +117,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final Optional<SideFpsController> mSideFpsController; private final FalsingA11yDelegate mFalsingA11yDelegate; private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor; + private final BouncerMessageInteractor mBouncerMessageInteractor; private int mTranslationY; // Whether the volume keys should be handled by keyguard. If true, then // they will be handled here for specific media types such as music, otherwise @@ -178,6 +180,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard @Override public void onUserInput() { + mBouncerMessageInteractor.onPrimaryBouncerUserInput(); mKeyguardFaceAuthInteractor.onPrimaryBouncerUserInput(); mUpdateMonitor.cancelFaceAuth(); } @@ -207,7 +210,15 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } @Override + public void onAttemptLockoutStart(long seconds) { + mBouncerMessageInteractor.onPrimaryAuthLockedOut(seconds); + } + + @Override public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) { + if (timeoutMs == 0 && !success) { + mBouncerMessageInteractor.onPrimaryAuthIncorrectAttempt(); + } int bouncerSide = SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__DEFAULT; if (mView.isSidedSecurityMode()) { bouncerSide = mView.isSecurityLeftAligned() @@ -392,7 +403,8 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard TelephonyManager telephonyManager, ViewMediatorCallback viewMediatorCallback, AudioManager audioManager, - KeyguardFaceAuthInteractor keyguardFaceAuthInteractor + KeyguardFaceAuthInteractor keyguardFaceAuthInteractor, + BouncerMessageInteractor bouncerMessageInteractor ) { super(view); mLockPatternUtils = lockPatternUtils; @@ -418,6 +430,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mViewMediatorCallback = viewMediatorCallback; mAudioManager = audioManager; mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor; + mBouncerMessageInteractor = bouncerMessageInteractor; } @Override @@ -438,6 +451,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard // Update ViewMediator with the current input method requirements mViewMediatorCallback.setNeedsInput(needsInput()); mView.setOnKeyListener(mOnKeyListener); + showPrimarySecurityScreen(false); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 0be82065e853..fbb43186c846 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -1461,14 +1461,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab ErrorAuthenticationStatus error = (ErrorAuthenticationStatus) status; handleFaceError(error.getMsgId(), error.getMsg()); } else if (status instanceof FailedAuthenticationStatus) { - if (isFaceLockedOut()) { - // TODO b/270090188: remove this hack when biometrics fixes this issue. - // FailedAuthenticationStatus is emitted after ErrorAuthenticationStatus - // for lockout error is received - mLogger.d("onAuthenticationFailed called after" - + " face has been locked out"); - return; - } handleFaceAuthFailed(); } else if (status instanceof HelpAuthenticationStatus) { HelpAuthenticationStatus helpMsg = (HelpAuthenticationStatus) status; @@ -1977,13 +1969,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Override public void onAuthenticationFailed() { - if (isFaceLockedOut()) { - // TODO b/270090188: remove this hack when biometrics fixes this issue. - // onAuthenticationFailed is called after onAuthenticationError - // for lockout error is received - mLogger.d("onAuthenticationFailed called after face has been locked out"); - return; - } handleFaceAuthFailed(); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java index c3bb423e5e4e..fd3c15889822 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java @@ -87,14 +87,15 @@ public class MagnificationSettingsController implements ComponentCallbacks { } /** - * Shows magnification settings panel {@link WindowMagnificationSettings}. + * Toggles the visibility of magnification settings panel {@link WindowMagnificationSettings}. + * We show the panel if it is not visible. Otherwise, hide the panel. */ - void showMagnificationSettings() { + void toggleSettingsPanelVisibility() { if (!mWindowMagnificationSettings.isSettingPanelShowing()) { onConfigurationChanged(mContext.getResources().getConfiguration()); mContext.registerComponentCallbacks(this); } - mWindowMagnificationSettings.showSettingPanel(); + mWindowMagnificationSettings.toggleSettingsPanelVisibility(); } void closeMagnificationSettings() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java index e2b85fa0ac00..2a14dc894d43 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java @@ -171,7 +171,7 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback mModeSwitchesController.setClickListenerDelegate( displayId -> mHandler.post(() -> { - showMagnificationSettingsPanel(displayId); + toggleSettingsPanelVisibility(displayId); })); } @@ -254,11 +254,11 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback } @MainThread - void showMagnificationSettingsPanel(int displayId) { + void toggleSettingsPanelVisibility(int displayId) { final MagnificationSettingsController magnificationSettingsController = mMagnificationSettingsSupplier.get(displayId); if (magnificationSettingsController != null) { - magnificationSettingsController.showMagnificationSettings(); + magnificationSettingsController.toggleSettingsPanelVisibility(); } } @@ -335,7 +335,7 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback @Override public void onClickSettingsButton(int displayId) { mHandler.post(() -> { - showMagnificationSettingsPanel(displayId); + toggleSettingsPanelVisibility(displayId); }); } }; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index a67f706777d9..7c765889a381 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -221,6 +221,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private boolean mAllowDiagonalScrolling = false; private boolean mEditSizeEnable = false; + private boolean mSettingsPanelVisibility = false; @Nullable private final MirrorWindowControl mMirrorWindowControl; @@ -1399,6 +1400,8 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold return; } + mSettingsPanelVisibility = settingsPanelIsShown; + mDragView.setBackground(mContext.getResources().getDrawable(settingsPanelIsShown ? R.drawable.accessibility_window_magnification_drag_handle_background_change : R.drawable.accessibility_window_magnification_drag_handle_background)); @@ -1439,12 +1442,19 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private class MirrorWindowA11yDelegate extends View.AccessibilityDelegate { + private CharSequence getClickAccessibilityActionLabel() { + return mSettingsPanelVisibility + ? mContext.getResources().getString( + R.string.magnification_close_settings_click_label) + : mContext.getResources().getString( + R.string.magnification_open_settings_click_label); + } + @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); final AccessibilityAction clickAction = new AccessibilityAction( - AccessibilityAction.ACTION_CLICK.getId(), mContext.getResources().getString( - R.string.magnification_open_settings_click_label)); + AccessibilityAction.ACTION_CLICK.getId(), getClickAccessibilityActionLabel()); info.addAction(clickAction); info.setClickable(true); info.addAction( diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index 3b1d695e3dad..d0ff9f82db92 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -311,6 +311,14 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mCallback.onSettingsPanelVisibilityChanged(/* shown= */ false); } + public void toggleSettingsPanelVisibility() { + if (!mIsVisible) { + showSettingPanel(); + } else { + hideSettingPanel(); + } + } + public void showSettingPanel() { showSettingPanel(true); } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index a00930508cc7..ee23c51464e5 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -72,10 +72,6 @@ object Flags { val NOTIFICATION_MEMORY_LOGGING_ENABLED = unreleasedFlag(119, "notification_memory_logging_enabled") - @JvmField - val SIMPLIFIED_APPEAR_FRACTION = - releasedFlag(259395680, "simplified_appear_fraction") - // TODO(b/257315550): Tracking Bug val NO_HUN_FOR_OLD_WHEN = releasedFlag(118, "no_hun_for_old_when") @@ -570,7 +566,7 @@ object Flags { // TODO(b/270987164): Tracking Bug @JvmField - val TRACKPAD_GESTURE_FEATURES = unreleasedFlag(1205, "trackpad_gesture_features", teamfood = true) + val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features") // TODO(b/263826204): Tracking Bug @JvmField @@ -705,8 +701,7 @@ object Flags { // TODO(b/259428678): Tracking Bug @JvmField - val KEYBOARD_BACKLIGHT_INDICATOR = - unreleasedFlag(2601, "keyboard_backlight_indicator", teamfood = true) + val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator") // TODO(b/277192623): Tracking Bug @JvmField diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 54da680d8a68..5d3f5f2ee2c2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -333,15 +333,29 @@ public class KeyguardService extends Service { }; private final IKeyguardService.Stub mBinder = new IKeyguardService.Stub() { + private static final String TRACK_NAME = "IKeyguardService"; + + /** + * Helper for tracing the most-recent call on the IKeyguardService interface. + * IKeyguardService is oneway, so we are most interested in the order of the calls as they + * are received. We use an async track to make it easier to visualize in the trace. + * @param name name of the trace section + */ + private static void trace(String name) { + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, 0); + Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TRACK_NAME, name, 0); + } @Override // Binder interface public void addStateMonitorCallback(IKeyguardStateCallback callback) { + trace("addStateMonitorCallback"); checkPermission(); mKeyguardViewMediator.addStateMonitorCallback(callback); } @Override // Binder interface public void verifyUnlock(IKeyguardExitCallback callback) { + trace("verifyUnlock"); Trace.beginSection("KeyguardService.mBinder#verifyUnlock"); checkPermission(); mKeyguardViewMediator.verifyUnlock(callback); @@ -350,6 +364,7 @@ public class KeyguardService extends Service { @Override // Binder interface public void setOccluded(boolean isOccluded, boolean animate) { + trace("setOccluded isOccluded=" + isOccluded + " animate=" + animate); Log.d(TAG, "setOccluded(" + isOccluded + ")"); Trace.beginSection("KeyguardService.mBinder#setOccluded"); @@ -360,24 +375,28 @@ public class KeyguardService extends Service { @Override // Binder interface public void dismiss(IKeyguardDismissCallback callback, CharSequence message) { + trace("dismiss message=" + message); checkPermission(); mKeyguardViewMediator.dismiss(callback, message); } @Override // Binder interface public void onDreamingStarted() { + trace("onDreamingStarted"); checkPermission(); mKeyguardViewMediator.onDreamingStarted(); } @Override // Binder interface public void onDreamingStopped() { + trace("onDreamingStopped"); checkPermission(); mKeyguardViewMediator.onDreamingStopped(); } @Override // Binder interface public void onStartedGoingToSleep(@PowerManager.GoToSleepReason int pmSleepReason) { + trace("onStartedGoingToSleep pmSleepReason=" + pmSleepReason); checkPermission(); mKeyguardViewMediator.onStartedGoingToSleep( WindowManagerPolicyConstants.translateSleepReasonToOffReason(pmSleepReason)); @@ -388,6 +407,8 @@ public class KeyguardService extends Service { @Override // Binder interface public void onFinishedGoingToSleep( @PowerManager.GoToSleepReason int pmSleepReason, boolean cameraGestureTriggered) { + trace("onFinishedGoingToSleep pmSleepReason=" + pmSleepReason + + " cameraGestureTriggered=" + cameraGestureTriggered); checkPermission(); mKeyguardViewMediator.onFinishedGoingToSleep( WindowManagerPolicyConstants.translateSleepReasonToOffReason(pmSleepReason), @@ -399,6 +420,8 @@ public class KeyguardService extends Service { @Override // Binder interface public void onStartedWakingUp( @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) { + trace("onStartedWakingUp pmWakeReason=" + pmWakeReason + + " cameraGestureTriggered=" + cameraGestureTriggered); Trace.beginSection("KeyguardService.mBinder#onStartedWakingUp"); checkPermission(); mKeyguardViewMediator.onStartedWakingUp(pmWakeReason, cameraGestureTriggered); @@ -409,6 +432,7 @@ public class KeyguardService extends Service { @Override // Binder interface public void onFinishedWakingUp() { + trace("onFinishedWakingUp"); Trace.beginSection("KeyguardService.mBinder#onFinishedWakingUp"); checkPermission(); mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.FINISHED_WAKING_UP); @@ -417,6 +441,7 @@ public class KeyguardService extends Service { @Override // Binder interface public void onScreenTurningOn(IKeyguardDrawnCallback callback) { + trace("onScreenTurningOn"); Trace.beginSection("KeyguardService.mBinder#onScreenTurningOn"); checkPermission(); mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNING_ON, @@ -451,6 +476,7 @@ public class KeyguardService extends Service { @Override // Binder interface public void onScreenTurnedOn() { + trace("onScreenTurnedOn"); Trace.beginSection("KeyguardService.mBinder#onScreenTurnedOn"); checkPermission(); mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNED_ON); @@ -460,12 +486,14 @@ public class KeyguardService extends Service { @Override // Binder interface public void onScreenTurningOff() { + trace("onScreenTurningOff"); checkPermission(); mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNING_OFF); } @Override // Binder interface public void onScreenTurnedOff() { + trace("onScreenTurnedOff"); checkPermission(); mKeyguardViewMediator.onScreenTurnedOff(); mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNED_OFF); @@ -474,12 +502,14 @@ public class KeyguardService extends Service { @Override // Binder interface public void setKeyguardEnabled(boolean enabled) { + trace("setKeyguardEnabled enabled" + enabled); checkPermission(); mKeyguardViewMediator.setKeyguardEnabled(enabled); } @Override // Binder interface public void onSystemReady() { + trace("onSystemReady"); Trace.beginSection("KeyguardService.mBinder#onSystemReady"); checkPermission(); mKeyguardViewMediator.onSystemReady(); @@ -488,24 +518,28 @@ public class KeyguardService extends Service { @Override // Binder interface public void doKeyguardTimeout(Bundle options) { + trace("doKeyguardTimeout"); checkPermission(); mKeyguardViewMediator.doKeyguardTimeout(options); } @Override // Binder interface public void setSwitchingUser(boolean switching) { + trace("setSwitchingUser switching=" + switching); checkPermission(); mKeyguardViewMediator.setSwitchingUser(switching); } @Override // Binder interface public void setCurrentUser(int userId) { + trace("setCurrentUser userId=" + userId); checkPermission(); mKeyguardViewMediator.setCurrentUser(userId); } - @Override + @Override // Binder interface public void onBootCompleted() { + trace("onBootCompleted"); checkPermission(); mKeyguardViewMediator.onBootCompleted(); } @@ -515,28 +549,33 @@ public class KeyguardService extends Service { * {@code IRemoteAnimationRunner#onAnimationStart} instead. */ @Deprecated - @Override + @Override // Binder interface public void startKeyguardExitAnimation(long startTime, long fadeoutDuration) { + trace("startKeyguardExitAnimation startTime=" + startTime + + " fadeoutDuration=" + fadeoutDuration); Trace.beginSection("KeyguardService.mBinder#startKeyguardExitAnimation"); checkPermission(); mKeyguardViewMediator.startKeyguardExitAnimation(startTime, fadeoutDuration); Trace.endSection(); } - @Override + @Override // Binder interface public void onShortPowerPressedGoHome() { + trace("onShortPowerPressedGoHome"); checkPermission(); mKeyguardViewMediator.onShortPowerPressedGoHome(); } - @Override + @Override // Binder interface public void dismissKeyguardToLaunch(Intent intentToLaunch) { + trace("dismissKeyguardToLaunch"); checkPermission(); mKeyguardViewMediator.dismissKeyguardToLaunch(intentToLaunch); } - @Override + @Override // Binder interface public void onSystemKeyPressed(int keycode) { + trace("onSystemKeyPressed keycode=" + keycode); checkPermission(); mKeyguardViewMediator.onSystemKeyPressed(keycode); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt index ea6700e92731..ca430da0ffce 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt @@ -17,12 +17,14 @@ package com.android.systemui.keyguard.domain.interactor +import android.content.Context import android.content.Intent import android.content.IntentFilter import android.view.accessibility.AccessibilityManager import androidx.annotation.VisibleForTesting import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger +import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -55,6 +57,7 @@ import kotlinx.coroutines.launch class KeyguardLongPressInteractor @Inject constructor( + @Application private val appContext: Context, @Application private val scope: CoroutineScope, transitionInteractor: KeyguardTransitionInteractor, repository: KeyguardRepository, @@ -169,7 +172,8 @@ constructor( private fun isFeatureEnabled(): Boolean { return featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED) && - featureFlags.isEnabled(Flags.REVAMPED_WALLPAPER_UI) + featureFlags.isEnabled(Flags.REVAMPED_WALLPAPER_UI) && + appContext.resources.getBoolean(R.bool.long_press_keyguard_customize_lockscreen_enabled) } /** Updates application state to ask to show the menu. */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 8e65c4d0a836..22753376a5d3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -19,12 +19,15 @@ package com.android.systemui.keyguard.domain.interactor import android.app.AlertDialog import android.app.admin.DevicePolicyManager +import android.content.Context import android.content.Intent import android.util.Log import com.android.internal.widget.LockPatternUtils +import com.android.systemui.R import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled import com.android.systemui.dock.DockManager @@ -75,6 +78,7 @@ constructor( private val devicePolicyManager: DevicePolicyManager, private val dockManager: DockManager, @Background private val backgroundDispatcher: CoroutineDispatcher, + @Application private val appContext: Context, ) { private val isUsingRepository: Boolean get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES) @@ -408,7 +412,8 @@ constructor( name = Contract.FlagsTable.FLAG_NAME_CUSTOM_LOCK_SCREEN_QUICK_AFFORDANCES_ENABLED, value = !isFeatureDisabledByDevicePolicy() && - featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES), + featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES) && + appContext.resources.getBoolean(R.bool.custom_lockscreen_shortcuts_enabled), ), KeyguardPickerFlag( name = Contract.FlagsTable.FLAG_NAME_CUSTOM_CLOCKS_ENABLED, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/BouncerMessageViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/BouncerMessageViewBinder.kt new file mode 100644 index 000000000000..5b40dd7ad884 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/BouncerMessageViewBinder.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2023 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.keyguard.ui.binder + +import android.text.TextUtils +import android.util.PluralsMessageFormatter +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.keyguard.BouncerKeyguardMessageArea +import com.android.keyguard.KeyguardMessageArea +import com.android.keyguard.KeyguardMessageAreaController +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor +import com.android.systemui.keyguard.bouncer.shared.model.Message +import com.android.systemui.keyguard.bouncer.ui.BouncerMessageView +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.log.BouncerLogger +import kotlinx.coroutines.launch + +object BouncerMessageViewBinder { + @JvmStatic + fun bind( + view: BouncerMessageView, + interactor: BouncerMessageInteractor, + factory: KeyguardMessageAreaController.Factory, + bouncerLogger: BouncerLogger, + featureFlags: FeatureFlags + ) { + view.repeatWhenAttached { + if (!featureFlags.isEnabled(Flags.REVAMPED_BOUNCER_MESSAGES)) { + view.primaryMessageView?.disable() + view.secondaryMessageView?.disable() + return@repeatWhenAttached + } + view.init(factory) + view.primaryMessage?.setIsVisible(true) + view.secondaryMessage?.setIsVisible(true) + repeatOnLifecycle(Lifecycle.State.STARTED) { + bouncerLogger.startBouncerMessageInteractor() + launch { + interactor.bouncerMessage.collect { + bouncerLogger.bouncerMessageUpdated(it) + updateView( + view.primaryMessage, + view.primaryMessageView, + message = it?.message, + allowTruncation = true, + ) + updateView( + view.secondaryMessage, + view.secondaryMessageView, + message = it?.secondaryMessage, + allowTruncation = false, + ) + view.requestLayout() + } + } + } + } + } + + private fun updateView( + controller: KeyguardMessageAreaController<KeyguardMessageArea>?, + view: BouncerKeyguardMessageArea?, + message: Message?, + allowTruncation: Boolean = false, + ) { + if (view == null || controller == null) return + if (message?.message != null || message?.messageResId != null) { + controller.setIsVisible(true) + var newMessage = message.message ?: "" + if (message.messageResId != null && message.messageResId != 0) { + newMessage = view.resources.getString(message.messageResId) + if (message.formatterArgs != null) { + newMessage = + PluralsMessageFormatter.format( + view.resources, + message.formatterArgs, + message.messageResId + ) + } + } + controller.setMessage(newMessage, message.animate) + } else { + controller.setIsVisible(false) + controller.setMessage(0) + } + message?.colorState?.let { controller.setNextMessageColor(it) } + view.ellipsize = + if (allowTruncation) TextUtils.TruncateAt.END else TextUtils.TruncateAt.MARQUEE + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt index c1aefc7bcbd7..dd1a9d21f2d6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt @@ -22,16 +22,20 @@ import android.view.ViewGroup import android.window.OnBackAnimationCallback import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.keyguard.KeyguardMessageAreaController import com.android.keyguard.KeyguardSecurityContainerController import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardSecurityView import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.dagger.KeyguardBouncerComponent +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor import com.android.systemui.keyguard.data.BouncerViewDelegate import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.log.BouncerLogger import com.android.systemui.plugins.ActivityStarter import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.filter @@ -44,7 +48,11 @@ object KeyguardBouncerViewBinder { view: ViewGroup, viewModel: KeyguardBouncerViewModel, primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, - componentFactory: KeyguardBouncerComponent.Factory + componentFactory: KeyguardBouncerComponent.Factory, + messageAreaControllerFactory: KeyguardMessageAreaController.Factory, + bouncerMessageInteractor: BouncerMessageInteractor, + bouncerLogger: BouncerLogger, + featureFlags: FeatureFlags, ) { // Builds the KeyguardSecurityContainerController from bouncer view group. val securityContainerController: KeyguardSecurityContainerController = @@ -125,8 +133,16 @@ object KeyguardBouncerViewBinder { securityContainerController.onResume( KeyguardSecurityView.SCREEN_ON ) + bouncerLogger.bindingBouncerMessageView() + it.bindMessageView( + bouncerMessageInteractor, + messageAreaControllerFactory, + bouncerLogger, + featureFlags + ) } } else { + bouncerMessageInteractor.onBouncerBeingHidden() securityContainerController.onBouncerVisibilityChanged( /* isVisible= */ false ) diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 5818fd0634a2..bbc86c839ad2 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -22,6 +22,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_ import static com.android.systemui.classifier.Classifier.BACK_GESTURE; import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadFourFingerSwipe; import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadMultiFingerSwipe; +import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll; import android.annotation.NonNull; import android.app.ActivityManager; @@ -38,6 +39,7 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; import android.hardware.input.InputManager; +import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.SystemClock; @@ -194,6 +196,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final ViewConfiguration mViewConfiguration; private final WindowManager mWindowManager; private final IWindowManager mWindowManagerService; + private final InputManager mInputManager; private final Optional<Pip> mPipOptional; private final Optional<DesktopMode> mDesktopModeOptional; private final FalsingManager mFalsingManager; @@ -205,6 +208,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final int mDisplayId; private final Executor mMainExecutor; + private final Handler mMainHandler; private final Executor mBackgroundExecutor; private final Rect mPipExcludedBounds = new Rect(); @@ -249,6 +253,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private boolean mIsAttached; private boolean mIsGesturalModeEnabled; + private boolean mIsTrackpadConnected; + private boolean mUsingThreeButtonNav; private boolean mIsEnabled; private boolean mIsNavBarShownTransiently; private boolean mIsBackGestureAllowed; @@ -346,12 +352,48 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } }; + private final InputManager.InputDeviceListener mInputDeviceListener = + new InputManager.InputDeviceListener() { + + // Only one trackpad can be connected to a device at a time, since it takes over the + // only USB port. + private int mTrackpadDeviceId; + + @Override + public void onInputDeviceAdded(int deviceId) { + if (isTrackpadDevice(deviceId)) { + mTrackpadDeviceId = deviceId; + update(true /* isTrackpadConnected */); + } + } + + @Override + public void onInputDeviceChanged(int deviceId) { } + + @Override + public void onInputDeviceRemoved(int deviceId) { + if (mTrackpadDeviceId == deviceId) { + update(false /* isTrackpadConnected */); + } + } + + private void update(boolean isTrackpadConnected) { + boolean isPreviouslyTrackpadConnected = mIsTrackpadConnected; + mIsTrackpadConnected = isTrackpadConnected; + if (isPreviouslyTrackpadConnected != mIsTrackpadConnected) { + updateIsEnabled(); + updateCurrentUserResources(); + } + } + }; + EdgeBackGestureHandler( Context context, OverviewProxyService overviewProxyService, SysUiState sysUiState, PluginManager pluginManager, @Main Executor executor, + @Main Handler handler, @Background Executor backgroundExecutor, UserTracker userTracker, ProtoTracer protoTracer, @@ -360,6 +402,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack ViewConfiguration viewConfiguration, WindowManager windowManager, IWindowManager windowManagerService, + InputManager inputManager, Optional<Pip> pipOptional, Optional<DesktopMode> desktopModeOptional, FalsingManager falsingManager, @@ -370,6 +413,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mContext = context; mDisplayId = context.getDisplayId(); mMainExecutor = executor; + mMainHandler = handler; mBackgroundExecutor = backgroundExecutor; mUserTracker = userTracker; mOverviewProxyService = overviewProxyService; @@ -381,6 +425,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mViewConfiguration = viewConfiguration; mWindowManager = windowManager; mWindowManagerService = windowManagerService; + mInputManager = inputManager; mPipOptional = pipOptional; mDesktopModeOptional = desktopModeOptional; mFalsingManager = falsingManager; @@ -389,6 +434,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mFeatureFlags = featureFlags; mLightBarControllerProvider = lightBarControllerProvider; mLastReportedConfig.setTo(mContext.getResources().getConfiguration()); + mIsTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled( + Flags.TRACKPAD_GESTURE_FEATURES); ComponentName recentsComponentName = ComponentName.unflattenFromString( context.getString(com.android.internal.R.string.config_recentsComponentName)); if (recentsComponentName != null) { @@ -420,7 +467,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack ViewConfiguration.getLongPressTimeout()); mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver( - mContext.getMainThreadHandler(), mContext, this::onNavigationSettingsChanged); + mMainHandler, mContext, this::onNavigationSettingsChanged); updateCurrentUserResources(); } @@ -507,6 +554,15 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mProtoTracer.add(this); mOverviewProxyService.addCallback(mQuickSwitchListener); mSysUiState.addCallback(mSysUiStateCallback); + if (mIsTrackpadGestureFeaturesEnabled) { + mInputManager.registerInputDeviceListener(mInputDeviceListener, mMainHandler); + int [] inputDevices = mInputManager.getInputDeviceIds(); + for (int inputDeviceId : inputDevices) { + if (isTrackpadDevice(inputDeviceId)) { + mIsTrackpadConnected = true; + } + } + } updateIsEnabled(); mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); } @@ -519,6 +575,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mProtoTracer.remove(this); mOverviewProxyService.removeCallback(mQuickSwitchListener); mSysUiState.removeCallback(mSysUiStateCallback); + mInputManager.unregisterInputDeviceListener(mInputDeviceListener); updateIsEnabled(); mUserTracker.removeCallback(mUserChangedCallback); } @@ -527,7 +584,9 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack * @see NavigationModeController.ModeChangedListener#onNavigationModeChanged */ public void onNavigationModeChanged(int mode) { - mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode); + mUsingThreeButtonNav = QuickStepContract.isLegacyMode(mode); + mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode) || ( + mIsTrackpadGestureFeaturesEnabled && mUsingThreeButtonNav && mIsTrackpadConnected); updateIsEnabled(); updateCurrentUserResources(); } @@ -617,8 +676,6 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack // Add a nav bar panel window mIsNewBackAffordanceEnabled = mFeatureFlags.isEnabled(Flags.NEW_BACK_AFFORDANCE); - mIsTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled( - Flags.TRACKPAD_GESTURE_FEATURES); resetEdgeBackPlugin(); mPluginManager.addPluginListener( this, NavigationEdgeBackPlugin.class, /*allowMultiple=*/ false); @@ -822,6 +879,12 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mDisplaySize.y - insets.bottom); } + private boolean isTrackpadDevice(int deviceId) { + InputDevice inputDevice = mInputManager.getInputDevice(deviceId); + return inputDevice.getSources() == (InputDevice.SOURCE_MOUSE + | InputDevice.SOURCE_TOUCHPAD); + } + private boolean desktopExcludeRegionContains(int x, int y) { return mDesktopModeExcludeRegion.contains(x, y); } @@ -946,17 +1009,21 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mMLResults = 0; mLogGesture = false; mInRejectedExclusion = false; - // Trackpad back gestures don't have zones, so we don't need to check if the down event - // is within insets. Also we don't allow back for button press from the trackpad, and - // yet we do with a mouse. boolean isWithinInsets = isWithinInsets((int) ev.getX(), (int) ev.getY()); - mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed - && !isButtonPressFromTrackpad(ev) - && (isTrackpadMultiFingerSwipe || isWithinInsets) + boolean isBackAllowedCommon = !mDisabledForQuickstep && mIsBackGestureAllowed && !mGestureBlockingActivityRunning && !QuickStepContract.isBackGestureDisabled(mSysUiFlags) - && (isValidTrackpadBackGesture(isTrackpadMultiFingerSwipe) - || isWithinTouchRegion((int) ev.getX(), (int) ev.getY())); + && !isTrackpadScroll(mIsTrackpadGestureFeaturesEnabled, ev); + if (isTrackpadMultiFingerSwipe) { + // Trackpad back gestures don't have zones, so we don't need to check if the down + // event is within insets. + mAllowGesture = isBackAllowedCommon && isValidTrackpadBackGesture( + isTrackpadMultiFingerSwipe); + } else { + mAllowGesture = isBackAllowedCommon && !mUsingThreeButtonNav && isWithinInsets + && isWithinTouchRegion((int) ev.getX(), (int) ev.getY()) + && !isButtonPressFromTrackpad(ev); + } if (mAllowGesture) { mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge); mEdgeBackPlugin.onMotionEvent(ev); @@ -1059,6 +1126,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } private boolean isButtonPressFromTrackpad(MotionEvent ev) { + // We don't allow back for button press from the trackpad, and yet we do with a mouse. int sources = InputManager.getInstance().getInputDevice(ev.getDeviceId()).getSources(); return (sources & (SOURCE_MOUSE | SOURCE_TOUCHPAD)) == sources && ev.getButtonState() != 0; } @@ -1182,6 +1250,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack pw.println(" mPredictionLog=" + String.join("\n", mPredictionLog)); pw.println(" mGestureLogInsideInsets=" + String.join("\n", mGestureLogInsideInsets)); pw.println(" mGestureLogOutsideInsets=" + String.join("\n", mGestureLogOutsideInsets)); + pw.println(" mIsTrackpadConnected=" + mIsTrackpadConnected); + pw.println(" mUsingThreeButtonNav=" + mUsingThreeButtonNav); pw.println(" mEdgeBackPlugin=" + mEdgeBackPlugin); if (mEdgeBackPlugin != null) { mEdgeBackPlugin.dump(pw); @@ -1231,6 +1301,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final SysUiState mSysUiState; private final PluginManager mPluginManager; private final Executor mExecutor; + private final Handler mHandler; private final Executor mBackgroundExecutor; private final UserTracker mUserTracker; private final ProtoTracer mProtoTracer; @@ -1239,6 +1310,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final ViewConfiguration mViewConfiguration; private final WindowManager mWindowManager; private final IWindowManager mWindowManagerService; + private final InputManager mInputManager; private final Optional<Pip> mPipOptional; private final Optional<DesktopMode> mDesktopModeOptional; private final FalsingManager mFalsingManager; @@ -1253,6 +1325,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack SysUiState sysUiState, PluginManager pluginManager, @Main Executor executor, + @Main Handler handler, @Background Executor backgroundExecutor, UserTracker userTracker, ProtoTracer protoTracer, @@ -1261,6 +1334,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack ViewConfiguration viewConfiguration, WindowManager windowManager, IWindowManager windowManagerService, + InputManager inputManager, Optional<Pip> pipOptional, Optional<DesktopMode> desktopModeOptional, FalsingManager falsingManager, @@ -1273,6 +1347,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mSysUiState = sysUiState; mPluginManager = pluginManager; mExecutor = executor; + mHandler = handler; mBackgroundExecutor = backgroundExecutor; mUserTracker = userTracker; mProtoTracer = protoTracer; @@ -1281,6 +1356,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mViewConfiguration = viewConfiguration; mWindowManager = windowManager; mWindowManagerService = windowManagerService; + mInputManager = inputManager; mPipOptional = pipOptional; mDesktopModeOptional = desktopModeOptional; mFalsingManager = falsingManager; @@ -1298,6 +1374,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mSysUiState, mPluginManager, mExecutor, + mHandler, mBackgroundExecutor, mUserTracker, mProtoTracer, @@ -1306,6 +1383,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mViewConfiguration, mWindowManager, mWindowManagerService, + mInputManager, mPipOptional, mDesktopModeOptional, mFalsingManager, diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java index 50e8aa7b2046..bf5e4423ff38 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java @@ -17,11 +17,18 @@ package com.android.systemui.navigationbar.gestural; import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE; +import static android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE; import android.view.MotionEvent; public final class Utilities { + public static boolean isTrackpadScroll(boolean isTrackpadGestureFeaturesEnabled, + MotionEvent event) { + return isTrackpadGestureFeaturesEnabled + && event.getClassification() == CLASSIFICATION_TWO_FINGER_SWIPE; + } + public static boolean isTrackpadMultiFingerSwipe(boolean isTrackpadGestureFeaturesEnabled, MotionEvent event) { return isTrackpadGestureFeaturesEnabled diff --git a/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl new file mode 100644 index 000000000000..3e947d9d2b16 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 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.notetask; + +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.os.UserHandle; + +/** A service to help with controlling the state of notes app bubble through the system user. */ +interface INoteTaskBubblesService { + + boolean areBubblesAvailable(); + + void showOrHideAppBubble(in Intent intent, in UserHandle userHandle, in Icon icon); +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt new file mode 100644 index 000000000000..ec205f87d9fa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2023 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.notetask + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Icon +import android.os.IBinder +import android.os.UserHandle +import com.android.internal.infra.ServiceConnector +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.DebugLogger.debugLog +import com.android.wm.shell.bubbles.Bubbles +import java.util.Optional +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +/** + * A utility class to help interact with [Bubbles] as system user. The SysUI instance running as + * system user is the only instance that has the instance of [Bubbles] that manages the notes app + * bubble for all users. + * + * <p>Note: This class is made overridable so that a fake can be created for as mocking suspending + * functions is not supported by the Android tree's version of mockito. + */ +@SysUISingleton +open class NoteTaskBubblesController +@Inject +constructor( + @Application private val context: Context, + @Background private val bgDispatcher: CoroutineDispatcher +) { + + private val serviceConnector: ServiceConnector<INoteTaskBubblesService> = + ServiceConnector.Impl( + context, + Intent(context, NoteTaskBubblesService::class.java), + Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE, + UserHandle.USER_SYSTEM, + INoteTaskBubblesService.Stub::asInterface + ) + + /** Returns whether notes app bubble is supported. */ + open suspend fun areBubblesAvailable(): Boolean = + withContext(bgDispatcher) { + suspendCoroutine { continuation -> + serviceConnector + .postForResult { it.areBubblesAvailable() } + .whenComplete { available, error -> + if (error != null) { + debugLog(error = error) { "Failed to query Bubbles as system user." } + } + continuation.resume(available ?: false) + } + } + } + + /** Calls the [Bubbles.showOrHideAppBubble] API as [UserHandle.USER_SYSTEM]. */ + open suspend fun showOrHideAppBubble( + intent: Intent, + userHandle: UserHandle, + icon: Icon + ) { + withContext(bgDispatcher) { + serviceConnector + .post { it.showOrHideAppBubble(intent, userHandle, icon) } + .whenComplete { _, error -> + if (error != null) { + debugLog(error = error) { + "Failed to show notes app bubble for intent $intent, " + + "user $userHandle, and icon $icon." + } + } else { + debugLog { + "Call to show notes app bubble for intent $intent, " + + "user $userHandle, and icon $icon successful." + } + } + } + } + } + + /** + * A helper service to call [Bubbles] APIs that should always be called from the system user + * instance of SysUI. + * + * <p>Note: This service always runs in the SysUI process running on the system user + * irrespective of which user started the service. This is required so that the correct instance + * of {@link Bubbles} is injected. This is set via attribute {@code android:singleUser=”true”} + * in AndroidManifest. + */ + class NoteTaskBubblesService + @Inject + constructor(private val mOptionalBubbles: Optional<Bubbles>) : Service() { + + override fun onBind(intent: Intent): IBinder { + return object : INoteTaskBubblesService.Stub() { + override fun areBubblesAvailable() = mOptionalBubbles.isPresent + + override fun showOrHideAppBubble( + intent: Intent, + userHandle: UserHandle, + icon: Icon + ) { + mOptionalBubbles.ifPresentOrElse( + { bubbles -> bubbles.showOrHideAppBubble(intent, userHandle, icon) }, + { + debugLog { + "Failed to show or hide bubble for intent $intent," + + "user $user, and icon $icon as bubble is empty." + } + } + ) + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 25272ae097a1..efbec29bcff1 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -33,25 +33,27 @@ import android.content.pm.ShortcutManager import android.graphics.drawable.Icon import android.os.UserHandle import android.os.UserManager +import android.provider.Settings import android.widget.Toast import androidx.annotation.VisibleForTesting import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.notetask.NoteTaskRoleManagerExt.createNoteShortcutInfoAsUser import com.android.systemui.notetask.NoteTaskRoleManagerExt.getDefaultRoleHolderAsUser import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity -import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity import com.android.systemui.settings.UserTracker import com.android.systemui.shared.system.ActivityManagerKt.isInForeground import com.android.systemui.util.kotlin.getOrNull +import com.android.systemui.util.settings.SecureSettings import com.android.wm.shell.bubbles.Bubble -import com.android.wm.shell.bubbles.Bubbles import com.android.wm.shell.bubbles.Bubbles.BubbleExpandListener -import java.util.Optional import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * Entry point for creating and managing note. @@ -69,13 +71,15 @@ constructor( private val shortcutManager: ShortcutManager, private val resolver: NoteTaskInfoResolver, private val eventLogger: NoteTaskEventLogger, - private val optionalBubbles: Optional<Bubbles>, + private val noteTaskBubblesController: NoteTaskBubblesController, private val userManager: UserManager, private val keyguardManager: KeyguardManager, private val activityManager: ActivityManager, @NoteTaskEnabledKey private val isEnabled: Boolean, private val devicePolicyManager: DevicePolicyManager, private val userTracker: UserTracker, + private val secureSettings: SecureSettings, + @Application private val applicationScope: CoroutineScope ) { @VisibleForTesting val infoReference = AtomicReference<NoteTaskInfo?>() @@ -100,18 +104,6 @@ constructor( } } - /** Starts [LaunchNoteTaskProxyActivity] on the given [user]. */ - fun startNoteTaskProxyActivityForUser(user: UserHandle) { - context.startActivityAsUser( - Intent().apply { - component = - ComponentName(context, LaunchNoteTaskManagedProfileProxyActivity::class.java) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }, - user - ) - } - /** Starts the notes role setting. */ fun startNotesRoleSetting(activityContext: Context, entryPoint: NoteTaskEntryPoint?) { val user = @@ -146,7 +138,7 @@ constructor( userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) }?.userHandle ?: userTracker.userHandle } else { - userTracker.userHandle + secureSettings.preferredUser } /** @@ -175,7 +167,19 @@ constructor( ) { if (!isEnabled) return - val bubbles = optionalBubbles.getOrNull() ?: return + applicationScope.launch { awaitShowNoteTaskAsUser(entryPoint, user) } + } + + private suspend fun awaitShowNoteTaskAsUser( + entryPoint: NoteTaskEntryPoint, + user: UserHandle, + ) { + if (!isEnabled) return + + if (!noteTaskBubblesController.areBubblesAvailable()) { + debugLog { "Bubbles not available in the system user SysUI instance" } + return + } // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing. if (!userManager.isUserUnlocked) return @@ -210,7 +214,7 @@ constructor( val intent = createNoteTaskIntent(info) val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) - bubbles.showOrHideAppBubble(intent, user, icon) + noteTaskBubblesController.showOrHideAppBubble(intent, user, icon) // App bubble logging happens on `onBubbleExpandChanged`. debugLog { "onShowNoteTask - opened as app bubble: $info" } } @@ -284,15 +288,55 @@ constructor( } /** + * Like [updateNoteTaskAsUser] but automatically apply to the current user and all its work + * profiles. + * + * @see updateNoteTaskAsUser + * @see UserTracker.userHandle + * @see UserTracker.userProfiles + */ + fun updateNoteTaskForCurrentUserAndManagedProfiles() { + updateNoteTaskAsUser(userTracker.userHandle) + for (profile in userTracker.userProfiles) { + if (userManager.isManagedProfile(profile.id)) { + updateNoteTaskAsUser(profile.userHandle) + } + } + } + + /** * Updates all [NoteTaskController] related information, including but not exclusively the * widget shortcut created by the [user] - by default it will use the current user. * + * If the user is not current user, the update will be dispatched to run in that user's process. + * * Keep in mind the shortcut API has a * [rate limiting](https://developer.android.com/develop/ui/views/launch/shortcuts/managing-shortcuts#rate-limiting) * and may not be updated in real-time. To reduce the chance of stale shortcuts, we run the * function during System UI initialization. */ fun updateNoteTaskAsUser(user: UserHandle) { + if (!userManager.isUserUnlocked(user)) { + debugLog { "updateNoteTaskAsUser call but user locked: user=$user" } + return + } + + if (user == userTracker.userHandle) { + updateNoteTaskAsUserInternal(user) + } else { + // TODO(b/278729185): Replace fire and forget service with a bounded service. + val intent = NoteTaskControllerUpdateService.createIntent(context) + context.startServiceAsUser(intent, user) + } + } + + @InternalNoteTaskApi + fun updateNoteTaskAsUserInternal(user: UserHandle) { + if (!userManager.isUserUnlocked(user)) { + debugLog { "updateNoteTaskAsUserInternal call but user locked: user=$user" } + return + } + val packageName = roleManager.getDefaultRoleHolderAsUser(ROLE_NOTES, user) val hasNotesRoleHolder = isEnabled && !packageName.isNullOrEmpty() @@ -310,20 +354,20 @@ constructor( /** @see OnRoleHoldersChangedListener */ fun onRoleHoldersChanged(roleName: String, user: UserHandle) { if (roleName != ROLE_NOTES) return - if (!userManager.isUserUnlocked(user)) { - debugLog { "onRoleHoldersChanged call but user locked: role=$roleName, user=$user" } - return - } - if (user == userTracker.userHandle) { - updateNoteTaskAsUser(user) - } else { - // TODO(b/278729185): Replace fire and forget service with a bounded service. - val intent = NoteTaskControllerUpdateService.createIntent(context) - context.startServiceAsUser(intent, user) - } + updateNoteTaskAsUser(user) } + private val SecureSettings.preferredUser: UserHandle + get() { + val userId = + secureSettings.getInt( + Settings.Secure.DEFAULT_NOTE_TASK_PROFILE, + userTracker.userHandle.identifier, + ) + return UserHandle.of(userId) + } + companion object { val TAG = NoteTaskController::class.simpleName.orEmpty() diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt index 26b35cc8ffd1..3e352afe3832 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt @@ -44,7 +44,7 @@ constructor( override fun onCreate() { super.onCreate() // TODO(b/278729185): Replace fire and forget service with a bounded service. - controller.updateNoteTaskAsUser(user) + controller.updateNoteTaskAsUserInternal(user) stopSelf() } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt index 221ff65e4dfe..fe1034a6aa32 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -15,7 +15,10 @@ */ package com.android.systemui.notetask +import android.app.role.OnRoleHoldersChangedListener import android.app.role.RoleManager +import android.content.Context +import android.content.pm.UserInfo import android.os.UserHandle import android.view.KeyEvent import android.view.KeyEvent.KEYCODE_N @@ -54,6 +57,7 @@ constructor( initializeHandleSystemKey() initializeOnRoleHoldersChanged() initializeOnUserUnlocked() + initializeUserTracker() } /** @@ -79,7 +83,7 @@ constructor( private fun initializeOnRoleHoldersChanged() { roleManager.addOnRoleHoldersChangedListenerAsUser( backgroundExecutor, - controller::onRoleHoldersChanged, + callbacks, UserHandle.ALL, ) } @@ -93,18 +97,41 @@ constructor( */ private fun initializeOnUserUnlocked() { if (keyguardUpdateMonitor.isUserUnlocked(userTracker.userId)) { - controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle) - } else { - keyguardUpdateMonitor.registerCallback(onUserUnlockedCallback) + controller.updateNoteTaskForCurrentUserAndManagedProfiles() } + keyguardUpdateMonitor.registerCallback(callbacks) } - // KeyguardUpdateMonitor.registerCallback uses a weak reference, so we need a hard reference. - private val onUserUnlockedCallback = - object : KeyguardUpdateMonitorCallback() { + private fun initializeUserTracker() { + userTracker.addCallback(callbacks, backgroundExecutor) + } + + // Some callbacks use a weak reference, so we play safe and keep a hard reference to them all. + private val callbacks = + object : + KeyguardUpdateMonitorCallback(), + CommandQueue.Callbacks, + UserTracker.Callback, + OnRoleHoldersChangedListener { + + override fun handleSystemKey(key: KeyEvent) { + key.toNoteTaskEntryPointOrNull()?.let(controller::showNoteTask) + } + + override fun onRoleHoldersChanged(roleName: String, user: UserHandle) { + controller.onRoleHoldersChanged(roleName, user) + } + override fun onUserUnlocked() { - controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle) - keyguardUpdateMonitor.removeCallback(this) + controller.updateNoteTaskForCurrentUserAndManagedProfiles() + } + + override fun onUserChanged(newUser: Int, userContext: Context) { + controller.updateNoteTaskForCurrentUserAndManagedProfiles() + } + + override fun onProfilesChanged(profiles: List<UserInfo>) { + controller.updateNoteTaskForCurrentUserAndManagedProfiles() } } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt index 109cfeec0723..c0e688f0f82f 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt @@ -26,7 +26,6 @@ import com.android.systemui.flags.Flags import com.android.systemui.notetask.quickaffordance.NoteTaskQuickAffordanceModule import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity -import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity import dagger.Binds import dagger.Module import dagger.Provides @@ -40,12 +39,12 @@ interface NoteTaskModule { @[Binds IntoMap ClassKey(NoteTaskControllerUpdateService::class)] fun NoteTaskControllerUpdateService.bindNoteTaskControllerUpdateService(): Service + @[Binds IntoMap ClassKey(NoteTaskBubblesController.NoteTaskBubblesService::class)] + fun NoteTaskBubblesController.NoteTaskBubblesService.bindNoteTaskBubblesService(): Service + @[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)] fun LaunchNoteTaskActivity.bindNoteTaskLauncherActivity(): Activity - @[Binds IntoMap ClassKey(LaunchNoteTaskManagedProfileProxyActivity::class)] - fun LaunchNoteTaskManagedProfileProxyActivity.bindNoteTaskLauncherProxyActivity(): Activity - @[Binds IntoMap ClassKey(LaunchNotesRoleSettingsTrampolineActivity::class)] fun LaunchNotesRoleSettingsTrampolineActivity.bindLaunchNotesRoleSettingsTrampolineActivity(): Activity diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt index 8ca13b9776bb..493330af56ce 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt @@ -19,61 +19,18 @@ package com.android.systemui.notetask.shortcut import android.content.Context import android.content.Intent import android.os.Bundle -import android.os.UserHandle -import android.os.UserManager import androidx.activity.ComponentActivity -import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.notetask.NoteTaskController import com.android.systemui.notetask.NoteTaskEntryPoint -import com.android.systemui.settings.UserTracker import javax.inject.Inject /** Activity responsible for launching the note experience, and finish. */ -class LaunchNoteTaskActivity -@Inject -constructor( - private val controller: NoteTaskController, - private val userManager: UserManager, - private val userTracker: UserTracker, -) : ComponentActivity() { +class LaunchNoteTaskActivity @Inject constructor(private val controller: NoteTaskController) : + ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - // Under the hood, notes app shortcuts are shown in a floating window, called Bubble. - // Bubble API is only available in the main user but not work profile. - // - // On devices with work profile (WP), SystemUI provides both personal notes app shortcuts & - // work profile notes app shortcuts. In order to make work profile notes app shortcuts to - // show in Bubble, a few redirections across users are required: - // 1. When `LaunchNoteTaskActivity` is started in the work profile user, we launch - // `LaunchNoteTaskManagedProfileProxyActivity` on the main user, which has access to the - // Bubble API. - // 2. `LaunchNoteTaskManagedProfileProxyActivity` calls `Bubble#showOrHideAppBubble` with - // the work profile user ID. - // 3. Bubble renders the work profile notes app activity in a floating window, which is - // hosted in the main user. - // - // WP main user - // ------------------------ ------------------------------------------- - // | LaunchNoteTaskActivity | -> | LaunchNoteTaskManagedProfileProxyActivity | - // ------------------------ ------------------------------------------- - // | - // main user | - // ---------------------------- | - // | Bubble#showOrHideAppBubble | <-------------- - // | (with WP user ID) | - // ---------------------------- - val mainUser: UserHandle? = userManager.mainUser - if (userManager.isManagedProfile) { - if (mainUser == null) { - debugLog { "Can't find the main user. Skipping the notes app launch." } - } else { - controller.startNoteTaskProxyActivityForUser(mainUser) - } - } else { - controller.showNoteTask(entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT) - } + controller.showNoteTaskAsUser(entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT, user) finish() } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt deleted file mode 100644 index 3259b0dcc53d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2023 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.notetask.shortcut - -import android.os.Build -import android.os.Bundle -import android.os.UserManager -import android.util.Log -import androidx.activity.ComponentActivity -import com.android.systemui.notetask.NoteTaskController -import com.android.systemui.notetask.NoteTaskEntryPoint -import com.android.systemui.settings.UserTracker -import javax.inject.Inject - -/** - * An internal proxy activity that starts notes app in the work profile. - * - * If there is no work profile, this activity finishes gracefully. - * - * This activity MUST NOT be exported because that would expose the INTERACT_ACROSS_USER privilege - * to any apps. - */ -class LaunchNoteTaskManagedProfileProxyActivity -@Inject -constructor( - private val controller: NoteTaskController, - private val userTracker: UserTracker, - private val userManager: UserManager, -) : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val managedProfileUser = - userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) } - - if (managedProfileUser == null) { - logDebug { "Fail to find the work profile user." } - } else { - controller.showNoteTaskAsUser( - entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT, - user = managedProfileUser.userHandle - ) - } - finish() - } -} - -private inline fun logDebug(message: () -> String) { - if (Build.IS_DEBUGGABLE) { - Log.d(NoteTaskController.TAG, message()) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java index afc8bff91766..7de22b1a9c77 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java @@ -19,7 +19,7 @@ package com.android.systemui.screenshot.appclips; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.os.UserManager; +import android.os.UserHandle; import androidx.annotation.Nullable; @@ -39,15 +39,14 @@ class AppClipsCrossProcessHelper { private final DisplayTracker mDisplayTracker; @Inject - AppClipsCrossProcessHelper(@Application Context context, UserManager userManager, - DisplayTracker displayTracker) { + AppClipsCrossProcessHelper(@Application Context context, DisplayTracker displayTracker) { // Start a service as main user so that even if the app clips activity is running as work // profile user the service is able to use correct instance of Bubbles to grab a screenshot // excluding the bubble layer. mProxyConnector = new ServiceConnector.Impl<>(context, new Intent(context, AppClipsScreenshotHelperService.class), Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY - | Context.BIND_NOT_VISIBLE, userManager.getMainUser().getIdentifier(), + | Context.BIND_NOT_VISIBLE, UserHandle.USER_SYSTEM, IAppClipsScreenshotHelperService.Stub::asInterface); mDisplayTracker = displayTracker; } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java index 83ff020362f1..e0b9f9b7ad93 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java @@ -33,6 +33,11 @@ import javax.inject.Inject; /** * A helper service that runs in SysUI process and helps {@link AppClipsActivity} which runs in its * own separate process take a screenshot. + * + * <p>Note: This service always runs in the SysUI process running on the system user irrespective of + * which user started the service. This is required so that the correct instance of {@link Bubbles} + * instance is injected. This is set via attribute {@code android:singleUser=”true”} in + * AndroidManifest. */ public class AppClipsScreenshotHelperService extends Service { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java index 394949297d6d..dce8c81c3462 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java @@ -16,6 +16,11 @@ package com.android.systemui.screenshot.appclips; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; + import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; import android.app.Activity; @@ -25,17 +30,12 @@ import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.Intent.CaptureContentForNoteStatusCodes; import android.content.res.Resources; import android.os.IBinder; -import android.os.UserHandle; -import android.os.UserManager; -import android.util.Log; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.android.internal.infra.AndroidFuture; -import com.android.internal.infra.ServiceConnector; import com.android.internal.statusbar.IAppClipsService; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Application; @@ -43,73 +43,36 @@ import com.android.systemui.flags.FeatureFlags; import com.android.wm.shell.bubbles.Bubbles; import java.util.Optional; -import java.util.concurrent.ExecutionException; import javax.inject.Inject; /** * A service that communicates with {@link StatusBarManager} to support the - * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} API. + * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} API. Also used by + * {@link AppClipsTrampolineActivity} to query if an app should be allowed to user App Clips. + * + * <p>Note: This service always runs in the SysUI process running on the system user irrespective of + * which user started the service. This is required so that the correct instance of {@link Bubbles} + * instance is injected. This is set via attribute {@code android:singleUser=”true”} in + * AndroidManifest. */ public class AppClipsService extends Service { - private static final String TAG = AppClipsService.class.getSimpleName(); - @Application private final Context mContext; private final FeatureFlags mFeatureFlags; private final Optional<Bubbles> mOptionalBubbles; private final DevicePolicyManager mDevicePolicyManager; - private final UserManager mUserManager; - private final boolean mAreTaskAndTimeIndependentPrerequisitesMet; - @VisibleForTesting() - @Nullable ServiceConnector<IAppClipsService> mProxyConnectorToMainProfile; - @Inject public AppClipsService(@Application Context context, FeatureFlags featureFlags, - Optional<Bubbles> optionalBubbles, DevicePolicyManager devicePolicyManager, - UserManager userManager) { + Optional<Bubbles> optionalBubbles, DevicePolicyManager devicePolicyManager) { mContext = context; mFeatureFlags = featureFlags; mOptionalBubbles = optionalBubbles; mDevicePolicyManager = devicePolicyManager; - mUserManager = userManager; - - // The consumer of this service are apps that call through StatusBarManager API to query if - // it can use app clips API. Since these apps can be launched as work profile users, this - // service will start as work profile user. SysUI doesn't share injected instances for - // different users. This is why the bubbles instance injected will be incorrect. As the apps - // don't generally have permission to connect to a service running as different user, we - // start a proxy connection to communicate with the main user's version of this service. - if (mUserManager.isManagedProfile()) { - // No need to check for prerequisites in this case as those are incorrect for work - // profile user instance of the service and the main user version of the service will - // take care of this check. - mAreTaskAndTimeIndependentPrerequisitesMet = false; - - // Get the main user so that we can connect to the main user's version of the service. - UserHandle mainUser = mUserManager.getMainUser(); - if (mainUser == null) { - // If main user is not available there isn't much we can do, no apps can use app - // clips. - return; - } - - // Set up the connection to be used later during onBind callback. - mProxyConnectorToMainProfile = - new ServiceConnector.Impl<>( - context, - new Intent(context, AppClipsService.class), - Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY - | Context.BIND_NOT_VISIBLE, - mainUser.getIdentifier(), - IAppClipsService.Stub::asInterface); - return; - } mAreTaskAndTimeIndependentPrerequisitesMet = checkIndependentVariables(); - mProxyConnectorToMainProfile = null; } private boolean checkIndependentVariables() { @@ -144,40 +107,25 @@ public class AppClipsService extends Service { return new IAppClipsService.Stub() { @Override public boolean canLaunchCaptureContentActivityForNote(int taskId) { - // In case of managed profile, use the main user's instance of the service. Callers - // cannot directly connect to the main user's instance as they may not have the - // permission to interact across users. - if (mUserManager.isManagedProfile()) { - return canLaunchCaptureContentActivityForNoteFromMainUser(taskId); - } + return canLaunchCaptureContentActivityForNoteInternal(taskId) + == CAPTURE_CONTENT_FOR_NOTE_SUCCESS; + } + @Override + @CaptureContentForNoteStatusCodes + public int canLaunchCaptureContentActivityForNoteInternal(int taskId) { if (!mAreTaskAndTimeIndependentPrerequisitesMet) { - return false; + return CAPTURE_CONTENT_FOR_NOTE_FAILED; } if (!mOptionalBubbles.get().isAppBubbleTaskId(taskId)) { - return false; + return CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; } - return !mDevicePolicyManager.getScreenCaptureDisabled(null); + return mDevicePolicyManager.getScreenCaptureDisabled(null) + ? CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN + : CAPTURE_CONTENT_FOR_NOTE_SUCCESS; } }; } - - /** Returns whether the app clips API can be used by querying the service as the main user. */ - private boolean canLaunchCaptureContentActivityForNoteFromMainUser(int taskId) { - if (mProxyConnectorToMainProfile == null) { - return false; - } - - try { - AndroidFuture<Boolean> future = mProxyConnectorToMainProfile.postForResult( - service -> service.canLaunchCaptureContentActivityForNote(taskId)); - return future.get(); - } catch (ExecutionException | InterruptedException e) { - Log.d(TAG, "Exception from service\n" + e); - } - - return false; - } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java index f00803c6d64b..6e5cef47400f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java @@ -22,41 +22,41 @@ import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; -import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; import android.app.Activity; -import android.app.admin.DevicePolicyManager; import android.content.ActivityNotFoundException; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.Intent.CaptureContentForNoteStatusCodes; import android.content.pm.PackageManager; import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.UserInfo; -import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Parcel; import android.os.ResultReceiver; import android.os.UserHandle; -import android.os.UserManager; import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.internal.infra.AndroidFuture; +import com.android.internal.infra.ServiceConnector; import com.android.internal.logging.UiEventLogger; +import com.android.internal.statusbar.IAppClipsService; import com.android.systemui.R; +import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.notetask.NoteTaskController; import com.android.systemui.notetask.NoteTaskEntryPoint; -import com.android.systemui.settings.UserTracker; -import com.android.wm.shell.bubbles.Bubbles; -import java.util.Optional; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -82,39 +82,57 @@ public class AppClipsTrampolineActivity extends Activity { private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName(); static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI"; - @VisibleForTesting - static final String EXTRA_USE_WP_USER = TAG + "USE_WP_USER"; static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE"; static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER"; static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME"; private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0); - private final DevicePolicyManager mDevicePolicyManager; - private final FeatureFlags mFeatureFlags; - private final Optional<Bubbles> mOptionalBubbles; private final NoteTaskController mNoteTaskController; private final PackageManager mPackageManager; - private final UserTracker mUserTracker; private final UiEventLogger mUiEventLogger; - private final UserManager mUserManager; + private final BroadcastSender mBroadcastSender; + @Background + private final Executor mBgExecutor; + @Main + private final Executor mMainExecutor; private final ResultReceiver mResultReceiver; + private final ServiceConnector<IAppClipsService> mAppClipsServiceConnector; + + private UserHandle mUserHandle; private Intent mKillAppClipsBroadcastIntent; - private UserHandle mNotesAppUser; @Inject - public AppClipsTrampolineActivity(DevicePolicyManager devicePolicyManager, FeatureFlags flags, - Optional<Bubbles> optionalBubbles, NoteTaskController noteTaskController, - PackageManager packageManager, UserTracker userTracker, UiEventLogger uiEventLogger, - UserManager userManager, @Main Handler mainHandler) { - mDevicePolicyManager = devicePolicyManager; - mFeatureFlags = flags; - mOptionalBubbles = optionalBubbles; + public AppClipsTrampolineActivity(@Application Context context, + NoteTaskController noteTaskController, PackageManager packageManager, + UiEventLogger uiEventLogger, BroadcastSender broadcastSender, + @Background Executor bgExecutor, @Main Executor mainExecutor, + @Main Handler mainHandler) { + mNoteTaskController = noteTaskController; + mPackageManager = packageManager; + mUiEventLogger = uiEventLogger; + mBroadcastSender = broadcastSender; + mBgExecutor = bgExecutor; + mMainExecutor = mainExecutor; + + mResultReceiver = createResultReceiver(mainHandler); + mAppClipsServiceConnector = createServiceConnector(context); + } + + /** A constructor used only for testing to verify interactions with {@link ServiceConnector}. */ + @VisibleForTesting + AppClipsTrampolineActivity(ServiceConnector<IAppClipsService> appClipsServiceConnector, + NoteTaskController noteTaskController, PackageManager packageManager, + UiEventLogger uiEventLogger, BroadcastSender broadcastSender, + @Background Executor bgExecutor, @Main Executor mainExecutor, + @Main Handler mainHandler) { + mAppClipsServiceConnector = appClipsServiceConnector; mNoteTaskController = noteTaskController; mPackageManager = packageManager; - mUserTracker = userTracker; mUiEventLogger = uiEventLogger; - mUserManager = userManager; + mBroadcastSender = broadcastSender; + mBgExecutor = bgExecutor; + mMainExecutor = mainExecutor; mResultReceiver = createResultReceiver(mainHandler); } @@ -127,62 +145,62 @@ public class AppClipsTrampolineActivity extends Activity { return; } - if (mUserManager.isManagedProfile()) { - maybeStartActivityForWPUser(); - finish(); - return; - } + mUserHandle = getUser(); - if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) { - finish(); - return; - } + mBgExecutor.execute(() -> { + AndroidFuture<Integer> statusCodeFuture = mAppClipsServiceConnector.postForResult( + service -> service.canLaunchCaptureContentActivityForNoteInternal(getTaskId())); + statusCodeFuture.whenCompleteAsync(this::handleAppClipsStatusCode, mMainExecutor); + }); + } - if (mOptionalBubbles.isEmpty()) { - setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED); - return; + @Override + protected void onDestroy() { + if (isFinishing() && mKillAppClipsBroadcastIntent != null) { + mBroadcastSender.sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF); } - if (!mOptionalBubbles.get().isAppBubbleTaskId(getTaskId())) { - setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED); - return; - } + super.onDestroy(); + } - if (mDevicePolicyManager.getScreenCaptureDisabled(null)) { - setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN); + private void handleAppClipsStatusCode(@CaptureContentForNoteStatusCodes int statusCode, + Throwable error) { + if (isFinishing()) { + // It's too late, trampoline activity is finishing or already finished. Return early. return; } - ComponentName componentName; - try { - componentName = ComponentName.unflattenFromString( - getString(R.string.config_screenshotAppClipsActivityComponent)); - } catch (Resources.NotFoundException e) { - setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED); + if (error != null) { + Log.d(TAG, "Error querying app clips service", error); + setErrorResultAndFinish(statusCode); return; } - if (componentName == null || componentName.getPackageName().isEmpty() - || componentName.getClassName().isEmpty()) { - setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED); - return; - } + switch (statusCode) { + case CAPTURE_CONTENT_FOR_NOTE_SUCCESS: + launchAppClipsActivity(); + break; - mNotesAppUser = getUser(); - if (getIntent().getBooleanExtra(EXTRA_USE_WP_USER, /* defaultValue= */ false)) { - // Get the work profile user internally instead of passing around via intent extras as - // this activity is exported apps could potentially mess around with intent extras. - mNotesAppUser = getWorkProfileUser().orElse(mNotesAppUser); + case CAPTURE_CONTENT_FOR_NOTE_FAILED: + case CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED: + case CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN: + default: + setErrorResultAndFinish(statusCode); } + } + private void launchAppClipsActivity() { + ComponentName componentName = ComponentName.unflattenFromString( + getString(R.string.config_screenshotAppClipsActivityComponent)); String callingPackageName = getCallingPackage(); - Intent intent = new Intent().setComponent(componentName) + + Intent intent = new Intent() + .setComponent(componentName) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver) .putExtra(EXTRA_CALLING_PACKAGE_NAME, callingPackageName); try { - // Start the App Clips activity for the user corresponding to the notes app user. - startActivityAsUser(intent, mNotesAppUser); + startActivity(intent); // Set up the broadcast intent that will inform the above App Clips activity to finish // when this trampoline activity is finished. @@ -198,39 +216,6 @@ public class AppClipsTrampolineActivity extends Activity { } } - @Override - protected void onDestroy() { - super.onDestroy(); - - if (isFinishing() && mKillAppClipsBroadcastIntent != null) { - sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF); - } - } - - private Optional<UserHandle> getWorkProfileUser() { - return mUserTracker.getUserProfiles().stream() - .filter(profile -> mUserManager.isManagedProfile(profile.id)) - .findFirst() - .map(UserInfo::getUserHandle); - } - - private void maybeStartActivityForWPUser() { - UserHandle mainUser = mUserManager.getMainUser(); - if (mainUser == null) { - setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED); - return; - } - - // Start the activity as the main user with activity result forwarding. Set the intent extra - // so that the newly started trampoline activity starts the actual app clips activity as the - // work profile user. Starting the app clips activity as the work profile user is required - // to save the screenshot in work profile user storage and grant read permission to the URI. - startActivityAsUser( - new Intent(this, AppClipsTrampolineActivity.class) - .putExtra(EXTRA_USE_WP_USER, /* value= */ true) - .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT), mainUser); - } - private void setErrorResultAndFinish(int errorCode) { setResult(RESULT_OK, new Intent().putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode)); @@ -241,7 +226,7 @@ public class AppClipsTrampolineActivity extends Activity { int callingPackageUid = 0; try { callingPackageUid = mPackageManager.getApplicationInfoAsUser(callingPackageName, - APPLICATION_INFO_FLAGS, mNotesAppUser.getIdentifier()).uid; + APPLICATION_INFO_FLAGS, mUserHandle.getIdentifier()).uid; } catch (NameNotFoundException e) { Log.d(TAG, "Couldn't find notes app UID " + e); } @@ -281,7 +266,7 @@ public class AppClipsTrampolineActivity extends Activity { mKillAppClipsBroadcastIntent = null; // Expand the note bubble before returning the result. - mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mNotesAppUser); + mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mUserHandle); setResult(RESULT_OK, convertedData); finish(); } @@ -298,11 +283,18 @@ public class AppClipsTrampolineActivity extends Activity { appClipsResultReceiver.writeToParcel(parcel, 0); parcel.setDataPosition(0); - ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(parcel); + ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(parcel); parcel.recycle(); return resultReceiver; } + private ServiceConnector<IAppClipsService> createServiceConnector( + @Application Context context) { + return new ServiceConnector.Impl<>(context, new Intent(context, AppClipsService.class), + Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY | Context.BIND_NOT_VISIBLE, + UserHandle.USER_SYSTEM, IAppClipsService.Stub::asInterface); + } + /** This is a test only API for mocking response from {@link AppClipsActivity}. */ @VisibleForTesting public ResultReceiver getResultReceiverForTest() { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 2f7644eb5c82..f4485c9da452 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -36,6 +36,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.keyguard.AuthKeyguardMessageArea; +import com.android.keyguard.KeyguardMessageAreaController; import com.android.keyguard.LockIconViewController; import com.android.keyguard.dagger.KeyguardBouncerComponent; import com.android.systemui.R; @@ -45,12 +46,14 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.binder.KeyguardBouncerViewBinder; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel; import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; +import com.android.systemui.log.BouncerLogger; import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor; import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor; import com.android.systemui.multishade.ui.view.MultiShadeView; @@ -149,12 +152,15 @@ public class NotificationShadeWindowViewController { PulsingGestureListener pulsingGestureListener, KeyguardBouncerViewModel keyguardBouncerViewModel, KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory, + KeyguardMessageAreaController.Factory messageAreaControllerFactory, KeyguardTransitionInteractor keyguardTransitionInteractor, PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel, FeatureFlags featureFlags, Provider<MultiShadeInteractor> multiShadeInteractorProvider, SystemClock clock, - Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider) { + Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider, + BouncerMessageInteractor bouncerMessageInteractor, + BouncerLogger bouncerLogger) { mLockscreenShadeTransitionController = transitionController; mFalsingCollector = falsingCollector; mStatusBarStateController = statusBarStateController; @@ -183,7 +189,11 @@ public class NotificationShadeWindowViewController { mView.findViewById(R.id.keyguard_bouncer_container), keyguardBouncerViewModel, primaryBouncerToGoneTransitionViewModel, - keyguardBouncerComponentFactory); + keyguardBouncerComponentFactory, + messageAreaControllerFactory, + bouncerMessageInteractor, + bouncerLogger, + featureFlags); collectFlow(mView, keyguardTransitionInteractor.getLockscreenToDreamingTransition(), mLockscreenToDreamingTransition); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 12420ff5f481..f409f80f95d0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -94,6 +94,7 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardIndication; import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor; import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.log.LogLevel; import com.android.systemui.plugins.FalsingManager; @@ -147,6 +148,7 @@ public class KeyguardIndicationController { private final AuthController mAuthController; private final KeyguardLogger mKeyguardLogger; private final UserTracker mUserTracker; + private final BouncerMessageInteractor mBouncerMessageInteractor; private ViewGroup mIndicationArea; private KeyguardIndicationTextView mTopIndicationView; private KeyguardIndicationTextView mLockScreenIndicationView; @@ -253,7 +255,8 @@ public class KeyguardIndicationController { KeyguardLogger keyguardLogger, AlternateBouncerInteractor alternateBouncerInteractor, AlarmManager alarmManager, - UserTracker userTracker + UserTracker userTracker, + BouncerMessageInteractor bouncerMessageInteractor ) { mContext = context; mBroadcastDispatcher = broadcastDispatcher; @@ -278,7 +281,7 @@ public class KeyguardIndicationController { mScreenLifecycle.addObserver(mScreenObserver); mAlternateBouncerInteractor = alternateBouncerInteractor; mUserTracker = userTracker; - + mBouncerMessageInteractor = bouncerMessageInteractor; mFaceAcquiredMessageDeferral = faceHelpMessageDeferral; mCoExFaceAcquisitionMsgIdsToShow = new HashSet<>(); int[] msgIds = context.getResources().getIntArray( @@ -1151,6 +1154,11 @@ public class KeyguardIndicationController { msgId, helpString); } else if (mStatusBarKeyguardViewManager.isBouncerShowing()) { + if (biometricSourceType == FINGERPRINT && !fpAuthFailed) { + mBouncerMessageInteractor.setFingerprintAcquisitionMessage(helpString); + } else if (faceAuthSoftError) { + mBouncerMessageInteractor.setFaceAcquisitionMessage(helpString); + } mStatusBarKeyguardViewManager.setKeyguardMessage(helpString, mInitialTextColorState); } else if (mScreenLifecycle.getScreenState() == SCREEN_ON) { @@ -1206,6 +1214,8 @@ public class KeyguardIndicationController { if (biometricSourceType == FACE) { mFaceAcquiredMessageDeferral.reset(); } + mBouncerMessageInteractor.setFaceAcquisitionMessage(null); + mBouncerMessageInteractor.setFingerprintAcquisitionMessage(null); } @Override @@ -1226,6 +1236,8 @@ public class KeyguardIndicationController { } else if (biometricSourceType == FINGERPRINT) { onFingerprintAuthError(msgId, errString); } + mBouncerMessageInteractor.setFaceAcquisitionMessage(null); + mBouncerMessageInteractor.setFingerprintAcquisitionMessage(null); } private void onFaceAuthError(int msgId, String errString) { @@ -1310,6 +1322,8 @@ public class KeyguardIndicationController { showActionToUnlock(); } } + mBouncerMessageInteractor.setFaceAcquisitionMessage(null); + mBouncerMessageInteractor.setFingerprintAcquisitionMessage(null); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index b81cb2be1c4d..cdc7cee9de5d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -197,7 +197,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ private Set<Integer> mDebugTextUsedYPositions; private final boolean mDebugRemoveAnimation; - private final boolean mSimplifiedAppearFraction; private final boolean mSensitiveRevealAnimEndabled; private boolean mAnimatedInsets; @@ -621,7 +620,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable FeatureFlags featureFlags = Dependency.get(FeatureFlags.class); mDebugLines = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES); mDebugRemoveAnimation = featureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION); - mSimplifiedAppearFraction = featureFlags.isEnabled(Flags.SIMPLIFIED_APPEAR_FRACTION); mSensitiveRevealAnimEndabled = featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM); setAnimatedInsetsEnabled(featureFlags.isEnabled(Flags.ANIMATED_NOTIFICATION_SHADE_INSETS)); mSectionsManager = Dependency.get(NotificationSectionsManager.class); @@ -1638,14 +1636,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return mAmbientState.getTrackedHeadsUpRow() != null; } - // TODO(b/246353296): remove it when Flags.SIMPLIFIED_APPEAR_FRACTION is removed - public float calculateAppearFractionOld(float height) { - float appearEndPosition = getAppearEndPosition(); - float appearStartPosition = getAppearStartPosition(); - return (height - appearStartPosition) - / (appearEndPosition - appearStartPosition); - } - /** * @param height the height of the panel * @return Fraction of the appear animation that has been performed. Normally follows expansion @@ -1653,33 +1643,24 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable * when HUN is swiped up. */ @FloatRange(from = -1.0, to = 1.0) - public float simplifiedAppearFraction(float height) { + public float calculateAppearFraction(float height) { if (isHeadsUpTransition()) { // HUN is a special case because fraction can go negative if swiping up. And for now // it must go negative as other pieces responsible for proper translation up assume // negative value for HUN going up. // This can't use expansion fraction as that goes only from 0 to 1. Also when // appear fraction for HUN is 0, expansion fraction will be already around 0.2-0.3 - // and that makes translation jump immediately. Let's use old implementation for now and - // see if we can figure out something better - return MathUtils.constrain(calculateAppearFractionOld(height), -1, 1); + // and that makes translation jump immediately. + float appearEndPosition = getAppearEndPosition(); + float appearStartPosition = getAppearStartPosition(); + float hunAppearFraction = (height - appearStartPosition) + / (appearEndPosition - appearStartPosition); + return MathUtils.constrain(hunAppearFraction, -1, 1); } else { return mAmbientState.getExpansionFraction(); } } - public float calculateAppearFraction(float height) { - if (mSimplifiedAppearFraction) { - return simplifiedAppearFraction(height); - } else if (mShouldUseSplitNotificationShade) { - // for split shade we want to always use the new way of calculating appear fraction - // because without it heads-up experience is very broken and it's less risky change - return simplifiedAppearFraction(height); - } else { - return calculateAppearFractionOld(height); - } - } - public float getStackTranslation() { return mStackTranslation; } diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index 080be6d8cf25..12da17f6a301 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -192,12 +192,6 @@ android:excludeFromRecents="true" /> <activity - android:name="com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity" - android:exported="false" - android:permission="com.android.systemui.permission.SELF" - android:excludeFromRecents="true" /> - - <activity android:name="com.android.systemui.notetask.LaunchNotesRoleSettingsTrampolineActivity" android:exported="false" android:permission="com.android.systemui.permission.SELF" diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java index 254f9531ef83..8dc1e8fba600 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java @@ -16,6 +16,7 @@ package com.android.keyguard; +import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static com.android.keyguard.KeyguardClockSwitch.LARGE; @@ -189,6 +190,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); + assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -198,6 +200,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); + assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -212,6 +215,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { // only big clock is removed at switch assertThat(mLargeClockFrame.getParent()).isNull(); assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); + assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -223,6 +227,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { // only big clock is removed at switch assertThat(mLargeClockFrame.getParent()).isNull(); assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); + assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index d2e5a45c3a93..e1ba48809f1c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -67,6 +67,7 @@ import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.ActivityStarter; @@ -216,7 +217,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { mUserSwitcherController, mFeatureFlags, mGlobalSettings, mSessionTracker, Optional.of(mSideFpsController), mFalsingA11yDelegate, mTelephonyManager, mViewMediatorCallback, mAudioManager, - mock(KeyguardFaceAuthInteractor.class)); + mock(KeyguardFaceAuthInteractor.class), + mock(BouncerMessageInteractor.class)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java index 665246bd7f7d..62a176c94d67 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java @@ -73,9 +73,9 @@ public class MagnificationSettingsControllerTest extends SysuiTestCase { @Test public void testShowSettingsPanel() { - mMagnificationSettingsController.showMagnificationSettings(); + mMagnificationSettingsController.toggleSettingsPanelVisibility(); - verify(mWindowMagnificationSettings).showSettingPanel(); + verify(mWindowMagnificationSettings).toggleSettingsPanelVisibility(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java index 9c36af3a35e4..31c09b8e7ec4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java @@ -16,6 +16,7 @@ package com.android.systemui.accessibility; +import static android.content.pm.PackageManager.FEATURE_WINDOW_MAGNIFICATION; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.content.res.Configuration.ORIENTATION_UNDEFINED; @@ -96,6 +97,7 @@ import com.android.systemui.utils.os.FakeHandler; import com.google.common.util.concurrent.AtomicDouble; import org.junit.After; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -147,8 +149,15 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { private View.OnTouchListener mTouchListener; private MotionEventHelper mMotionEventHelper = new MotionEventHelper(); + /** + * return whether window magnification is supported for current test context. + */ + private boolean isWindowModeSupported() { + return getContext().getPackageManager().hasSystemFeature(FEATURE_WINDOW_MAGNIFICATION); + } + @Before - public void setUp() throws RemoteException { + public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mContext = Mockito.spy(getContext()); mHandler = new FakeHandler(TestableLooper.get(this).getLooper()); @@ -202,6 +211,9 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { return null; }).when(mSpyView).setOnTouchListener( any(View.OnTouchListener.class)); + + // skip test if window magnification is not supported to prevent fail results. (b/279820875) + Assume.assumeTrue(isWindowModeSupported()); } @After diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java index 38ecec0b4602..db580742a68f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java @@ -110,7 +110,7 @@ public class WindowMagnificationTest extends SysuiTestCase { mWindowMagnification.mMagnificationSettingsControllerCallback .onSettingsPanelVisibilityChanged(TEST_DISPLAY, /* shown= */ true); return null; - }).when(mMagnificationSettingsController).showMagnificationSettings(); + }).when(mMagnificationSettingsController).toggleSettingsPanelVisibility(); doAnswer(invocation -> { mWindowMagnification.mMagnificationSettingsControllerCallback .onSettingsPanelVisibilityChanged(TEST_DISPLAY, /* shown= */ false); @@ -198,7 +198,7 @@ public class WindowMagnificationTest extends SysuiTestCase { mWindowMagnification.mWindowMagnifierCallback.onClickSettingsButton(TEST_DISPLAY); waitForIdleSync(); - verify(mMagnificationSettingsController).showMagnificationSettings(); + verify(mMagnificationSettingsController).toggleSettingsPanelVisibility(); verify(mA11yLogger).log( eq(MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_PANEL_OPENED)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt index 9cf988e5b00a..8ee7d3e86265 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt @@ -30,6 +30,7 @@ import android.testing.TestableLooper import android.view.SurfaceControlViewHost import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils +import com.android.systemui.R import com.android.systemui.SystemUIAppComponentFactoryBase import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogLaunchAnimator @@ -67,6 +68,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -103,6 +105,7 @@ class CustomizationProviderTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + overrideResource(R.bool.custom_lockscreen_shortcuts_enabled, true) whenever(previewRenderer.surfacePackage).thenReturn(previewSurfacePackage) whenever(previewRendererFactory.create(any())).thenReturn(previewRenderer) whenever(backgroundHandler.looper).thenReturn(TestableLooper.get(this).looper) @@ -195,6 +198,7 @@ class CustomizationProviderTest : SysuiTestCase() { devicePolicyManager = devicePolicyManager, dockManager = dockManager, backgroundDispatcher = testDispatcher, + appContext = mContext, ) underTest.previewManager = KeyguardRemotePreviewManager( @@ -216,6 +220,13 @@ class CustomizationProviderTest : SysuiTestCase() { ) } + @After + fun tearDown() { + mContext + .getOrCreateTestableResources() + .removeOverride(R.bool.custom_lockscreen_shortcuts_enabled) + } + @Test fun onAttachInfo_reportsContext() { val callback: SystemUIAppComponentFactoryBase.ContextAvailableCallback = mock() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt index dfef94777039..5de24e4fc322 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt @@ -21,6 +21,7 @@ import android.content.Intent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.systemui.R import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -39,6 +40,7 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -65,6 +67,7 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, true) whenever(accessibilityManager.getRecommendedTimeoutMillis(anyInt(), anyInt())).thenAnswer { it.arguments[0] } @@ -76,6 +79,13 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() { runBlocking { createUnderTest() } } + @After + fun tearDown() { + mContext + .getOrCreateTestableResources() + .removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled) + } + @Test fun isEnabled() = testScope.runTest { @@ -108,6 +118,17 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() { } @Test + fun isEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() = + testScope.runTest { + overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, false) + createUnderTest() + val isEnabled by collectLastValue(underTest.isLongPressHandlingEnabled) + runCurrent() + + assertThat(isEnabled).isFalse() + } + + @Test fun longPressed_menuClicked_showsSettings() = testScope.runTest { val isMenuVisible by collectLastValue(underTest.isMenuVisible) @@ -267,6 +288,7 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() { ) { underTest = KeyguardLongPressInteractor( + appContext = mContext, scope = testScope.backgroundScope, transitionInteractor = KeyguardTransitionInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index a75e11a23deb..fb21847cd4d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -340,6 +340,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { devicePolicyManager = devicePolicyManager, dockManager = dockManager, backgroundDispatcher = testDispatcher, + appContext = mContext, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 3336e3b21180..5d2c3edd40af 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -200,6 +200,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { devicePolicyManager = devicePolicyManager, dockManager = dockManager, backgroundDispatcher = testDispatcher, + appContext = mContext, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 69d43af60321..8a36dbc86e81 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -211,6 +211,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { ) val keyguardLongPressInteractor = KeyguardLongPressInteractor( + appContext = mContext, scope = testScope.backgroundScope, transitionInteractor = KeyguardTransitionInteractor( @@ -240,6 +241,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { devicePolicyManager = devicePolicyManager, dockManager = dockManager, backgroundDispatcher = testDispatcher, + appContext = mContext, ), bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository), burnInHelperWrapper = burnInHelperWrapper, diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt new file mode 100644 index 000000000000..450aadd70171 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 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.notetask + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Icon +import android.os.UserHandle +import com.android.wm.shell.bubbles.Bubbles +import java.util.Optional +import kotlinx.coroutines.CoroutineDispatcher + +/** + * Fake for [NoteTaskBubblesController] as mocking suspending functions is not supported in the + * Android tree's version of mockito. Ideally the [NoteTaskBubblesController] should be implemented + * using an interface for effectively providing multiple implementations but as this fake primarily + * for dealing with old version of mockito there isn't any benefit in adding complexity. + */ +class FakeNoteTaskBubbleController( + unUsed1: Context, + unsUsed2: CoroutineDispatcher, + private val optionalBubbles: Optional<Bubbles> +) : NoteTaskBubblesController(unUsed1, unsUsed2) { + override suspend fun areBubblesAvailable() = optionalBubbles.isPresent + + override suspend fun showOrHideAppBubble(intent: Intent, userHandle: UserHandle, icon: Icon) { + optionalBubbles.ifPresentOrElse( + { bubbles -> bubbles.showOrHideAppBubble(intent, userHandle, icon) }, + { throw IllegalAccessException() } + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt new file mode 100644 index 000000000000..baac9e020280 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2023 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.notetask + +import android.content.Intent +import android.graphics.drawable.Icon +import android.os.UserHandle +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.notetask.NoteTaskBubblesController.NoteTaskBubblesService +import com.android.wm.shell.bubbles.Bubbles +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** atest SystemUITests:NoteTaskBubblesServiceTest */ +@SmallTest +@RunWith(AndroidJUnit4::class) +internal class NoteTaskBubblesServiceTest : SysuiTestCase() { + + @Mock private lateinit var bubbles: Bubbles + + private fun createServiceBinder(bubbles: Bubbles? = this.bubbles) = + NoteTaskBubblesService(Optional.ofNullable(bubbles)).onBind(Intent()) + as INoteTaskBubblesService + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun areBubblesAvailable_bubblesNotNull_shouldReturnTrue() { + assertThat(createServiceBinder().areBubblesAvailable()).isTrue() + } + + @Test + fun areBubblesAvailable_bubblesNull_shouldReturnFalse() { + assertThat(createServiceBinder(bubbles = null).areBubblesAvailable()).isFalse() + } + + @Test + fun showOrHideAppBubble() { + val intent = Intent() + val user = UserHandle.SYSTEM + val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) + + createServiceBinder().showOrHideAppBubble(intent, user, icon) + + verify(bubbles).showOrHideAppBubble(intent, user, icon) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index e99f8b6aa47b..079ef372cae6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -42,6 +42,7 @@ import android.content.pm.UserInfo import android.graphics.drawable.Icon import android.os.UserHandle import android.os.UserManager +import android.provider.Settings import androidx.test.ext.truth.content.IntentSubject.assertThat import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 @@ -56,22 +57,27 @@ import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity -import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity import com.android.systemui.settings.FakeUserTracker import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.settings.SecureSettings import com.android.wm.shell.bubbles.Bubble import com.android.wm.shell.bubbles.Bubbles import com.google.common.truth.Truth.assertThat import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.doNothing import org.mockito.Mockito.never import org.mockito.Mockito.spy @@ -80,6 +86,7 @@ import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations /** atest SystemUITests:NoteTaskControllerTest */ +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) internal class NoteTaskControllerTest : SysuiTestCase() { @@ -97,7 +104,10 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Mock private lateinit var shortcutManager: ShortcutManager @Mock private lateinit var activityManager: ActivityManager @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var secureSettings: SecureSettings private val userTracker = FakeUserTracker() + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) @Before fun setUp() { @@ -122,6 +132,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { whenever(activityManager.getRunningTasks(anyInt())).thenReturn(emptyList()) whenever(userManager.isManagedProfile(workUserInfo.id)).thenReturn(true) whenever(context.resources).thenReturn(getContext().resources) + whenever(secureSettings.userTracker).thenReturn(userTracker) } private fun createNoteTaskController( @@ -132,7 +143,6 @@ internal class NoteTaskControllerTest : SysuiTestCase() { context = context, resolver = resolver, eventLogger = eventLogger, - optionalBubbles = Optional.ofNullable(bubbles), userManager = userManager, keyguardManager = keyguardManager, isEnabled = isEnabled, @@ -141,6 +151,10 @@ internal class NoteTaskControllerTest : SysuiTestCase() { roleManager = roleManager, shortcutManager = shortcutManager, activityManager = activityManager, + secureSettings = secureSettings, + noteTaskBubblesController = + FakeNoteTaskBubbleController(context, testDispatcher, Optional.ofNullable(bubbles)), + applicationScope = testScope, ) // region onBubbleExpandChanged @@ -156,7 +170,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) verify(eventLogger).logNoteTaskOpened(expectedInfo) - verifyZeroInteractions(context, bubbles, keyguardManager, userManager) + verifyZeroInteractions(bubbles, keyguardManager, userManager) } @Test @@ -171,7 +185,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) verify(eventLogger).logNoteTaskClosed(expectedInfo) - verifyZeroInteractions(context, bubbles, keyguardManager, userManager) + verifyZeroInteractions(bubbles, keyguardManager, userManager) } @Test @@ -185,7 +199,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), ) - verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger) + verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger) } @Test @@ -199,7 +213,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), ) - verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger) + verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger) } @Test @@ -210,7 +224,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { key = "any other key", ) - verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger) + verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger) } @Test @@ -221,7 +235,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { key = Bubble.getAppBubbleKeyForApp(NOTE_TASK_INFO.packageName, NOTE_TASK_INFO.user), ) - verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger) + verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger) } // endregion @@ -251,6 +265,44 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test + fun showNoteTask_defaultUserSet_shouldStartActivityWithExpectedUserAndLogUiEvent() { + whenever(secureSettings.getInt(eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), any())) + .thenReturn(10) + val user10 = UserHandle.of(/* userId= */ 10) + + val expectedInfo = + NOTE_TASK_INFO.copy( + entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, + isKeyguardLocked = true, + user = user10, + ) + whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) + whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) + + createNoteTaskController() + .showNoteTask( + entryPoint = expectedInfo.entryPoint!!, + ) + + val intentCaptor = argumentCaptor<Intent>() + val userCaptor = argumentCaptor<UserHandle>() + verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) + intentCaptor.value.let { intent -> + assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) + assertThat(intent.`package`).isEqualTo(NOTE_TASK_PACKAGE_NAME) + assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) + assertThat(intent.flags and FLAG_ACTIVITY_MULTIPLE_TASK) + .isEqualTo(FLAG_ACTIVITY_MULTIPLE_TASK) + assertThat(intent.flags and FLAG_ACTIVITY_NEW_DOCUMENT) + .isEqualTo(FLAG_ACTIVITY_NEW_DOCUMENT) + assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() + } + assertThat(userCaptor.value).isEqualTo(user10) + verify(eventLogger).logNoteTaskOpened(expectedInfo) + verifyZeroInteractions(bubbles) + } + + @Test fun showNoteTaskWithUser_keyguardIsLocked_shouldStartActivityWithExpectedUserAndLogUiEvent() { val user10 = UserHandle.of(/* userId= */ 10) val expectedInfo = @@ -309,7 +361,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) // Context package name used to create bubble icon from drawable resource id - verify(context).packageName + verify(context, atLeastOnce()).packageName verifyNoteTaskOpenInBubbleInUser(userTracker.userHandle) verifyZeroInteractions(eventLogger) } @@ -318,7 +370,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { fun showNoteTask_bubblesIsNull_shouldDoNothing() { createNoteTaskController(bubbles = null).showNoteTask(entryPoint = TAIL_BUTTON) - verifyZeroInteractions(context, bubbles, eventLogger) + verifyZeroInteractions(bubbles, eventLogger) } @Test @@ -330,14 +382,14 @@ internal class NoteTaskControllerTest : SysuiTestCase() { noteTaskController.showNoteTask(entryPoint = TAIL_BUTTON) verify(noteTaskController).showNoDefaultNotesAppToast() - verifyZeroInteractions(context, bubbles, eventLogger) + verifyZeroInteractions(bubbles, eventLogger) } @Test fun showNoteTask_flagDisabled_shouldDoNothing() { createNoteTaskController(isEnabled = false).showNoteTask(entryPoint = TAIL_BUTTON) - verifyZeroInteractions(context, bubbles, eventLogger) + verifyZeroInteractions(bubbles, eventLogger) } @Test @@ -346,7 +398,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON) - verifyZeroInteractions(context, bubbles, eventLogger) + verifyZeroInteractions(bubbles, eventLogger) } @Test @@ -466,7 +518,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) - verifyZeroInteractions(context, bubbles, eventLogger) + verifyZeroInteractions(bubbles, eventLogger) } @Test @@ -482,7 +534,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) - verifyZeroInteractions(context, bubbles, eventLogger) + verifyZeroInteractions(bubbles, eventLogger) } @Test @@ -594,11 +646,11 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController(isEnabled = true).onRoleHoldersChanged("NOT_NOTES", user) - verifyZeroInteractions(context) + verify(context, never()).startActivityAsUser(any(), any()) } @Test - fun onRoleHoldersChanged_notesRole_sameUser_shouldUpdateShortcuts() { + fun onRoleHoldersChanged_notesRole_shouldUpdateShortcuts() { val user = userTracker.userHandle val controller = spy(createNoteTaskController()) doNothing().whenever(controller).updateNoteTaskAsUser(any()) @@ -607,22 +659,41 @@ internal class NoteTaskControllerTest : SysuiTestCase() { verify(controller).updateNoteTaskAsUser(user) } + // endregion + + // region updateNoteTaskAsUser + @Test + fun updateNoteTaskAsUser_sameUser_shouldUpdateShortcuts() { + val user = userTracker.userHandle + val controller = spy(createNoteTaskController()) + doNothing().whenever(controller).updateNoteTaskAsUserInternal(any()) + + controller.updateNoteTaskAsUser(user) + + verify(controller).updateNoteTaskAsUserInternal(user) + verify(context, never()).startServiceAsUser(any(), any()) + } @Test - fun onRoleHoldersChanged_notesRole_differentUser_shouldUpdateShortcutsInUserProcess() { + fun updateNoteTaskAsUser_differentUser_shouldUpdateShortcutsInUserProcess() { // FakeUserTracker will default to UserHandle.SYSTEM. val user = UserHandle.CURRENT + val controller = spy(createNoteTaskController(isEnabled = true)) + doNothing().whenever(controller).updateNoteTaskAsUserInternal(any()) - createNoteTaskController(isEnabled = true).onRoleHoldersChanged(ROLE_NOTES, user) + controller.updateNoteTaskAsUser(user) - verify(context).startServiceAsUser(any(), eq(user)) + verify(controller, never()).updateNoteTaskAsUserInternal(any()) + val intent = withArgCaptor { verify(context).startServiceAsUser(capture(), eq(user)) } + assertThat(intent).hasComponentClass(NoteTaskControllerUpdateService::class.java) } // endregion - // region updateNoteTaskAsUser + // region internalUpdateNoteTaskAsUser @Test - fun updateNoteTaskAsUser_withNotesRole_withShortcuts_shouldUpdateShortcuts() { - createNoteTaskController(isEnabled = true).updateNoteTaskAsUser(userTracker.userHandle) + fun updateNoteTaskAsUserInternal_withNotesRole_withShortcuts_shouldUpdateShortcuts() { + createNoteTaskController(isEnabled = true) + .updateNoteTaskAsUserInternal(userTracker.userHandle) val actualComponent = argumentCaptor<ComponentName>() verify(context.packageManager) @@ -651,11 +722,12 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - fun updateNoteTaskAsUser_noNotesRole_shouldDisableShortcuts() { + fun updateNoteTaskAsUserInternal_noNotesRole_shouldDisableShortcuts() { whenever(roleManager.getRoleHoldersAsUser(ROLE_NOTES, userTracker.userHandle)) .thenReturn(emptyList()) - createNoteTaskController(isEnabled = true).updateNoteTaskAsUser(userTracker.userHandle) + createNoteTaskController(isEnabled = true) + .updateNoteTaskAsUserInternal(userTracker.userHandle) val argument = argumentCaptor<ComponentName>() verify(context.packageManager) @@ -672,8 +744,9 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - fun updateNoteTaskAsUser_flagDisabled_shouldDisableShortcuts() { - createNoteTaskController(isEnabled = false).updateNoteTaskAsUser(userTracker.userHandle) + fun updateNoteTaskAsUserInternal_flagDisabled_shouldDisableShortcuts() { + createNoteTaskController(isEnabled = false) + .updateNoteTaskAsUserInternal(userTracker.userHandle) val argument = argumentCaptor<ComponentName>() verify(context.packageManager) @@ -690,18 +763,17 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } // endregion - // startregion startNoteTaskProxyActivityForUser + // startregion updateNoteTaskForAllUsers @Test - fun startNoteTaskProxyActivityForUser_shouldStartLaunchNoteTaskProxyActivityWithExpectedUser() { - val user0 = UserHandle.of(0) - createNoteTaskController().startNoteTaskProxyActivityForUser(user0) + fun updateNoteTaskForAllUsers_shouldRunUpdateForCurrentUserAndProfiles() { + userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) + val controller = spy(createNoteTaskController()) + doNothing().whenever(controller).updateNoteTaskAsUser(any()) - val intentCaptor = argumentCaptor<Intent>() - verify(context).startActivityAsUser(intentCaptor.capture(), eq(user0)) - assertThat(intentCaptor.value).run { - hasComponentClass(LaunchNoteTaskManagedProfileProxyActivity::class.java) - hasFlags(FLAG_ACTIVITY_NEW_TASK) - } + controller.updateNoteTaskForCurrentUserAndManagedProfiles() + + verify(controller).updateNoteTaskAsUser(mainUserInfo.userHandle) + verify(controller).updateNoteTaskAsUser(workUserInfo.userHandle) } // endregion diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt index 4e85b6c555ef..95bb3e0a4538 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt @@ -32,9 +32,12 @@ import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.time.FakeSystemClock import com.android.wm.shell.bubbles.Bubbles +import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Before @@ -71,19 +74,19 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { } private fun createUnderTest( - isEnabled: Boolean, - bubbles: Bubbles?, + isEnabled: Boolean, + bubbles: Bubbles?, ): NoteTaskInitializer = - NoteTaskInitializer( - controller = controller, - commandQueue = commandQueue, - optionalBubbles = Optional.ofNullable(bubbles), - isEnabled = isEnabled, - roleManager = roleManager, - userTracker = userTracker, - keyguardUpdateMonitor = keyguardMonitor, - backgroundExecutor = executor, - ) + NoteTaskInitializer( + controller = controller, + commandQueue = commandQueue, + optionalBubbles = Optional.ofNullable(bubbles), + isEnabled = isEnabled, + roleManager = roleManager, + userTracker = userTracker, + keyguardUpdateMonitor = keyguardMonitor, + backgroundExecutor = executor, + ) @Test fun initialize_withUserUnlocked() { @@ -93,8 +96,8 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { verify(commandQueue).addCallback(any()) verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any()) - verify(controller).setNoteTaskShortcutEnabled(any(), any()) - verify(keyguardMonitor, never()).registerCallback(any()) + verify(controller).updateNoteTaskForCurrentUserAndManagedProfiles() + verify(keyguardMonitor).registerCallback(any()) } @Test @@ -107,6 +110,7 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any()) verify(controller, never()).setNoteTaskShortcutEnabled(any(), any()) verify(keyguardMonitor).registerCallback(any()) + assertThat(userTracker.callbacks).isNotEmpty() } @Test @@ -116,12 +120,12 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { underTest.initialize() verifyZeroInteractions( - commandQueue, - bubbles, - controller, - roleManager, - userManager, - keyguardMonitor, + commandQueue, + bubbles, + controller, + roleManager, + userManager, + keyguardMonitor, ) } @@ -132,12 +136,12 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { underTest.initialize() verifyZeroInteractions( - commandQueue, - bubbles, - controller, - roleManager, - userManager, - keyguardMonitor, + commandQueue, + bubbles, + controller, + roleManager, + userManager, + keyguardMonitor, ) } @@ -146,7 +150,7 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { val expectedKeyEvent = KeyEvent(ACTION_DOWN, KEYCODE_STYLUS_BUTTON_TAIL) val underTest = createUnderTest(isEnabled = true, bubbles = bubbles) underTest.initialize() - val callback = captureArgument { verify(commandQueue).addCallback(capture()) } + val callback = withArgCaptor { verify(commandQueue).addCallback(capture()) } callback.handleSystemKey(expectedKeyEvent) @@ -154,31 +158,49 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { } @Test - fun initialize_userUnlocked() { + fun initialize_userUnlocked_shouldUpdateNoteTask() { whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(false) val underTest = createUnderTest(isEnabled = true, bubbles = bubbles) underTest.initialize() - val callback = captureArgument { verify(keyguardMonitor).registerCallback(capture()) } + val callback = withArgCaptor { verify(keyguardMonitor).registerCallback(capture()) } whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(true) callback.onUserUnlocked() - verify(controller).setNoteTaskShortcutEnabled(any(), any()) + + verify(controller).updateNoteTaskForCurrentUserAndManagedProfiles() } @Test - fun initialize_onRoleHoldersChanged() { + fun initialize_onRoleHoldersChanged_shouldRunOnRoleHoldersChanged() { val underTest = createUnderTest(isEnabled = true, bubbles = bubbles) underTest.initialize() - val callback = captureArgument { + val callback = withArgCaptor { verify(roleManager) - .addOnRoleHoldersChangedListenerAsUser(any(), capture(), eq(UserHandle.ALL)) + .addOnRoleHoldersChangedListenerAsUser(any(), capture(), eq(UserHandle.ALL)) } callback.onRoleHoldersChanged(ROLE_NOTES, userTracker.userHandle) verify(controller).onRoleHoldersChanged(ROLE_NOTES, userTracker.userHandle) } -} -private inline fun <reified T : Any> captureArgument(block: ArgumentCaptor<T>.() -> Unit) = - argumentCaptor<T>().apply(block).value + @Test + fun initialize_onProfilesChanged_shouldUpdateNoteTask() { + val underTest = createUnderTest(isEnabled = true, bubbles = bubbles) + underTest.initialize() + + userTracker.callbacks.first().onProfilesChanged(emptyList()) + + verify(controller, times(2)).updateNoteTaskForCurrentUserAndManagedProfiles() + } + + @Test + fun initialize_onUserChanged_shouldUpdateNoteTask() { + val underTest = createUnderTest(isEnabled = true, bubbles = bubbles) + underTest.initialize() + + userTracker.callbacks.first().onUserChanged(0, mock()) + + verify(controller, times(2)).updateNoteTaskForCurrentUserAndManagedProfiles() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt index a0c376ff1a1c..627c4a80e1ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt @@ -17,9 +17,6 @@ package com.android.systemui.notetask.shortcut import android.content.Intent -import android.content.pm.UserInfo -import android.os.UserHandle -import android.os.UserManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest @@ -29,17 +26,14 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.verify import com.android.systemui.SysuiTestCase import com.android.systemui.notetask.NoteTaskController import com.android.systemui.notetask.NoteTaskEntryPoint -import com.android.systemui.settings.FakeUserTracker import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.whenever import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.never import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -48,8 +42,6 @@ import org.mockito.MockitoAnnotations class LaunchNoteTaskActivityTest : SysuiTestCase() { @Mock lateinit var noteTaskController: NoteTaskController - @Mock lateinit var userManager: UserManager - private val userTracker: FakeUserTracker = FakeUserTracker() @Rule @JvmField @@ -60,8 +52,6 @@ class LaunchNoteTaskActivityTest : SysuiTestCase() { override fun create(intent: Intent?) = LaunchNoteTaskActivity( controller = noteTaskController, - userManager = userManager, - userTracker = userTracker ) }, /* initialTouchMode= */ false, @@ -71,7 +61,6 @@ class LaunchNoteTaskActivityTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - whenever(userManager.isManagedProfile(eq(workProfileUser.id))).thenReturn(true) } @After @@ -83,36 +72,7 @@ class LaunchNoteTaskActivityTest : SysuiTestCase() { fun startActivityOnNonWorkProfileUser_shouldLaunchNoteTask() { activityRule.launchActivity(/* startIntent= */ null) - verify(noteTaskController).showNoteTask(eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT)) - } - - @Test - fun startActivityOnWorkProfileUser_shouldLaunchProxyActivity() { - val mainUserHandle: UserHandle = mainUser.userHandle - userTracker.set(listOf(mainUser, workProfileUser), selectedUserIndex = 1) - whenever(userManager.isManagedProfile).thenReturn(true) - whenever(userManager.mainUser).thenReturn(mainUserHandle) - - activityRule.launchActivity(/* startIntent= */ null) - - verify(noteTaskController).startNoteTaskProxyActivityForUser(eq(mainUserHandle)) - } - - @Test - fun startActivityOnWorkProfileUser_noMainUser_shouldNotLaunch() { - userTracker.set(listOf(mainUser, workProfileUser), selectedUserIndex = 1) - whenever(userManager.isManagedProfile).thenReturn(true) - whenever(userManager.mainUser).thenReturn(null) - - activityRule.launchActivity(/* startIntent= */ null) - - verify(noteTaskController, never()).showNoteTask(any()) - verify(noteTaskController, never()).startNoteTaskProxyActivityForUser(any()) - } - - private companion object { - val mainUser = UserInfo(/* id= */ 0, /* name= */ "primary", /* flags= */ UserInfo.FLAG_MAIN) - val workProfileUser = - UserInfo(/* id= */ 10, /* name= */ "work", /* flags= */ UserInfo.FLAG_PROFILE) + verify(noteTaskController) + .showNoteTaskAsUser(eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT), any()) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt deleted file mode 100644 index 6347c3404348..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2023 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.notetask.shortcut - -import android.content.Intent -import android.content.pm.UserInfo -import android.os.UserHandle -import android.os.UserManager -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import androidx.test.filters.SmallTest -import androidx.test.rule.ActivityTestRule -import androidx.test.runner.intercepting.SingleActivityFactory -import com.android.dx.mockito.inline.extended.ExtendedMockito.never -import com.android.dx.mockito.inline.extended.ExtendedMockito.verify -import com.android.systemui.SysuiTestCase -import com.android.systemui.notetask.NoteTaskController -import com.android.systemui.notetask.NoteTaskEntryPoint -import com.android.systemui.settings.FakeUserTracker -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.whenever -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations - -@RunWith(AndroidTestingRunner::class) -@SmallTest -@TestableLooper.RunWithLooper -class LaunchNoteTaskManagedProfileProxyActivityTest : SysuiTestCase() { - - @Mock lateinit var noteTaskController: NoteTaskController - @Mock lateinit var userManager: UserManager - private val userTracker = FakeUserTracker() - - @Rule - @JvmField - val activityRule = - ActivityTestRule<LaunchNoteTaskManagedProfileProxyActivity>( - /* activityFactory= */ object : - SingleActivityFactory<LaunchNoteTaskManagedProfileProxyActivity>( - LaunchNoteTaskManagedProfileProxyActivity::class.java - ) { - override fun create(intent: Intent?) = - LaunchNoteTaskManagedProfileProxyActivity( - controller = noteTaskController, - userManager = userManager, - userTracker = userTracker - ) - }, - /* initialTouchMode= */ false, - /* launchActivity= */ false, - ) - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(userManager.isManagedProfile(eq(workProfileUser.id))).thenReturn(true) - } - - @After - fun tearDown() { - activityRule.finishActivity() - } - - @Test - fun startActivity_noWorkProfileUser_shouldNotLaunchNoteTask() { - userTracker.set(listOf(mainUser), selectedUserIndex = 0) - activityRule.launchActivity(/* startIntent= */ null) - - verify(noteTaskController, never()).showNoteTaskAsUser(any(), any()) - } - - @Test - fun startActivity_hasWorkProfileUser_shouldLaunchNoteTaskOnTheWorkProfileUser() { - userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUser)) - activityRule.launchActivity(/* startIntent= */ null) - - val workProfileUserHandle: UserHandle = workProfileUser.userHandle - verify(noteTaskController) - .showNoteTaskAsUser( - eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT), - eq(workProfileUserHandle) - ) - } - - private companion object { - val mainUser = UserInfo(/* id= */ 0, /* name= */ "primary", /* flags= */ UserInfo.FLAG_MAIN) - val workProfileUser = - UserInfo(/* id= */ 10, /* name= */ "work", /* flags= */ UserInfo.FLAG_PROFILE) - val mainAndWorkProfileUsers = listOf(mainUser, workProfileUser) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java index 67b1099c1e0a..d8897e9048c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java @@ -16,14 +16,17 @@ package com.android.systemui.screenshot.appclips; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; + import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; 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.eq; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.admin.DevicePolicyManager; @@ -31,8 +34,6 @@ import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; import androidx.test.runner.AndroidJUnit4; @@ -46,7 +47,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Optional; @@ -63,7 +63,6 @@ public final class AppClipsServiceTest extends SysuiTestCase { @Mock private Optional<Bubbles> mOptionalBubbles; @Mock private Bubbles mBubbles; @Mock private DevicePolicyManager mDevicePolicyManager; - @Mock private UserManager mUserManager; private AppClipsService mAppClipsService; @@ -81,51 +80,84 @@ public final class AppClipsServiceTest extends SysuiTestCase { } @Test + public void flagOff_internal_shouldReturnFailed() throws RemoteException { + when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(false); + + assertThat(getInterfaceWithRealContext() + .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID)) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); + } + + @Test public void emptyBubbles_shouldReturnFalse() throws RemoteException { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(true); + mockForEmptyBubbles(); assertThat(getInterfaceWithRealContext() .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse(); } @Test + public void emptyBubbles_internal_shouldReturnFailed() throws RemoteException { + mockForEmptyBubbles(); + + assertThat(getInterfaceWithRealContext() + .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID)) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); + } + + @Test public void taskIdNotAppBubble_shouldReturnFalse() throws RemoteException { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(false); - when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false); + mockForTaskIdNotAppBubble(); assertThat(getInterfaceWithRealContext() .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse(); } @Test + public void taskIdNotAppBubble_internal_shouldReturnWindowUnsupported() throws RemoteException { + mockForTaskIdNotAppBubble(); + + assertThat(getInterfaceWithRealContext() + .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID)) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED); + } + + @Test public void dpmScreenshotBlocked_shouldReturnFalse() throws RemoteException { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(false); - when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); - when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true); + mockForScreenshotBlocked(); assertThat(getInterfaceWithRealContext() .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse(); } @Test + public void dpmScreenshotBlocked_internal_shouldReturnBlockedByAdmin() throws RemoteException { + mockForScreenshotBlocked(); + + assertThat(getInterfaceWithRealContext() + .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID)) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN); + } + + @Test public void configComponentNameNotValid_shouldReturnFalse() throws RemoteException { - when(mMockContext.getString(anyInt())).thenReturn(EMPTY); - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(false); - when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); - when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false); + mockForInvalidConfigComponentName(); assertThat(getInterfaceWithMockContext() .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse(); } @Test + public void configComponentNameNotValid_internal_shouldReturnFailed() throws RemoteException { + mockForInvalidConfigComponentName(); + + assertThat(getInterfaceWithMockContext() + .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID)) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); + } + + + @Test public void allPrerequisitesSatisfy_shouldReturnTrue() throws RemoteException { mockToSatisfyAllPrerequisites(); @@ -134,28 +166,44 @@ public final class AppClipsServiceTest extends SysuiTestCase { } @Test - public void isManagedProfile_shouldUseProxyConnection() throws RemoteException { - when(mUserManager.isManagedProfile()).thenReturn(true); - when(mUserManager.getMainUser()).thenReturn(UserHandle.SYSTEM); - IAppClipsService service = getInterfaceWithRealContext(); - mAppClipsService.mProxyConnectorToMainProfile = - Mockito.spy(mAppClipsService.mProxyConnectorToMainProfile); + public void allPrerequisitesSatisfy_internal_shouldReturnSuccess() throws RemoteException { + mockToSatisfyAllPrerequisites(); - service.canLaunchCaptureContentActivityForNote(FAKE_TASK_ID); + assertThat(getInterfaceWithRealContext() + .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID)) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS); + } - verify(mAppClipsService.mProxyConnectorToMainProfile).postForResult(any()); + private void mockForEmptyBubbles() { + when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); + when(mOptionalBubbles.isEmpty()).thenReturn(true); } - @Test - public void isManagedProfile_noMainUser_shouldReturnFalse() { - when(mUserManager.isManagedProfile()).thenReturn(true); - when(mUserManager.getMainUser()).thenReturn(null); + private void mockForTaskIdNotAppBubble() { + when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); + when(mOptionalBubbles.isEmpty()).thenReturn(false); + when(mOptionalBubbles.get()).thenReturn(mBubbles); + when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false); + } - getInterfaceWithRealContext(); + private void mockForScreenshotBlocked() { + when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); + when(mOptionalBubbles.isEmpty()).thenReturn(false); + when(mOptionalBubbles.get()).thenReturn(mBubbles); + when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); + when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true); + } - assertThat(mAppClipsService.mProxyConnectorToMainProfile).isNull(); + private void mockForInvalidConfigComponentName() { + when(mMockContext.getString(anyInt())).thenReturn(EMPTY); + when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); + when(mOptionalBubbles.isEmpty()).thenReturn(false); + when(mOptionalBubbles.get()).thenReturn(mBubbles); + when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); + when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false); } + private void mockToSatisfyAllPrerequisites() { when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); when(mOptionalBubbles.isEmpty()).thenReturn(false); @@ -166,13 +214,13 @@ public final class AppClipsServiceTest extends SysuiTestCase { private IAppClipsService getInterfaceWithRealContext() { mAppClipsService = new AppClipsService(getContext(), mFeatureFlags, - mOptionalBubbles, mDevicePolicyManager, mUserManager); + mOptionalBubbles, mDevicePolicyManager); return getInterfaceFromService(mAppClipsService); } private IAppClipsService getInterfaceWithMockContext() { mAppClipsService = new AppClipsService(mMockContext, mFeatureFlags, - mOptionalBubbles, mDevicePolicyManager, mUserManager); + mOptionalBubbles, mDevicePolicyManager); return getInterfaceFromService(mAppClipsService); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java index e9007ff84f13..7fad972d83fc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java @@ -24,22 +24,19 @@ import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; -import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; +import static com.android.internal.infra.AndroidFuture.completedFuture; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI; -import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_USE_WP_USER; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Activity; -import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -52,20 +49,22 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.UserHandle; -import android.os.UserManager; import android.testing.AndroidTestingRunner; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.intercepting.SingleActivityFactory; +import com.android.internal.infra.ServiceConnector; import com.android.internal.logging.UiEventLogger; +import com.android.internal.statusbar.IAppClipsService; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.notetask.NoteTaskController; -import com.android.systemui.settings.UserTracker; -import com.android.wm.shell.bubbles.Bubbles; + +import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; import org.junit.Before; @@ -75,8 +74,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.List; -import java.util.Optional; +import java.util.concurrent.Executor; @RunWith(AndroidTestingRunner.class) public final class AppClipsTrampolineActivityTest extends SysuiTestCase { @@ -86,25 +84,19 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { private static final int TEST_UID = 42; private static final String TEST_CALLING_PACKAGE = "test-calling-package"; - @Mock - private DevicePolicyManager mDevicePolicyManager; - @Mock - private FeatureFlags mFeatureFlags; - @Mock - private Optional<Bubbles> mOptionalBubbles; - @Mock - private Bubbles mBubbles; + @Mock private ServiceConnector<IAppClipsService> mServiceConnector; @Mock private NoteTaskController mNoteTaskController; @Mock private PackageManager mPackageManager; @Mock - private UserTracker mUserTracker; - @Mock private UiEventLogger mUiEventLogger; @Mock - private UserManager mUserManager; - + private BroadcastSender mBroadcastSender; + @Background + private Executor mBgExecutor; + @Main + private Executor mMainExecutor; @Main private Handler mMainHandler; @@ -114,9 +106,9 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { new SingleActivityFactory<>(AppClipsTrampolineActivityTestable.class) { @Override protected AppClipsTrampolineActivityTestable create(Intent unUsed) { - return new AppClipsTrampolineActivityTestable(mDevicePolicyManager, - mFeatureFlags, mOptionalBubbles, mNoteTaskController, mPackageManager, - mUserTracker, mUiEventLogger, mUserManager, mMainHandler); + return new AppClipsTrampolineActivityTestable(mServiceConnector, + mNoteTaskController, mPackageManager, mUiEventLogger, mBroadcastSender, + mBgExecutor, mMainExecutor, mMainHandler); } }; @@ -133,6 +125,8 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)); MockitoAnnotations.initMocks(this); + mBgExecutor = MoreExecutors.directExecutor(); + mMainExecutor = MoreExecutors.directExecutor(); mMainHandler = mContext.getMainThreadHandler(); mActivityIntent = new Intent(mContext, AppClipsTrampolineActivityTestable.class); @@ -169,19 +163,9 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { } @Test - public void flagOff_shouldFinishWithResultCancel() { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(false); - - mActivityRule.launchActivity(mActivityIntent); - - assertThat(mActivityRule.getActivityResult().getResultCode()) - .isEqualTo(Activity.RESULT_CANCELED); - } - - @Test - public void bubblesEmpty_shouldFinishWithFailed() { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(true); + public void queryService_returnedFailed_shouldFinishWithFailed() { + when(mServiceConnector.postForResult(any())) + .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_FAILED)); mActivityRule.launchActivity(mActivityIntent); @@ -189,14 +173,13 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); assertThat(getStatusCodeExtra(actualResult.getResultData())) .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); + assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); } @Test - public void taskIdNotAppBubble_shouldFinishWithWindowModeUnsupported() { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(false); - when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(false); + public void queryService_returnedWindowModeUnsupported_shouldFinishWithWindowModeUnsupported() { + when(mServiceConnector.postForResult(any())) + .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED)); mActivityRule.launchActivity(mActivityIntent); @@ -204,15 +187,13 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); assertThat(getStatusCodeExtra(actualResult.getResultData())) .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED); + assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); } @Test - public void dpmScreenshotBlocked_shouldFinishWithBlockedByAdmin() { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(false); - when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true); - when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true); + public void queryService_returnedScreenshotBlocked_shouldFinishWithBlockedByAdmin() { + when(mServiceConnector.postForResult(any())) + .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN)); mActivityRule.launchActivity(mActivityIntent); @@ -220,6 +201,7 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); assertThat(getStatusCodeExtra(actualResult.getResultData())) .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN); + assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); } @Test @@ -240,6 +222,7 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); assertThat(getStatusCodeExtra(actualResult.getResultData())) .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED); + assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); } @Test @@ -261,6 +244,7 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(getStatusCodeExtra(actualResult.getResultData())) .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS); assertThat(actualResult.getResultData().getData()).isEqualTo(TEST_URI); + assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); } @Test @@ -274,48 +258,9 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { verify(mUiEventLogger).log(SCREENSHOT_FOR_NOTE_TRIGGERED, TEST_UID, TEST_CALLING_PACKAGE); } - @Test - public void startAppClipsActivity_throughWPUser_shouldStartMainUserActivity() - throws NameNotFoundException { - when(mUserManager.isManagedProfile()).thenReturn(true); - when(mUserManager.getMainUser()).thenReturn(UserHandle.SYSTEM); - mockToSatisfyAllPrerequisites(); - - AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent); - waitForIdleSync(); - - Intent actualIntent = activity.mStartedIntent; - assertThat(actualIntent.getComponent()).isEqualTo( - new ComponentName(mContext, AppClipsTrampolineActivity.class)); - assertThat(actualIntent.getFlags()).isEqualTo(Intent.FLAG_ACTIVITY_FORWARD_RESULT); - assertThat(actualIntent.getBooleanExtra(EXTRA_USE_WP_USER, false)).isTrue(); - assertThat(activity.mStartingUser).isEqualTo(UserHandle.SYSTEM); - } - - @Test - public void startAppClipsActivity_throughWPUser_noMainUser_shouldFinishWithFailed() - throws NameNotFoundException { - when(mUserManager.isManagedProfile()).thenReturn(true); - when(mUserManager.getMainUser()).thenReturn(null); - - mockToSatisfyAllPrerequisites(); - - mActivityRule.launchActivity(mActivityIntent); - waitForIdleSync(); - - ActivityResult actualResult = mActivityRule.getActivityResult(); - assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); - assertThat(getStatusCodeExtra(actualResult.getResultData())) - .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); - } - private void mockToSatisfyAllPrerequisites() throws NameNotFoundException { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(false); - when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true); - when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false); - when(mUserTracker.getUserProfiles()).thenReturn(List.of()); + when(mServiceConnector.postForResult(any())) + .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_SUCCESS)); ApplicationInfo testApplicationInfo = new ApplicationInfo(); testApplicationInfo.uid = TEST_UID; @@ -330,17 +275,14 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { Intent mStartedIntent; UserHandle mStartingUser; - public AppClipsTrampolineActivityTestable(DevicePolicyManager devicePolicyManager, - FeatureFlags flags, - Optional<Bubbles> optionalBubbles, - NoteTaskController noteTaskController, - PackageManager packageManager, - UserTracker userTracker, - UiEventLogger uiEventLogger, - UserManager userManager, + public AppClipsTrampolineActivityTestable( + ServiceConnector<IAppClipsService> serviceServiceConnector, + NoteTaskController noteTaskController, PackageManager packageManager, + UiEventLogger uiEventLogger, BroadcastSender broadcastSender, + @Background Executor bgExecutor, @Main Executor mainExecutor, @Main Handler mainHandler) { - super(devicePolicyManager, flags, optionalBubbles, noteTaskController, packageManager, - userTracker, uiEventLogger, userManager, mainHandler); + super(serviceServiceConnector, noteTaskController, packageManager, uiEventLogger, + broadcastSender, bgExecutor, mainExecutor, mainHandler); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 16277de850d6..0393bef165db 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -21,6 +21,7 @@ import android.testing.TestableLooper.RunWithLooper import android.view.MotionEvent import android.view.ViewGroup import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardMessageAreaController import com.android.keyguard.KeyguardSecurityContainerController import com.android.keyguard.LockIconViewController import com.android.keyguard.dagger.KeyguardBouncerComponent @@ -29,14 +30,20 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollectorFake import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.dock.DockManager +import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory +import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor +import com.android.systemui.keyguard.bouncer.domain.interactor.CountDownTimerUtil +import com.android.systemui.keyguard.data.repository.FakeBouncerMessageRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel +import com.android.systemui.log.BouncerLogger import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy import com.android.systemui.multishade.data.repository.MultiShadeRepository import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor @@ -54,6 +61,7 @@ import com.android.systemui.statusbar.phone.PhoneStatusBarViewController import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.window.StatusBarWindowStateController import com.android.systemui.unfold.UnfoldTransitionProgressProvider +import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -104,7 +112,8 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent @Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController @Mock - private lateinit var unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider> + private lateinit var unfoldTransitionProgressProvider: + Optional<UnfoldTransitionProgressProvider> @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel @@ -134,6 +143,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false) featureFlags.set(Flags.DUAL_SHADE, false) featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true) + featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) val inputProxy = MultiShadeInputProxy() testScope = TestScope() @@ -170,6 +180,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { pulsingGestureListener, keyguardBouncerViewModel, keyguardBouncerComponentFactory, + mock(KeyguardMessageAreaController.Factory::class.java), keyguardTransitionInteractor, primaryBouncerToGoneTransitionViewModel, featureFlags, @@ -189,6 +200,10 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { shadeController = shadeController, ) }, + BouncerMessageInteractor(FakeBouncerMessageRepository(), + mock(BouncerMessageFactory::class.java), + FakeUserRepository(), CountDownTimerUtil(), featureFlags), + BouncerLogger(logcatLogBuffer("BouncerLog")) ) underTest.setupExpandedStatusBar() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 16af208fd531..5ae8ee14678a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -21,6 +21,7 @@ import android.testing.TestableLooper.RunWithLooper import android.view.MotionEvent import android.widget.FrameLayout import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardMessageAreaController import com.android.keyguard.KeyguardSecurityContainerController import com.android.keyguard.LockIconViewController import com.android.keyguard.dagger.KeyguardBouncerComponent @@ -29,13 +30,19 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollectorFake import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.dock.DockManager +import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory +import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor +import com.android.systemui.keyguard.bouncer.domain.interactor.CountDownTimerUtil +import com.android.systemui.keyguard.data.repository.FakeBouncerMessageRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel +import com.android.systemui.log.BouncerLogger import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy import com.android.systemui.multishade.data.repository.MultiShadeRepository import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor @@ -54,11 +61,13 @@ import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.window.StatusBarWindowStateController import com.android.systemui.unfold.UnfoldTransitionProgressProvider +import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.TestScope @@ -69,10 +78,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import java.util.Optional @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidTestingRunner::class) @@ -106,7 +115,8 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { @Mock private lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController @Mock - private lateinit var unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider> + private lateinit var unfoldTransitionProgressProvider: + Optional<UnfoldTransitionProgressProvider> @Mock private lateinit var notificationInsetsController: NotificationInsetsController @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock @@ -146,6 +156,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false) featureFlags.set(Flags.DUAL_SHADE, false) featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true) + featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) val inputProxy = MultiShadeInputProxy() testScope = TestScope() val multiShadeInteractor = @@ -181,6 +192,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { pulsingGestureListener, keyguardBouncerViewModel, keyguardBouncerComponentFactory, + Mockito.mock(KeyguardMessageAreaController.Factory::class.java), keyguardTransitionInteractor, primaryBouncerToGoneTransitionViewModel, featureFlags, @@ -200,6 +212,14 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { shadeController = shadeController, ) }, + BouncerMessageInteractor( + FakeBouncerMessageRepository(), + Mockito.mock(BouncerMessageFactory::class.java), + FakeUserRepository(), + CountDownTimerUtil(), + featureFlags + ), + BouncerLogger(logcatLogBuffer("BouncerLog")) ) controller.setupExpandedStatusBar() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java index 542e0cb728a3..c810f0cbf328 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java @@ -102,6 +102,7 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardIndication; import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor; import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -297,7 +298,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { mFaceHelpMessageDeferral, mock(KeyguardLogger.class), mAlternateBouncerInteractor, mAlarmManager, - mUserTracker + mUserTracker, + mock(BouncerMessageInteractor.class) ); mController.init(); mController.setIndicationArea(mIndicationArea); diff --git a/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java new file mode 100644 index 000000000000..5221468e125a --- /dev/null +++ b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 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.server.contentprotection; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.PackageManager.PackageInfoFlags; +import android.util.Slog; + +import java.util.Arrays; + +/** + * Basic package manager for content protection using content capture. + * + * @hide + */ +final class ContentProtectionPackageManager { + private static final String TAG = ContentProtectionPackageManager.class.getSimpleName(); + + private static final PackageInfoFlags PACKAGE_INFO_FLAGS = + PackageInfoFlags.of(PackageManager.GET_PERMISSIONS); + + @NonNull private final PackageManager mPackageManager; + + ContentProtectionPackageManager(@NonNull Context context) { + mPackageManager = context.getPackageManager(); + } + + @Nullable + public PackageInfo getPackageInfo(@NonNull String packageName) { + try { + return mPackageManager.getPackageInfo(packageName, PACKAGE_INFO_FLAGS); + } catch (NameNotFoundException ex) { + Slog.w(TAG, "Package info not found: ", ex); + return null; + } + } + + public boolean isSystemApp(@NonNull PackageInfo packageInfo) { + return packageInfo.applicationInfo != null && isSystemApp(packageInfo.applicationInfo); + } + + private boolean isSystemApp(@NonNull ApplicationInfo applicationInfo) { + return (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + } + + public boolean isUpdatedSystemApp(@NonNull PackageInfo packageInfo) { + return packageInfo.applicationInfo != null + && isUpdatedSystemApp(packageInfo.applicationInfo); + } + + private boolean isUpdatedSystemApp(@NonNull ApplicationInfo applicationInfo) { + return (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; + } + + public boolean hasRequestedInternetPermissions(@NonNull PackageInfo packageInfo) { + return packageInfo.requestedPermissions != null + && Arrays.asList(packageInfo.requestedPermissions) + .contains(Manifest.permission.INTERNET); + } +} diff --git a/services/core/java/com/android/server/input/InputFeatureFlagProvider.java b/services/core/java/com/android/server/input/InputFeatureFlagProvider.java new file mode 100644 index 000000000000..3854adad9cd8 --- /dev/null +++ b/services/core/java/com/android/server/input/InputFeatureFlagProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright 2023 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.server.input; + +import android.sysprop.InputProperties; + +import java.util.Optional; + +/** + * A component of {@link InputManagerService} responsible for managing the input sysprop flags + * + * @hide + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public final class InputFeatureFlagProvider { + + // To disable Keyboard backlight control via Framework, run: + // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart) + private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = + InputProperties.enable_keyboard_backlight_control().orElse(true); + + // To disable Framework controlled keyboard backlight animation run: + // adb shell setprop persist.input.keyboard.backlight_animation.enabled false (requires restart) + private static final boolean KEYBOARD_BACKLIGHT_ANIMATION_ENABLED = + InputProperties.enable_keyboard_backlight_animation().orElse(false); + + private static Optional<Boolean> sKeyboardBacklightControlOverride = Optional.empty(); + private static Optional<Boolean> sKeyboardBacklightAnimationOverride = Optional.empty(); + + public static boolean isKeyboardBacklightControlEnabled() { + return sKeyboardBacklightControlOverride.orElse(KEYBOARD_BACKLIGHT_CONTROL_ENABLED); + } + + public static boolean isKeyboardBacklightAnimationEnabled() { + return sKeyboardBacklightAnimationOverride.orElse(KEYBOARD_BACKLIGHT_ANIMATION_ENABLED); + } + + public static void setKeyboardBacklightControlEnabled(boolean enabled) { + sKeyboardBacklightControlOverride = Optional.of(enabled); + } + + public static void setKeyboardBacklightAnimationEnabled(boolean enabled) { + sKeyboardBacklightAnimationOverride = Optional.of(enabled); + } + + /** + * Clears all input feature flag overrides. + */ + public static void clearOverrides() { + sKeyboardBacklightControlOverride = Optional.empty(); + sKeyboardBacklightAnimationOverride = Optional.empty(); + } +} diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 662591e3d264..9f3ab885bf16 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -74,7 +74,6 @@ import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; -import android.os.SystemProperties; import android.os.UserHandle; import android.os.VibrationEffect; import android.os.vibrator.StepSegment; @@ -160,11 +159,6 @@ public class InputManagerService extends IInputManager.Stub private static final AdditionalDisplayInputProperties DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties(); - // To disable Keyboard backlight control via Framework, run: - // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart) - private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean( - "persist.input.keyboard.backlight_control.enabled", true); - private final NativeInputManagerService mNative; private final Context mContext; @@ -439,10 +433,9 @@ public class InputManagerService extends IInputManager.Stub mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore, injector.getLooper()); mBatteryController = new BatteryController(mContext, mNative, injector.getLooper()); - mKeyboardBacklightController = - KEYBOARD_BACKLIGHT_CONTROL_ENABLED ? new KeyboardBacklightController(mContext, - mNative, mDataStore, injector.getLooper()) - : new KeyboardBacklightControllerInterface() {}; + mKeyboardBacklightController = InputFeatureFlagProvider.isKeyboardBacklightControlEnabled() + ? new KeyboardBacklightController(mContext, mNative, mDataStore, + injector.getLooper()) : new KeyboardBacklightControllerInterface() {}; mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper()); mUseDevInputEventForAudioJack = diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java index 48c346a2fe22..61ca0cbff7bf 100644 --- a/services/core/java/com/android/server/input/KeyboardBacklightController.java +++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java @@ -16,6 +16,7 @@ package com.android.server.input; +import android.animation.ValueAnimator; import android.annotation.BinderThread; import android.content.Context; import android.graphics.Color; @@ -70,6 +71,8 @@ final class KeyboardBacklightController implements private static final int MSG_INTERACTIVE_STATE_CHANGED = 6; private static final int MAX_BRIGHTNESS = 255; private static final int NUM_BRIGHTNESS_CHANGE_STEPS = 10; + private static final long TRANSITION_ANIMATION_DURATION_MILLIS = + Duration.ofSeconds(1).toMillis(); private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight"; @@ -85,6 +88,7 @@ final class KeyboardBacklightController implements @GuardedBy("mDataStore") private final PersistentDataStore mDataStore; private final Handler mHandler; + private final AnimatorFactory mAnimatorFactory; // Always access on handler thread or need to lock this for synchronization. private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1); // Maintains state if all backlights should be on or turned off @@ -109,10 +113,17 @@ final class KeyboardBacklightController implements KeyboardBacklightController(Context context, NativeInputManagerService nativeService, PersistentDataStore dataStore, Looper looper) { + this(context, nativeService, dataStore, looper, ValueAnimator::ofInt); + } + + @VisibleForTesting + KeyboardBacklightController(Context context, NativeInputManagerService nativeService, + PersistentDataStore dataStore, Looper looper, AnimatorFactory animatorFactory) { mContext = context; mNative = nativeService; mDataStore = dataStore; mHandler = new Handler(looper, this::handleMessage); + mAnimatorFactory = animatorFactory; } @Override @@ -177,8 +188,7 @@ final class KeyboardBacklightController implements } else { newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0); } - updateBacklightState(deviceId, keyboardBacklight, newBrightnessLevel, - true /* isTriggeredByKeyPress */); + updateBacklightState(deviceId, newBrightnessLevel, true /* isTriggeredByKeyPress */); synchronized (mDataStore) { try { @@ -203,8 +213,7 @@ final class KeyboardBacklightController implements if (index < 0) { index = Math.min(NUM_BRIGHTNESS_CHANGE_STEPS, -(index + 1)); } - updateBacklightState(inputDevice.getId(), keyboardBacklight, index, - false /* isTriggeredByKeyPress */); + updateBacklightState(inputDevice.getId(), index, false /* isTriggeredByKeyPress */); if (DEBUG) { Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt()); } @@ -217,14 +226,10 @@ final class KeyboardBacklightController implements if (!mIsInteractive) { return; } - if (!mIsBacklightOn) { - mIsBacklightOn = true; - for (int i = 0; i < mKeyboardBacklights.size(); i++) { - int deviceId = mKeyboardBacklights.keyAt(i); - KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); - updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel, - false /* isTriggeredByKeyPress */); - } + mIsBacklightOn = true; + for (int i = 0; i < mKeyboardBacklights.size(); i++) { + KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); + state.onBacklightStateChanged(); } mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY); mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY, @@ -232,14 +237,10 @@ final class KeyboardBacklightController implements } private void handleUserInactivity() { - if (mIsBacklightOn) { - mIsBacklightOn = false; - for (int i = 0; i < mKeyboardBacklights.size(); i++) { - int deviceId = mKeyboardBacklights.keyAt(i); - KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); - updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel, - false /* isTriggeredByKeyPress */); - } + mIsBacklightOn = false; + for (int i = 0; i < mKeyboardBacklights.size(); i++) { + KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); + state.onBacklightStateChanged(); } } @@ -310,7 +311,7 @@ final class KeyboardBacklightController implements return; } // The keyboard backlight was added or changed. - mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(keyboardBacklight)); + mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(deviceId, keyboardBacklight)); restoreBacklightBrightness(inputDevice, keyboardBacklight); } @@ -372,21 +373,14 @@ final class KeyboardBacklightController implements } } - private void updateBacklightState(int deviceId, Light light, int brightnessLevel, + private void updateBacklightState(int deviceId, int brightnessLevel, boolean isTriggeredByKeyPress) { KeyboardBacklightState state = mKeyboardBacklights.get(deviceId); if (state == null) { return; } - mNative.setLightColor(deviceId, light.getId(), - mIsBacklightOn ? Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel], 0, 0, 0) - : 0); - if (DEBUG) { - Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel - + "(isBacklightOn = " + mIsBacklightOn + ")"); - } - state.mBrightnessLevel = brightnessLevel; + state.setBrightnessLevel(brightnessLevel); synchronized (mKeyboardBacklightListenerRecords) { for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) { @@ -397,6 +391,10 @@ final class KeyboardBacklightController implements deviceId, callbackState, isTriggeredByKeyPress); } } + + if (DEBUG) { + Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel); + } } private void onKeyboardBacklightListenerDied(int pid) { @@ -436,10 +434,7 @@ final class KeyboardBacklightController implements @Override public void dump(PrintWriter pw) { IndentingPrintWriter ipw = new IndentingPrintWriter(pw); - ipw.println( - TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights, isBacklightOn = " - + mIsBacklightOn); - + ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights"); ipw.increaseIndent(); for (int i = 0; i < mKeyboardBacklights.size(); i++) { KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); @@ -448,6 +443,10 @@ final class KeyboardBacklightController implements ipw.decreaseIndent(); } + private static boolean isAnimationEnabled() { + return InputFeatureFlagProvider.isKeyboardBacklightAnimationEnabled(); + } + // A record of a registered Keyboard backlight listener from one process. private class KeyboardBacklightListenerRecord implements IBinder.DeathRecipient { public final int mPid; @@ -478,14 +477,55 @@ final class KeyboardBacklightController implements } } - private static class KeyboardBacklightState { + private class KeyboardBacklightState { + private final int mDeviceId; private final Light mLight; private int mBrightnessLevel; + private ValueAnimator mAnimator; - KeyboardBacklightState(Light light) { + KeyboardBacklightState(int deviceId, Light light) { + mDeviceId = deviceId; mLight = light; } + private void onBacklightStateChanged() { + setBacklightValue(mIsBacklightOn ? BRIGHTNESS_VALUE_FOR_LEVEL[mBrightnessLevel] : 0); + } + private void setBrightnessLevel(int brightnessLevel) { + if (mIsBacklightOn) { + setBacklightValue(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel]); + } + mBrightnessLevel = brightnessLevel; + } + + private void cancelAnimation() { + if (mAnimator != null && mAnimator.isRunning()) { + mAnimator.cancel(); + } + } + + private void setBacklightValue(int toValue) { + int fromValue = Color.alpha(mNative.getLightColor(mDeviceId, mLight.getId())); + if (fromValue == toValue) { + return; + } + if (isAnimationEnabled()) { + startAnimation(fromValue, toValue); + } else { + mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb(toValue, 0, 0, 0)); + } + } + + private void startAnimation(int fromValue, int toValue) { + // Cancel any ongoing animation before starting a new one + cancelAnimation(); + mAnimator = mAnimatorFactory.makeIntAnimator(fromValue, toValue); + mAnimator.addUpdateListener( + (animation) -> mNative.setLightColor(mDeviceId, mLight.getId(), + Color.argb((int) animation.getAnimatedValue(), 0, 0, 0))); + mAnimator.setDuration(TRANSITION_ANIMATION_DURATION_MILLIS).start(); + } + @Override public String toString() { return "KeyboardBacklightState{Light=" + mLight.getId() @@ -493,4 +533,9 @@ final class KeyboardBacklightController implements + "}"; } } + + @VisibleForTesting + interface AnimatorFactory { + ValueAnimator makeIntAnimator(int from, int to); + } } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index d7eff52af9b4..56dcac8d122a 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -536,10 +536,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { int mShortPressOnSleepBehavior; int mShortPressOnWindowBehavior; int mPowerVolUpBehavior; - int mShortPressOnStemPrimaryBehavior; - int mDoublePressOnStemPrimaryBehavior; - int mTriplePressOnStemPrimaryBehavior; - int mLongPressOnStemPrimaryBehavior; boolean mStylusButtonsEnabled = true; boolean mHasSoftInput = false; boolean mHapticTextHandleEnabled; @@ -553,6 +549,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { int mSearchKeyBehavior; ComponentName mSearchKeyTargetActivity; + // Key Behavior - Stem Primary + private int mShortPressOnStemPrimaryBehavior; + private int mDoublePressOnStemPrimaryBehavior; + private int mTriplePressOnStemPrimaryBehavior; + private int mLongPressOnStemPrimaryBehavior; + private boolean mHandleVolumeKeysInWM; private boolean mPendingKeyguardOccluded; @@ -1989,6 +1991,21 @@ public class PhoneWindowManager implements WindowManagerPolicy { Supplier<GlobalActions> getGlobalActionsFactory() { return () -> new GlobalActions(mContext, mWindowManagerFuncs); } + + KeyguardServiceDelegate getKeyguardServiceDelegate() { + return new KeyguardServiceDelegate(mContext, + new StateCallback() { + @Override + public void onTrustedChanged() { + mWindowManagerFuncs.notifyKeyguardTrustedChanged(); + } + + @Override + public void onShowingChanged() { + mWindowManagerFuncs.onKeyguardShowingAndNotOccludedChanged(); + } + }); + } } /** {@inheritDoc} */ @@ -2246,18 +2263,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mKeyguardDrawnTimeout = mContext.getResources().getInteger( com.android.internal.R.integer.config_keyguardDrawnTimeout); - mKeyguardDelegate = new KeyguardServiceDelegate(mContext, - new StateCallback() { - @Override - public void onTrustedChanged() { - mWindowManagerFuncs.notifyKeyguardTrustedChanged(); - } - - @Override - public void onShowingChanged() { - mWindowManagerFuncs.onKeyguardShowingAndNotOccludedChanged(); - } - }); + mKeyguardDelegate = injector.getKeyguardServiceDelegate(); initKeyCombinationRules(); initSingleKeyGestureRules(); mSideFpsEventHandler = new SideFpsEventHandler(mContext, mHandler, mPowerManager); diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 77e70a25d497..d0ca8e3ed3e7 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -176,7 +176,7 @@ public class DisplayPolicy { // TODO(b/266197298): Remove this by a more general protocol from the insets providers. private static final boolean USE_CACHED_INSETS_FOR_DISPLAY_SWITCH = - SystemProperties.getBoolean("persist.wm.debug.cached_insets_switch", false); + SystemProperties.getBoolean("persist.wm.debug.cached_insets_switch", true); private final WindowManagerService mService; private final Context mContext; diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java index 258351436188..4a3d0c142e1d 100644 --- a/services/core/java/com/android/server/wm/DragDropController.java +++ b/services/core/java/com/android/server/wm/DragDropController.java @@ -181,7 +181,11 @@ class DragDropController { } } finally { if (surface != null) { - surface.release(); + try (final SurfaceControl.Transaction transaction = + mService.mTransactionFactory.get()) { + transaction.remove(surface); + transaction.apply(); + } } } } diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index ddf96c53323d..698c9abee1f5 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -440,8 +440,8 @@ class InsetsPolicy { return originalState; } - void onInsetsModified(InsetsControlTarget caller) { - mStateController.onInsetsModified(caller); + void onRequestedVisibleTypesChanged(InsetsControlTarget caller) { + mStateController.onRequestedVisibleTypesChanged(caller); checkAbortTransient(caller); updateBarControlTarget(mFocusedWin); } diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 249ead0a8509..addb5219c663 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -190,7 +190,7 @@ class InsetsStateController { } } - void onInsetsModified(InsetsControlTarget caller) { + void onRequestedVisibleTypesChanged(InsetsControlTarget caller) { boolean changed = false; for (int i = mProviders.size() - 1; i >= 0; i--) { changed |= mProviders.valueAt(i).updateClientVisibility(caller); @@ -352,7 +352,7 @@ class InsetsStateController { // to the clients, so that the clients can change the current visibilities to the // requested visibilities with animations. for (int i = newControlTargets.size() - 1; i >= 0; i--) { - onInsetsModified(newControlTargets.valueAt(i)); + onRequestedVisibleTypesChanged(newControlTargets.valueAt(i)); } newControlTargets.clear(); }); diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 7b10c6372b0e..b49c5fb2c25d 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -687,11 +687,11 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { @Override public void updateRequestedVisibleTypes(IWindow window, @InsetsType int requestedVisibleTypes) { synchronized (mService.mGlobalLock) { - final WindowState windowState = mService.windowForClientLocked(this, window, + final WindowState win = mService.windowForClientLocked(this, window, false /* throwOnError */); - if (windowState != null) { - windowState.setRequestedVisibleTypes(requestedVisibleTypes); - windowState.getDisplayContent().getInsetsPolicy().onInsetsModified(windowState); + if (win != null) { + win.setRequestedVisibleTypes(requestedVisibleTypes); + win.getDisplayContent().getInsetsPolicy().onRequestedVisibleTypesChanged(win); } } } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index e33c6f03c720..9c636ea0ae85 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -4537,7 +4537,8 @@ public class WindowManagerService extends IWindowManager.Stub return; } dc.mRemoteInsetsControlTarget.setRequestedVisibleTypes(requestedVisibleTypes); - dc.getInsetsStateController().onInsetsModified(dc.mRemoteInsetsControlTarget); + dc.getInsetsStateController().onRequestedVisibleTypesChanged( + dc.mRemoteInsetsControlTarget); } } finally { Binder.restoreCallingIdentity(origId); diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index f0d718a30535..2f1bf35a5ca8 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -1596,8 +1596,8 @@ PointerIconStyle NativeInputManager::getDefaultPointerIconId() { } PointerIconStyle NativeInputManager::getDefaultStylusIconId() { - // TODO: add resource for default stylus icon and change this - return PointerIconStyle::TYPE_CROSSHAIR; + // Use the empty icon as the default pointer icon for a hovering stylus. + return PointerIconStyle::TYPE_NULL; } PointerIconStyle NativeInputManager::getCustomPointerIconId() { diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index cfeaf0b54552..2b9a227be758 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -28,6 +28,7 @@ android_test { "services.accessibility", "services.appwidget", "services.autofill", + "services.contentcapture", "services.backup", "services.companion", "services.core", diff --git a/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionPackageManagerTest.java b/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionPackageManagerTest.java new file mode 100644 index 000000000000..7d45ea4ce39a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionPackageManagerTest.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2023 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.server.contentprotection; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.Manifest.permission; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.PackageManager.PackageInfoFlags; +import android.testing.TestableContext; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** + * Test for {@link ContentProtectionPackageManager}. + * + * <p>Run with: {@code atest + * FrameworksServicesTests:com.android.server.contentprotection.ContentProtectionPackageManagerTest} + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ContentProtectionPackageManagerTest { + private static final String PACKAGE_NAME = "PACKAGE_NAME"; + + private static final PackageInfo EMPTY_PACKAGE_INFO = new PackageInfo(); + + private static final PackageInfo SYSTEM_APP_PACKAGE_INFO = createSystemAppPackageInfo(); + + private static final PackageInfo UPDATED_SYSTEM_APP_PACKAGE_INFO = + createUpdatedSystemAppPackageInfo(); + + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Rule + public final TestableContext mContext = + new TestableContext(ApplicationProvider.getApplicationContext()); + + @Mock private PackageManager mMockPackageManager; + + private ContentProtectionPackageManager mContentProtectionPackageManager; + + @Before + public void setup() { + mContext.setMockPackageManager(mMockPackageManager); + mContentProtectionPackageManager = new ContentProtectionPackageManager(mContext); + } + + @Test + public void getPackageInfo_found() throws Exception { + PackageInfo expected = createPackageInfo(/* flags= */ 0); + when(mMockPackageManager.getPackageInfo(eq(PACKAGE_NAME), any(PackageInfoFlags.class))) + .thenReturn(expected); + + PackageInfo actual = mContentProtectionPackageManager.getPackageInfo(PACKAGE_NAME); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void getPackageInfo_notFound() throws Exception { + when(mMockPackageManager.getPackageInfo(eq(PACKAGE_NAME), any(PackageInfoFlags.class))) + .thenThrow(new NameNotFoundException()); + + PackageInfo actual = mContentProtectionPackageManager.getPackageInfo(PACKAGE_NAME); + + assertThat(actual).isNull(); + } + + @Test + public void getPackageInfo_null() { + PackageInfo actual = mContentProtectionPackageManager.getPackageInfo(PACKAGE_NAME); + + assertThat(actual).isNull(); + } + + @Test + public void isSystemApp_true() { + boolean actual = mContentProtectionPackageManager.isSystemApp(SYSTEM_APP_PACKAGE_INFO); + + assertThat(actual).isTrue(); + } + + @Test + public void isSystemApp_false() { + boolean actual = + mContentProtectionPackageManager.isSystemApp(UPDATED_SYSTEM_APP_PACKAGE_INFO); + + assertThat(actual).isFalse(); + } + + @Test + public void isSystemApp_noApplicationInfo() { + boolean actual = mContentProtectionPackageManager.isSystemApp(EMPTY_PACKAGE_INFO); + + assertThat(actual).isFalse(); + } + + @Test + public void isUpdatedSystemApp_true() { + boolean actual = + mContentProtectionPackageManager.isUpdatedSystemApp( + UPDATED_SYSTEM_APP_PACKAGE_INFO); + + assertThat(actual).isTrue(); + } + + @Test + public void isUpdatedSystemApp_false() { + boolean actual = + mContentProtectionPackageManager.isUpdatedSystemApp(SYSTEM_APP_PACKAGE_INFO); + + assertThat(actual).isFalse(); + } + + @Test + public void isUpdatedSystemApp_noApplicationInfo() { + boolean actual = mContentProtectionPackageManager.isUpdatedSystemApp(EMPTY_PACKAGE_INFO); + + assertThat(actual).isFalse(); + } + + @Test + public void hasRequestedInternetPermissions_true() { + PackageInfo packageInfo = createPackageInfo(new String[] {permission.INTERNET}); + + boolean actual = + mContentProtectionPackageManager.hasRequestedInternetPermissions(packageInfo); + + assertThat(actual).isTrue(); + } + + @Test + public void hasRequestedInternetPermissions_false() { + PackageInfo packageInfo = createPackageInfo(new String[] {permission.ACCESS_FINE_LOCATION}); + + boolean actual = + mContentProtectionPackageManager.hasRequestedInternetPermissions(packageInfo); + + assertThat(actual).isFalse(); + } + + @Test + public void hasRequestedInternetPermissions_noRequestedPermissions() { + boolean actual = + mContentProtectionPackageManager.hasRequestedInternetPermissions( + EMPTY_PACKAGE_INFO); + + assertThat(actual).isFalse(); + } + + private static PackageInfo createSystemAppPackageInfo() { + return createPackageInfo(ApplicationInfo.FLAG_SYSTEM); + } + + private static PackageInfo createUpdatedSystemAppPackageInfo() { + return createPackageInfo(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP); + } + + private static PackageInfo createPackageInfo(int flags) { + return createPackageInfo(flags, /* requestedPermissions= */ new String[0]); + } + + private static PackageInfo createPackageInfo(String[] requestedPermissions) { + return createPackageInfo(/* flags= */ 0, requestedPermissions); + } + + private static PackageInfo createPackageInfo(int flags, String[] requestedPermissions) { + PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = PACKAGE_NAME; + packageInfo.applicationInfo = new ApplicationInfo(); + packageInfo.applicationInfo.packageName = PACKAGE_NAME; + packageInfo.applicationInfo.flags = flags; + packageInfo.requestedPermissions = requestedPermissions; + return packageInfo; + } +} diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt index 64c05dc8ab84..272679280a62 100644 --- a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt @@ -16,6 +16,7 @@ package com.android.server.input +import android.animation.ValueAnimator import android.content.Context import android.content.ContextWrapper import android.graphics.Color @@ -29,6 +30,7 @@ import android.os.UEventObserver import android.os.test.TestLooper import android.platform.test.annotations.Presubmit import android.view.InputDevice +import androidx.test.annotation.UiThreadTest import androidx.test.core.app.ApplicationProvider import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_VALUE_FOR_LEVEL import com.android.server.input.KeyboardBacklightController.USER_INACTIVITY_THRESHOLD_MILLIS @@ -96,9 +98,11 @@ class KeyboardBacklightControllerTests { private lateinit var context: Context private lateinit var dataStore: PersistentDataStore private lateinit var testLooper: TestLooper + private val totalLevels = BRIGHTNESS_VALUE_FOR_LEVEL.size private var lightColorMap: HashMap<Int, Int> = HashMap() private var lastBacklightState: KeyboardBacklightState? = null private var sysfsNodeChanges = 0 + private var lastAnimationValues = IntArray(2) @Before fun setup() { @@ -115,8 +119,8 @@ class KeyboardBacklightControllerTests { override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} }) testLooper = TestLooper() - keyboardBacklightController = - KeyboardBacklightController(context, native, dataStore, testLooper.looper) + keyboardBacklightController = KeyboardBacklightController(context, native, dataStore, + testLooper.looper, FakeAnimatorFactory()) InputManagerGlobal.resetInstance(iInputManager) val inputManager = InputManager(context) `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager) @@ -125,6 +129,10 @@ class KeyboardBacklightControllerTests { val args = it.arguments lightColorMap.put(args[1] as Int, args[2] as Int) } + `when`(native.getLightColor(anyInt(), anyInt())).thenAnswer { + val args = it.arguments + lightColorMap.getOrDefault(args[1] as Int, 0) + } lightColorMap.clear() `when`(native.sysfsNodeChanged(any())).then { sysfsNodeChanges++ @@ -138,271 +146,287 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklightIncrementDecrement() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) { + for (level in 1 until totalLevels) { + incrementKeyboardBacklight(DEVICE_ID) + assertEquals( + "Light value for level $level mismatched", + Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + assertEquals( + "Light value for level $level must be correctly stored in the datastore", + BRIGHTNESS_VALUE_FOR_LEVEL[level], + dataStore.getKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID + ).asInt + ) + } + + // Increment above max level incrementKeyboardBacklight(DEVICE_ID) assertEquals( - "Light value for level $level mismatched", - Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + "Light value for max level mismatched", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), lightColorMap[LIGHT_ID] ) assertEquals( - "Light value for level $level must be correctly stored in the datastore", - BRIGHTNESS_VALUE_FOR_LEVEL[level], + "Light value for max level must be correctly stored in the datastore", + MAX_BRIGHTNESS, dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID + keyboardWithBacklight.descriptor, + LIGHT_ID ).asInt ) - } - // Increment above max level - incrementKeyboardBacklight(DEVICE_ID) - assertEquals( - "Light value for max level mismatched", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) - assertEquals( - "Light value for max level must be correctly stored in the datastore", - MAX_BRIGHTNESS, - dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID - ).asInt - ) + for (level in totalLevels - 2 downTo 0) { + decrementKeyboardBacklight(DEVICE_ID) + assertEquals( + "Light value for level $level mismatched", + Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + assertEquals( + "Light value for level $level must be correctly stored in the datastore", + BRIGHTNESS_VALUE_FOR_LEVEL[level], + dataStore.getKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID + ).asInt + ) + } - for (level in BRIGHTNESS_VALUE_FOR_LEVEL.size - 2 downTo 0) { + // Decrement below min level decrementKeyboardBacklight(DEVICE_ID) assertEquals( - "Light value for level $level mismatched", - Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + "Light value for min level mismatched", + Color.argb(0, 0, 0, 0), lightColorMap[LIGHT_ID] ) assertEquals( - "Light value for level $level must be correctly stored in the datastore", - BRIGHTNESS_VALUE_FOR_LEVEL[level], + "Light value for min level must be correctly stored in the datastore", + 0, dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID + keyboardWithBacklight.descriptor, + LIGHT_ID ).asInt ) } - - // Decrement below min level - decrementKeyboardBacklight(DEVICE_ID) - assertEquals( - "Light value for min level mismatched", - Color.argb(0, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) - assertEquals( - "Light value for min level must be correctly stored in the datastore", - 0, - dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID - ).asInt - ) } @Test fun testKeyboardWithoutBacklight() { - val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) - val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - - incrementKeyboardBacklight(DEVICE_ID) - assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty()) + BacklightAnimationFlag(false).use { + val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) + val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + incrementKeyboardBacklight(DEVICE_ID) + assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty()) + } } @Test fun testKeyboardWithMultipleLight() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn( - listOf( - keyboardBacklight, - keyboardInputLight + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn( + listOf( + keyboardBacklight, + keyboardInputLight + ) ) - ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - incrementKeyboardBacklight(DEVICE_ID) - assertEquals("Only keyboard backlights should change", 1, lightColorMap.size) - assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID]) - assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID]) + incrementKeyboardBacklight(DEVICE_ID) + assertEquals("Only keyboard backlights should change", 1, lightColorMap.size) + assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID]) + assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID]) + } } @Test fun testRestoreBacklightOnInputDeviceAdded() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - - for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) { - dataStore.setKeyboardBacklightBrightness( + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + + for (level in 1 until totalLevels) { + dataStore.setKeyboardBacklightBrightness( keyboardWithBacklight.descriptor, LIGHT_ID, BRIGHTNESS_VALUE_FOR_LEVEL[level] - 1 - ) - - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data " + - "store", + ) + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be restored to the level saved in the " + + "data store", Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), lightColorMap[LIGHT_ID] - ) - keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID) + ) + keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID) + } } } @Test fun testRestoreBacklightOnInputDeviceChanged() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - MAX_BRIGHTNESS - ) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + MAX_BRIGHTNESS + ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertTrue( - "Keyboard backlight should not be changed until its added", - lightColorMap.isEmpty() - ) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertTrue( + "Keyboard backlight should not be changed until its added", + lightColorMap.isEmpty() + ) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceChanged(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data store", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceChanged(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data store", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + } } @Test fun testKeyboardBacklight_registerUnregisterListener() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - // Register backlight listener - val listener = KeyboardBacklightListener() - keyboardBacklightController.registerKeyboardBacklightListener(listener, 0) + // Register backlight listener + val listener = KeyboardBacklightListener() + keyboardBacklightController.registerKeyboardBacklightListener(listener, 0) - lastBacklightState = null - keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) - testLooper.dispatchNext() + lastBacklightState = null + keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) + testLooper.dispatchNext() - assertEquals( - "Backlight state device Id should be $DEVICE_ID", - DEVICE_ID, - lastBacklightState!!.deviceId - ) - assertEquals( - "Backlight state brightnessLevel should be " + 1, - 1, - lastBacklightState!!.brightnessLevel - ) - assertEquals( - "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1), - (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1), - lastBacklightState!!.maxBrightnessLevel - ) - assertEquals( - "Backlight state isTriggeredByKeyPress should be true", - true, - lastBacklightState!!.isTriggeredByKeyPress - ) + assertEquals( + "Backlight state device Id should be $DEVICE_ID", + DEVICE_ID, + lastBacklightState!!.deviceId + ) + assertEquals( + "Backlight state brightnessLevel should be " + 1, + 1, + lastBacklightState!!.brightnessLevel + ) + assertEquals( + "Backlight state maxBrightnessLevel should be " + (totalLevels - 1), + (totalLevels - 1), + lastBacklightState!!.maxBrightnessLevel + ) + assertEquals( + "Backlight state isTriggeredByKeyPress should be true", + true, + lastBacklightState!!.isTriggeredByKeyPress + ) - // Unregister listener - keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0) + // Unregister listener + keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0) - lastBacklightState = null - incrementKeyboardBacklight(DEVICE_ID) + lastBacklightState = null + incrementKeyboardBacklight(DEVICE_ID) - assertNull("Listener should not receive any updates", lastBacklightState) + assertNull("Listener should not receive any updates", lastBacklightState) + } } @Test fun testKeyboardBacklight_userActivity() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - MAX_BRIGHTNESS - ) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + MAX_BRIGHTNESS + ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data store", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data store", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) - testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000) - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be turned off after inactivity", - 0, - lightColorMap[LIGHT_ID] - ) + testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000) + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be turned off after inactivity", + 0, + lightColorMap[LIGHT_ID] + ) + } } @Test fun testKeyboardBacklight_displayOnOff() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - MAX_BRIGHTNESS - ) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + MAX_BRIGHTNESS + ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */) - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data " + - "store when display turned on", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */) + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data " + + "store when display turned on", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) - keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */) - assertEquals( - "Keyboard backlight level should be turned off after display is turned off", - 0, - lightColorMap[LIGHT_ID] - ) + keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */) + assertEquals( + "Keyboard backlight level should be turned off after display is turned off", + 0, + lightColorMap[LIGHT_ID] + ) + } } @Test @@ -463,6 +487,30 @@ class KeyboardBacklightControllerTests { ) } + @Test + @UiThreadTest + fun testKeyboardBacklightAnimation_onChangeLevels() { + BacklightAnimationFlag(true).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + incrementKeyboardBacklight(DEVICE_ID) + assertEquals( + "Should start animation from level 0", + BRIGHTNESS_VALUE_FOR_LEVEL[0], + lastAnimationValues[0] + ) + assertEquals( + "Should start animation to level 1", + BRIGHTNESS_VALUE_FOR_LEVEL[1], + lastAnimationValues[1] + ) + } + } + inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() { override fun onBrightnessChanged( deviceId: Int, @@ -496,4 +544,22 @@ class KeyboardBacklightControllerTests { val maxBrightnessLevel: Int, val isTriggeredByKeyPress: Boolean ) + + private inner class BacklightAnimationFlag constructor(enabled: Boolean) : AutoCloseable { + init { + InputFeatureFlagProvider.setKeyboardBacklightAnimationEnabled(enabled) + } + + override fun close() { + InputFeatureFlagProvider.clearOverrides() + } + } + + private inner class FakeAnimatorFactory : KeyboardBacklightController.AnimatorFactory { + override fun makeIntAnimator(from: Int, to: Int): ValueAnimator { + lastAnimationValues[0] = from + lastAnimationValues[1] = to + return ValueAnimator.ofInt(from, to) + } + } } diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java index 676bfb00c60c..2015ae9b8081 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java +++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java @@ -39,6 +39,7 @@ import static android.view.KeyEvent.META_SHIFT_RIGHT_ON; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER; import static java.util.Collections.unmodifiableMap; @@ -59,7 +60,7 @@ import java.util.Map; class ShortcutKeyTestBase { TestPhoneWindowManager mPhoneWindowManager; - final Context mContext = getInstrumentation().getTargetContext(); + final Context mContext = spy(getInstrumentation().getTargetContext()); /** Modifier key to meta state */ private static final Map<Integer, Integer> MODIFIER; diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java new file mode 100644 index 000000000000..fe8017e5f513 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 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.server.policy; + +import static android.view.KeyEvent.KEYCODE_STEM_PRIMARY; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; + +import android.content.Context; +import android.content.res.Resources; + +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +/** + * Test class for stem key gesture. + * + * Build/Install/Run: + * atest WmTests:StemKeyGestureTests + */ +public class StemKeyGestureTests extends ShortcutKeyTestBase { + @Mock private Resources mResources; + + /** + * Stem single key should not launch behavior during set up. + */ + @Test + public void stemSingleKey_duringSetup_doNothing() { + stemKeySetup( + () -> overrideBehavior( + com.android.internal.R.integer.config_shortPressOnStemPrimaryBehavior, + SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS)); + mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false); + mPhoneWindowManager.overrideIsUserSetupComplete(false); + + sendKey(KEYCODE_STEM_PRIMARY); + + mPhoneWindowManager.assertNotOpenAllAppView(); + } + + /** + * Stem single key should launch all app after set up. + */ + @Test + public void stemSingleKey_AfterSetup_openAllApp() { + stemKeySetup( + () -> overrideBehavior( + com.android.internal.R.integer.config_shortPressOnStemPrimaryBehavior, + SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS)); + mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false); + mPhoneWindowManager.overrideIsUserSetupComplete(true); + + sendKey(KEYCODE_STEM_PRIMARY); + + mPhoneWindowManager.assertOpenAllAppView(); + } + + private void stemKeySetup(Runnable behaviorOverrideRunnable) { + super.tearDown(); + setupResourcesMock(); + behaviorOverrideRunnable.run(); + super.setUp(); + } + + private void setupResourcesMock() { + Resources realResources = mContext.getResources(); + + mResources = Mockito.mock(Resources.class); + doReturn(mResources).when(mContext).getResources(); + + doAnswer(invocation -> realResources.getXml((Integer) invocation.getArguments()[0])) + .when(mResources).getXml(anyInt()); + doAnswer(invocation -> realResources.getString((Integer) invocation.getArguments()[0])) + .when(mResources).getString(anyInt()); + doAnswer(invocation -> realResources.getBoolean((Integer) invocation.getArguments()[0])) + .when(mResources).getBoolean(anyInt()); + } + + private void overrideBehavior(int resId, int expectedBehavior) { + doReturn(expectedBehavior).when(mResources).getInteger(eq(resId)); + } +} diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index d38302429c02..af48cbd616ac 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -16,6 +16,7 @@ package com.android.server.policy; +import static android.os.Build.HW_TIMEOUT_MULTIPLIER; import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.STATE_ON; @@ -43,8 +44,11 @@ import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_SHUT import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.after; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mockingDetails; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.withSettings; @@ -64,6 +68,7 @@ import android.os.HandlerThread; import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.RemoteException; +import android.os.UserHandle; import android.os.Vibrator; import android.service.dreams.DreamManagerInternal; import android.telecom.TelecomManager; @@ -79,6 +84,7 @@ import com.android.server.LocalServices; import com.android.server.input.InputManagerInternal; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.pm.UserManagerInternal; +import com.android.server.policy.keyguard.KeyguardServiceDelegate; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.vr.VrManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal; @@ -100,6 +106,8 @@ import java.util.function.Supplier; class TestPhoneWindowManager { private static final long SHORTCUT_KEY_DELAY_MILLIS = 150; + private static final long TEST_SINGLE_KEY_DELAY_MILLIS + = SingleKeyGestureDetector.MULTI_PRESS_TIMEOUT + 1000L * HW_TIMEOUT_MULTIPLIER; private PhoneWindowManager mPhoneWindowManager; private Context mContext; @@ -134,6 +142,8 @@ class TestPhoneWindowManager { @Mock private StatusBarManagerInternal mStatusBarManagerInternal; + @Mock private KeyguardServiceDelegate mKeyguardServiceDelegate; + private StaticMockitoSession mMockitoSession; private HandlerThread mHandlerThread; private Handler mHandler; @@ -151,6 +161,10 @@ class TestPhoneWindowManager { Supplier<GlobalActions> getGlobalActionsFactory() { return () -> mGlobalActions; } + + KeyguardServiceDelegate getKeyguardServiceDelegate() { + return mKeyguardServiceDelegate; + } } TestPhoneWindowManager(Context context) { @@ -158,12 +172,12 @@ class TestPhoneWindowManager { mHandlerThread = new HandlerThread("fake window manager"); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); - mHandler.runWithScissors(()-> setUp(context), 0 /* timeout */); + mContext = mockingDetails(context).isSpy() ? context : spy(context); + mHandler.runWithScissors(this::setUp, 0 /* timeout */); } - private void setUp(Context context) { + private void setUp() { mPhoneWindowManager = spy(new PhoneWindowManager()); - mContext = spy(context); // Use stubOnly() to reduce memory usage if it doesn't need verification. final MockSettings spyStubOnly = withSettings().stubOnly() @@ -251,6 +265,7 @@ class TestPhoneWindowManager { overrideLaunchAccessibility(); doReturn(false).when(mPhoneWindowManager).keyguardOn(); doNothing().when(mContext).startActivityAsUser(any(), any()); + doNothing().when(mContext).startActivityAsUser(any(), any(), any()); Mockito.reset(mContext); } @@ -381,6 +396,14 @@ class TestPhoneWindowManager { doNothing().when(mPhoneWindowManager).launchHomeFromHotKey(anyInt()); } + void overrideIsUserSetupComplete(boolean isCompleted) { + doReturn(isCompleted).when(mPhoneWindowManager).isUserSetupComplete(); + } + + void setKeyguardServiceDelegateIsShowing(boolean isShowing) { + doReturn(isShowing).when(mKeyguardServiceDelegate).isShowing(); + } + /** * Below functions will check the policy behavior could be invoked. */ @@ -514,4 +537,18 @@ class TestPhoneWindowManager { waitForIdle(); verify(mPhoneWindowManager).launchHomeFromHotKey(anyInt()); } + + void assertOpenAllAppView() { + waitForIdle(); + ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mContext, timeout(TEST_SINGLE_KEY_DELAY_MILLIS)) + .startActivityAsUser(intentCaptor.capture(), isNull(), any(UserHandle.class)); + Assert.assertEquals(Intent.ACTION_ALL_APPS, intentCaptor.getValue().getAction()); + } + + void assertNotOpenAllAppView() { + waitForIdle(); + verify(mContext, after(TEST_SINGLE_KEY_DELAY_MILLIS).never()) + .startActivityAsUser(any(Intent.class), any(), any(UserHandle.class)); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index cb984f814f1a..77e944f35cb2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -3329,7 +3329,7 @@ public class ActivityRecordTests extends WindowTestsBase { // app1 requests IME visible. app1.setRequestedVisibleTypes(ime(), ime()); - mDisplayContent.getInsetsStateController().onInsetsModified(app1); + mDisplayContent.getInsetsStateController().onRequestedVisibleTypesChanged(app1); // Verify app1's IME insets is visible and app2's IME insets frozen flag set. assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible()); @@ -3398,7 +3398,7 @@ public class ActivityRecordTests extends WindowTestsBase { assertFalse(activity2.mImeInsetsFrozenUntilStartInput); app1.setRequestedVisibleTypes(ime()); - controller.onInsetsModified(app1); + controller.onRequestedVisibleTypesChanged(app1); // Expect all activities in split-screen will get IME insets visible state assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible()); diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java index a8fc25fc4477..204cbf79dba9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java @@ -370,7 +370,7 @@ public class InsetsPolicyTest extends WindowTestsBase { mAppWindow.setRequestedVisibleTypes( navigationBars() | statusBars(), navigationBars() | statusBars()); - policy.onInsetsModified(mAppWindow); + policy.onRequestedVisibleTypesChanged(mAppWindow); waitUntilWindowAnimatorIdle(); controls = mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow); diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java index d1d83f62934d..866260757df9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java @@ -190,17 +190,16 @@ public class InsetsStateControllerTest extends WindowTestsBase { // This can be the IME z-order target while app cannot be the IME z-order target. // This is also the only IME control target in this test, so IME won't be invisible caused // by the control-target change. - mDisplayContent.updateImeInputAndControlTarget( - createWindow(null, TYPE_APPLICATION, "base")); + final WindowState base = createWindow(null, TYPE_APPLICATION, "base"); + mDisplayContent.updateImeInputAndControlTarget(base); // Make IME and stay visible during the test. mImeWindow.setHasSurface(true); getController().getOrCreateSourceProvider(ID_IME, ime()) .setWindowContainer(mImeWindow, null, null); - getController().onImeControlTargetChanged( - mDisplayContent.getImeInputTarget().getWindowState()); - mDisplayContent.getImeInputTarget().getWindowState().setRequestedVisibleTypes(ime(), ime()); - getController().onInsetsModified(mDisplayContent.getImeInputTarget().getWindowState()); + getController().onImeControlTargetChanged(base); + base.setRequestedVisibleTypes(ime(), ime()); + getController().onRequestedVisibleTypesChanged(base); // Send our spy window (app) into the system so that we can detect the invocation. final WindowState win = createWindow(null, TYPE_APPLICATION, "app"); @@ -481,7 +480,7 @@ public class InsetsStateControllerTest extends WindowTestsBase { mDisplayContent.updateImeInputAndControlTarget(app); app.setRequestedVisibleTypes(ime(), ime()); - getController().onInsetsModified(app); + getController().onRequestedVisibleTypesChanged(app); assertTrue(ime.getControllableInsetProvider().getSource().isVisible()); getController().updateAboveInsetsState(true /* notifyInsetsChange */); diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt new file mode 100644 index 000000000000..c0c738b16c2a --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2023 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.server.wm.flicker.activityembedding + +import android.platform.test.annotations.Presubmit +import android.tools.common.datatypes.Rect +import android.tools.common.traces.component.ComponentNameMatcher +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test closing a secondary activity in a split. + * + * Setup: Launch A|B in split with B being the secondary activity. + * Transitions: Finish B and expect A to become fullscreen. + * + * To run this test: `atest FlickerTests:CloseSecondaryActivityInSplitTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class CloseSecondaryActivityInSplitTest(flicker: FlickerTest) : + ActivityEmbeddingTestBase(flicker) { + + override val transition: FlickerBuilder.() -> Unit = { + setup { + tapl.setExpectedRotationCheckEnabled(false) + // Launches fullscreen A. + testApp.launchViaIntent(wmHelper) + // Launches a split A|B and waits for both activities to show. + testApp.launchSecondaryActivity(wmHelper) + // Get fullscreen bounds + startDisplayBounds = + wmHelper.currentState.layerState.physicalDisplayBounds ?: + error("Can't get display bounds") + } + transitions { + // Finish secondary activity B. + testApp.finishSecondaryActivity(wmHelper) + // Expect the main activity A to expand into fullscreen. + wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify() + } + teardown { + tapl.goHome() + testApp.exit(wmHelper) + } + } + + /** Main activity is always visible and becomes fullscreen in the end. */ + @Presubmit + @Test + fun mainActivityWindowBecomesFullScreen() { + flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) } + flicker.assertWmEnd { + this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + .coversExactly(startDisplayBounds) + } + } + + /** Main activity surface is animated from split to fullscreen. */ + @Presubmit + @Test + fun mainActivityLayerIsAlwaysVisible() { + flicker.assertLayers { + isVisible( + ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT.or( + ComponentNameMatcher.TRANSITION_SNAPSHOT + ) + ) + } + flicker.assertLayersEnd { + isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + .isInvisible(ComponentNameMatcher.TRANSITION_SNAPSHOT) + } + } + + /** Secondary activity should destroy and become invisible. */ + @Presubmit + @Test + fun secondaryActivityWindowFinishes() { + flicker.assertWm { + contains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + .then() + .notContains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + } + } + + @Presubmit + @Test + fun secondaryActivityLayerFinishes() { + flicker.assertLayers { + isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + .then() + .isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + } + } + + companion object { + /** {@inheritDoc} */ + private var startDisplayBounds = Rect.EMPTY + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } + }
\ No newline at end of file diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt new file mode 100644 index 000000000000..39ae8e2d9799 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2023 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.server.wm.flicker.activityembedding + +import android.platform.test.annotations.Presubmit +import android.tools.common.traces.component.ComponentNameMatcher +import com.android.server.wm.flicker.rotation.RotationTransition +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Tests rotating two activities in an Activity Embedding split. + * + * Setup: Launch A|B in split with B being the secondary activity. + * Transitions: Rotate display, and expect A and B to split evenly in new rotation. + * + * To run this test: `atest FlickerTests:RotateSplitNoChangeTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class RotateSplitNoChangeTest(flicker: FlickerTest) : RotationTransition(flicker) { + + override val testApp = ActivityEmbeddingAppHelper(instrumentation) + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + testApp.launchViaIntent(wmHelper) + testApp.launchSecondaryActivity(wmHelper) + } + } + + /** + * Checks that the [ComponentNameMatcher.ROTATION] layer appears during the transition, doesn't + * flicker, and disappears before the transition is complete + */ + @Presubmit + @Test + fun rotationLayerAppearsAndVanishes() { + flicker.assertLayers { + this.isVisible(testApp) + .then() + .isVisible(ComponentNameMatcher.ROTATION) + .then() + .isVisible(testApp) + .isInvisible(ComponentNameMatcher.ROTATION) + } + } + + /** + * Overrides inherited assertion because in AE Split, the main and secondary activity are separate + * layers, each covering up exactly half of the display. + */ + @Presubmit + @Test + override fun appLayerRotates_StartingPos() { + flicker.assertLayersStart { + this.entry.displays.map { display -> + val leftLayerRegion = this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + val rightLayerRegion = + this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + // Compare dimensions of two splits, given we're using default split attributes, + // both activities take up the same visible size on the display. + check{"height"}.that(leftLayerRegion.region.height).isEqual(rightLayerRegion.region.height) + check{"width"}.that(leftLayerRegion.region.width).isEqual(rightLayerRegion.region.width) + leftLayerRegion.notOverlaps(rightLayerRegion.region) + // Layers of two activities sum to be fullscreen size on display. + leftLayerRegion.plus(rightLayerRegion.region).coversExactly(display.layerStackSpace) + } + } + } + + /** + * Verifies dimensions of both split activities hold their invariance after transition too. + */ + @Presubmit + @Test + override fun appLayerRotates_EndingPos() { + flicker.assertLayersEnd { + this.entry.displays.map { display -> + val leftLayerRegion = this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + val rightLayerRegion = + this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + check{"height"}.that(leftLayerRegion.region.height).isEqual(rightLayerRegion.region.height) + check{"width"}.that(leftLayerRegion.region.width).isEqual(rightLayerRegion.region.width) + leftLayerRegion.notOverlaps(rightLayerRegion.region) + leftLayerRegion.plus(rightLayerRegion.region).coversExactly(display.layerStackSpace) + } + } + } + + /** Both activities in split should remain visible during rotation. */ + @Presubmit + @Test + fun bothActivitiesAreAlwaysVisible() { + flicker.assertWm { + isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + } + flicker.assertWm { + isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.rotationTests() + } + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt index daecfe7e4c4d..e019b2b22680 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt @@ -60,6 +60,24 @@ constructor( } /** + * Clicks the button to finishes the secondary activity launched through + * [launchSecondaryActivity], waits for the main activity to resume. + */ + fun finishSecondaryActivity(wmHelper: WindowManagerStateHelper) { + val finishButton = + uiDevice.wait( + Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")), + FIND_TIMEOUT + ) + require(finishButton != null) { "Can't find finish secondary activity button on screen." } + finishButton.click() + wmHelper + .StateSyncBuilder() + .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT) + .waitForAndVerify() + } + + /** * Clicks the button to launch the placeholder primary activity, which should launch the * placeholder secondary activity based on the placeholder rule. */ diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml index 3a02cadc90dd..f0dfdfce035f 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml @@ -20,5 +20,4 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> - </LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml new file mode 100644 index 000000000000..239aba59f4a7 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2023 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:id="@+id/secondary_activity_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <Button + android:id="@+id/finish_secondary_activity_button" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:text="Finish" /> + +</LinearLayout>
\ No newline at end of file diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java index 00f4c2576eb1..6e78750cdeee 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java @@ -16,15 +16,28 @@ package com.android.server.wm.flicker.testapp; +import android.app.Activity; import android.graphics.Color; +import android.os.Bundle; +import android.view.View; /** * Activity to be used as the secondary activity to split with * {@link ActivityEmbeddingMainActivity}. */ -public class ActivityEmbeddingSecondaryActivity extends ActivityEmbeddingBaseActivity { +public class ActivityEmbeddingSecondaryActivity extends Activity { + @Override - int getBackgroundColor() { - return Color.YELLOW; + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_embedding_secondary_activity_layout); + findViewById(R.id.secondary_activity_layout).setBackgroundColor(Color.YELLOW); + findViewById(R.id.finish_secondary_activity_button).setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); } } |