Merge changes from topic "trim_fonts_on_unlock" into udc-dev am: ef3d80a859 am: c58731a7f0 am: 5bd575e2c7
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23167758
Change-Id: I7630895521a3c72ce6200e53ef1f4bade186604b
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 70a2e53..aa947eb 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -3882,7 +3882,9 @@
* 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 @@
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 @@
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 411d157..4851279 100644
--- a/core/java/android/app/Dialog.java
+++ b/core/java/android/app/Dialog.java
@@ -694,12 +694,22 @@
*/
@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 5311b09..baf2a47 100644
--- a/core/java/android/app/servertransaction/TransactionExecutorHelper.java
+++ b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
@@ -193,8 +193,8 @@
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 5c79f69..de982ee 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7486,6 +7486,14 @@
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.
*
@@ -17991,6 +17999,15 @@
"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 d457847..01a99b9 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -2043,7 +2043,10 @@
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 21fe87f..7596459 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -1482,6 +1482,11 @@
}
/** @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 adeb889..6ad1960 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -4703,7 +4703,7 @@
@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 634cbe3..405099d 100644
--- a/core/java/android/widget/Button.java
+++ b/core/java/android/widget/Button.java
@@ -18,6 +18,7 @@
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 @@
@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 e1b0c91..b6c5396c 100644
--- a/core/java/android/widget/ImageButton.java
+++ b/core/java/android/widget/ImageButton.java
@@ -18,6 +18,7 @@
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 @@
@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 f3600b0..edf0f48 100644
--- a/core/java/android/widget/RadialTimePickerView.java
+++ b/core/java/android/widget/RadialTimePickerView.java
@@ -38,6 +38,7 @@
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 @@
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 6c53a44..1317b51 100644
--- a/core/java/android/widget/SimpleMonthView.java
+++ b/core/java/android/widget/SimpleMonthView.java
@@ -39,6 +39,7 @@
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 @@
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 ad431ef..ecc41a5 100644
--- a/core/java/android/widget/Spinner.java
+++ b/core/java/android/widget/Spinner.java
@@ -38,6 +38,7 @@
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 @@
@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 db7d484..7e1e52d 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -9223,18 +9223,20 @@
@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 (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);
+ 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 013d0d3..d6ab8bc 100644
--- a/core/java/com/android/internal/statusbar/IAppClipsService.aidl
+++ b/core/java/com/android/internal/statusbar/IAppClipsService.aidl
@@ -23,4 +23,6 @@
*/
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 4ef9efd..4f905fc 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 d374bdd..c446c3e 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 c1deba3..129de64 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 0000000..a2a6447
--- /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 0000000..8e9e1a5
--- /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 0000000..d491fb0
--- /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 be2c27d..3e0e36d 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 @@
// 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/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 4980e49..bc0b71c 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.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 @@
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 b9d2be2..1169af9 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 @@
);
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 3ab175d..402bb96 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 @@
*/
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 @@
}
/**
+ * 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 @@
*/
@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 91bb155..de7d3ad 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.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 @@
}
}
+ /**
+ * 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 @@
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 @@
}
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(
- WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: switch fullscreen task to freeform on transition" +
- " taskId=%d",
- task.taskId
- )
- return WindowContainerTransaction().also { wct ->
- addMoveToDesktopChanges(wct, task.token)
- }
- }
+ 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
}
+ }
- // 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 handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
+ val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId)
+ if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) {
+ KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
"DesktopTasksController: switch freeform task to fullscreen oon transition" +
- " taskId=%d",
+ " taskId=%d",
task.taskId
- )
- return WindowContainerTransaction().also { wct ->
- addMoveToFullscreenChanges(wct, task.token)
- }
+ )
+ return WindowContainerTransaction().also { wct ->
+ addMoveToFullscreenChanges(wct, task.token)
}
}
return null
}
+ 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 fullscreen task to freeform on transition" +
+ " taskId=%d",
+ task.taskId
+ )
+ 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 @@
@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 @@
) { 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 @@
)
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 899d672..05a6e33 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 @@
/** 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 0000000..39128a8
--- /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 99922fb..c795a05 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 @@
- 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 04fc79a..55e34fe 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 @@
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 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.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 @@
@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 @@
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 @@
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 39fb793..2267c75 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 @@
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 @@
}
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 9fd57d7..15abbf2 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 @@
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 @@
}
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 65b5a7a..58644b2 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 @@
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/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 3bc2f0e..17c0463 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 @@
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 @@
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 @@
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 1335ebf..9ce18db 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 @@
}
@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 @@
}
@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 @@
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 aae30df..a0b3469 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 2502bbf..5398579 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 e85190b..222877b 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 4154029..97016f5 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 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 @@
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 @@
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 @@
// 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 @@
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 @@
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 @@
throw new RuntimeException("Unsupported profile " + deviceProfile);
}
- mPermissionTypes = new ArrayList<>();
-
try {
vendorIcon = getVendorHeaderIcon(this, packageName, userId);
vendorName = getVendorHeaderName(this, packageName, userId);
@@ -486,17 +483,13 @@
}
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 @@
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 @@
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;
+ final Drawable profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile));
- if (!SUPPORTED_PROFILES.contains(deviceProfile)) {
- throw new RuntimeException("Unsupported profile " + deviceProfile);
- }
+ updatePermissionUi();
- 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));
-
- 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 @@
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 @@
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 @@
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 @@
// 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 7aed139..060c032 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
@@ -86,21 +86,11 @@
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 ef4b814..29958ed 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -592,6 +592,7 @@
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,
@@ -726,6 +727,7 @@
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 fe90caf..b661ba4 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 8634c95..1898b97 100644
--- a/packages/SystemUI/docs/device-entry/keyguard.md
+++ b/packages/SystemUI/docs/device-entry/keyguard.md
@@ -20,6 +20,10 @@
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 d662649..afcf846 100644
--- a/packages/SystemUI/docs/device-entry/quickaffordance.md
+++ b/packages/SystemUI/docs/device-entry/quickaffordance.md
@@ -17,7 +17,9 @@
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 b4e1b66..c3651cf 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 511b1a3..a217254 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 e057188..7acfbf6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -140,6 +140,7 @@
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 e54d473..7cce75f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -240,6 +240,7 @@
if (!animate) {
out.setAlpha(0f);
+ out.setVisibility(INVISIBLE);
in.setAlpha(1f);
in.setVisibility(VISIBLE);
mStatusArea.setTranslationY(statusAreaYTranslation);
@@ -255,7 +256,10 @@
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 @@
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 @@
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 d8bf570..3defec7 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -425,10 +425,6 @@
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 @@
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 3c05299..b0a5d7c 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.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 @@
/** 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 64b1c50..bcf8e98 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -365,6 +365,7 @@
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 bf9c3bb..2878df2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
@@ -87,6 +87,11 @@
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 b5e5420..57df33e 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.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 @@
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 @@
@Override
public void onUserInput() {
+ mBouncerMessageInteractor.onPrimaryBouncerUserInput();
mKeyguardFaceAuthInteractor.onPrimaryBouncerUserInput();
mUpdateMonitor.cancelFaceAuth();
}
@@ -207,7 +210,15 @@
}
@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 @@
TelephonyManager telephonyManager,
ViewMediatorCallback viewMediatorCallback,
AudioManager audioManager,
- KeyguardFaceAuthInteractor keyguardFaceAuthInteractor
+ KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
+ BouncerMessageInteractor bouncerMessageInteractor
) {
super(view);
mLockPatternUtils = lockPatternUtils;
@@ -418,6 +430,7 @@
mViewMediatorCallback = viewMediatorCallback;
mAudioManager = audioManager;
mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor;
+ mBouncerMessageInteractor = bouncerMessageInteractor;
}
@Override
@@ -438,6 +451,7 @@
// 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 0be8206..fbb4318 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -1461,14 +1461,6 @@
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 @@
@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 c3bb423..fd3c158 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 @@
}
/**
- * 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 e2b85fa..2a14dc8 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 @@
mModeSwitchesController.setClickListenerDelegate(
displayId -> mHandler.post(() -> {
- showMagnificationSettingsPanel(displayId);
+ toggleSettingsPanelVisibility(displayId);
}));
}
@@ -254,11 +254,11 @@
}
@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 @@
@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 a67f706..7c76588 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 @@
private boolean mAllowDiagonalScrolling = false;
private boolean mEditSizeEnable = false;
+ private boolean mSettingsPanelVisibility = false;
@Nullable
private final MirrorWindowControl mMirrorWindowControl;
@@ -1399,6 +1400,8 @@
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 @@
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 3b1d695..d0ff9f8 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 @@
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 2fa1ef9..ab9bb4e 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 @@
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")
@@ -564,7 +560,7 @@
// 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
@@ -699,8 +695,7 @@
// 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/domain/interactor/KeyguardLongPressInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt
index ea6700e..ca430da 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 @@
class KeyguardLongPressInteractor
@Inject
constructor(
+ @Application private val appContext: Context,
@Application private val scope: CoroutineScope,
transitionInteractor: KeyguardTransitionInteractor,
repository: KeyguardRepository,
@@ -169,7 +172,8 @@
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 8e65c4d..2275337 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 @@
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 @@
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 @@
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 0000000..5b40dd7a
--- /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 c1aefc7..dd1a9d2 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.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 @@
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 @@
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 5818fd0..bbc86c8 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 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.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 @@
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 @@
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 @@
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 @@
}
};
+ 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 @@
ViewConfiguration viewConfiguration,
WindowManager windowManager,
IWindowManager windowManagerService,
+ InputManager inputManager,
Optional<Pip> pipOptional,
Optional<DesktopMode> desktopModeOptional,
FalsingManager falsingManager,
@@ -370,6 +413,7 @@
mContext = context;
mDisplayId = context.getDisplayId();
mMainExecutor = executor;
+ mMainHandler = handler;
mBackgroundExecutor = backgroundExecutor;
mUserTracker = userTracker;
mOverviewProxyService = overviewProxyService;
@@ -381,6 +425,7 @@
mViewConfiguration = viewConfiguration;
mWindowManager = windowManager;
mWindowManagerService = windowManagerService;
+ mInputManager = inputManager;
mPipOptional = pipOptional;
mDesktopModeOptional = desktopModeOptional;
mFalsingManager = falsingManager;
@@ -389,6 +434,8 @@
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 @@
ViewConfiguration.getLongPressTimeout());
mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver(
- mContext.getMainThreadHandler(), mContext, this::onNavigationSettingsChanged);
+ mMainHandler, mContext, this::onNavigationSettingsChanged);
updateCurrentUserResources();
}
@@ -507,6 +554,15 @@
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 @@
mProtoTracer.remove(this);
mOverviewProxyService.removeCallback(mQuickSwitchListener);
mSysUiState.removeCallback(mSysUiStateCallback);
+ mInputManager.unregisterInputDeviceListener(mInputDeviceListener);
updateIsEnabled();
mUserTracker.removeCallback(mUserChangedCallback);
}
@@ -527,7 +584,9 @@
* @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 @@
// 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 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
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 @@
SysUiState sysUiState,
PluginManager pluginManager,
@Main Executor executor,
+ @Main Handler handler,
@Background Executor backgroundExecutor,
UserTracker userTracker,
ProtoTracer protoTracer,
@@ -1261,6 +1334,7 @@
ViewConfiguration viewConfiguration,
WindowManager windowManager,
IWindowManager windowManagerService,
+ InputManager inputManager,
Optional<Pip> pipOptional,
Optional<DesktopMode> desktopModeOptional,
FalsingManager falsingManager,
@@ -1273,6 +1347,7 @@
mSysUiState = sysUiState;
mPluginManager = pluginManager;
mExecutor = executor;
+ mHandler = handler;
mBackgroundExecutor = backgroundExecutor;
mUserTracker = userTracker;
mProtoTracer = protoTracer;
@@ -1281,6 +1356,7 @@
mViewConfiguration = viewConfiguration;
mWindowManager = windowManager;
mWindowManagerService = windowManagerService;
+ mInputManager = inputManager;
mPipOptional = pipOptional;
mDesktopModeOptional = desktopModeOptional;
mFalsingManager = falsingManager;
@@ -1298,6 +1374,7 @@
mSysUiState,
mPluginManager,
mExecutor,
+ mHandler,
mBackgroundExecutor,
mUserTracker,
mProtoTracer,
@@ -1306,6 +1383,7 @@
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 50e8aa7..bf5e442 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 0000000..3e947d9
--- /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 0000000..ec205f8
--- /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 25272ae..7627c0c 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.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 @@
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 @@
}
}
- /** 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 @@
userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) }?.userHandle
?: userTracker.userHandle
} else {
- userTracker.userHandle
+ secureSettings.preferredUser
}
/**
@@ -175,7 +167,19 @@
) {
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 @@
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" }
}
@@ -324,6 +328,16 @@
}
}
+ 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/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index 109cfee..c0e688f 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.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 @@
@[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 8ca13b9..493330a 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 @@
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 3259b0d..0000000
--- 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 afc8bff..7de22b1 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 @@
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 @@
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 83ff020..e0b9f9b 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 @@
/**
* 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 3949492..dce8c81 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.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.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 @@
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 f00803c..6e5cef4 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_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 @@
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;
- mUserTracker = userTracker;
mUiEventLogger = uiEventLogger;
- mUserManager = userManager;
+ 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;
+ mUiEventLogger = uiEventLogger;
+ mBroadcastSender = broadcastSender;
+ mBgExecutor = bgExecutor;
+ mMainExecutor = mainExecutor;
mResultReceiver = createResultReceiver(mainHandler);
}
@@ -127,62 +145,62 @@
return;
}
- if (mUserManager.isManagedProfile()) {
- maybeStartActivityForWPUser();
- finish();
+ mUserHandle = getUser();
+
+ mBgExecutor.execute(() -> {
+ AndroidFuture<Integer> statusCodeFuture = mAppClipsServiceConnector.postForResult(
+ service -> service.canLaunchCaptureContentActivityForNoteInternal(getTaskId()));
+ statusCodeFuture.whenCompleteAsync(this::handleAppClipsStatusCode, mMainExecutor);
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (isFinishing() && mKillAppClipsBroadcastIntent != null) {
+ mBroadcastSender.sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF);
+ }
+
+ super.onDestroy();
+ }
+
+ private void handleAppClipsStatusCode(@CaptureContentForNoteStatusCodes int statusCode,
+ Throwable error) {
+ if (isFinishing()) {
+ // It's too late, trampoline activity is finishing or already finished. Return early.
return;
}
- if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) {
- finish();
+ if (error != null) {
+ Log.d(TAG, "Error querying app clips service", error);
+ setErrorResultAndFinish(statusCode);
return;
}
- if (mOptionalBubbles.isEmpty()) {
- setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
- return;
- }
+ switch (statusCode) {
+ case CAPTURE_CONTENT_FOR_NOTE_SUCCESS:
+ launchAppClipsActivity();
+ break;
- if (!mOptionalBubbles.get().isAppBubbleTaskId(getTaskId())) {
- setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED);
- return;
+ 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);
}
+ }
- if (mDevicePolicyManager.getScreenCaptureDisabled(null)) {
- setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN);
- return;
- }
-
- ComponentName componentName;
- try {
- componentName = ComponentName.unflattenFromString(
+ private void launchAppClipsActivity() {
+ ComponentName componentName = ComponentName.unflattenFromString(
getString(R.string.config_screenshotAppClipsActivityComponent));
- } catch (Resources.NotFoundException e) {
- setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
- return;
- }
-
- if (componentName == null || componentName.getPackageName().isEmpty()
- || componentName.getClassName().isEmpty()) {
- setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
- return;
- }
-
- 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);
- }
-
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 @@
}
}
- @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 @@
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 @@
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 @@
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 2f7644e..f4485c9 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 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.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 @@
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 @@
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 12420ff..f409f80 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.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 @@
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 @@
KeyguardLogger keyguardLogger,
AlternateBouncerInteractor alternateBouncerInteractor,
AlarmManager alarmManager,
- UserTracker userTracker
+ UserTracker userTracker,
+ BouncerMessageInteractor bouncerMessageInteractor
) {
mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
@@ -278,7 +281,7 @@
mScreenLifecycle.addObserver(mScreenObserver);
mAlternateBouncerInteractor = alternateBouncerInteractor;
mUserTracker = userTracker;
-
+ mBouncerMessageInteractor = bouncerMessageInteractor;
mFaceAcquiredMessageDeferral = faceHelpMessageDeferral;
mCoExFaceAcquisitionMsgIdsToShow = new HashSet<>();
int[] msgIds = context.getResources().getIntArray(
@@ -1151,6 +1154,11 @@
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 @@
if (biometricSourceType == FACE) {
mFaceAcquiredMessageDeferral.reset();
}
+ mBouncerMessageInteractor.setFaceAcquisitionMessage(null);
+ mBouncerMessageInteractor.setFingerprintAcquisitionMessage(null);
}
@Override
@@ -1226,6 +1236,8 @@
} else if (biometricSourceType == FINGERPRINT) {
onFingerprintAuthError(msgId, errString);
}
+ mBouncerMessageInteractor.setFaceAcquisitionMessage(null);
+ mBouncerMessageInteractor.setFingerprintAcquisitionMessage(null);
}
private void onFaceAuthError(int msgId, String errString) {
@@ -1310,6 +1322,8 @@
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 b81cb2b..cdc7cee 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 @@
*/
private Set<Integer> mDebugTextUsedYPositions;
private final boolean mDebugRemoveAnimation;
- private final boolean mSimplifiedAppearFraction;
private final boolean mSensitiveRevealAnimEndabled;
private boolean mAnimatedInsets;
@@ -621,7 +620,6 @@
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 @@
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 @@
* 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 080be6d..12da17f 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 254f953..8dc1e8f 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 @@
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 @@
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 @@
// 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 @@
// 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 d2e5a45..e1ba488 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.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 @@
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 665246b..62a176c9 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 @@
@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 9c36af3..31c09b8 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.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 @@
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 @@
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 38ecec0..db58074 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 @@
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 @@
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 9cf988e..8ee7d3e 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.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.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 @@
@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 @@
devicePolicyManager = devicePolicyManager,
dockManager = dockManager,
backgroundDispatcher = testDispatcher,
+ appContext = mContext,
)
underTest.previewManager =
KeyguardRemotePreviewManager(
@@ -216,6 +220,13 @@
)
}
+ @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 dfef947..5de24e4 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 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.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 @@
@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 @@
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 @@
}
@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 @@
) {
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 a75e11a..fb21847 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 @@
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 3336e3b..5d2c3ed 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 @@
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 69d43af..8a36dbc 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 @@
)
val keyguardLongPressInteractor =
KeyguardLongPressInteractor(
+ appContext = mContext,
scope = testScope.backgroundScope,
transitionInteractor =
KeyguardTransitionInteractor(
@@ -240,6 +241,7 @@
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 0000000..450aadd
--- /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 0000000..baac9e0
--- /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 e99f8b6..8b070ef 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.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,26 @@
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.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 +85,7 @@
import org.mockito.MockitoAnnotations
/** atest SystemUITests:NoteTaskControllerTest */
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
internal class NoteTaskControllerTest : SysuiTestCase() {
@@ -97,7 +103,10 @@
@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 +131,7 @@
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 +142,6 @@
context = context,
resolver = resolver,
eventLogger = eventLogger,
- optionalBubbles = Optional.ofNullable(bubbles),
userManager = userManager,
keyguardManager = keyguardManager,
isEnabled = isEnabled,
@@ -141,6 +150,10 @@
roleManager = roleManager,
shortcutManager = shortcutManager,
activityManager = activityManager,
+ secureSettings = secureSettings,
+ noteTaskBubblesController =
+ FakeNoteTaskBubbleController(context, testDispatcher, Optional.ofNullable(bubbles)),
+ applicationScope = testScope,
)
// region onBubbleExpandChanged
@@ -156,7 +169,7 @@
)
verify(eventLogger).logNoteTaskOpened(expectedInfo)
- verifyZeroInteractions(context, bubbles, keyguardManager, userManager)
+ verifyZeroInteractions(bubbles, keyguardManager, userManager)
}
@Test
@@ -171,7 +184,7 @@
)
verify(eventLogger).logNoteTaskClosed(expectedInfo)
- verifyZeroInteractions(context, bubbles, keyguardManager, userManager)
+ verifyZeroInteractions(bubbles, keyguardManager, userManager)
}
@Test
@@ -185,7 +198,7 @@
key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
)
- verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger)
+ verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger)
}
@Test
@@ -199,7 +212,7 @@
key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
)
- verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger)
+ verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger)
}
@Test
@@ -210,7 +223,7 @@
key = "any other key",
)
- verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger)
+ verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger)
}
@Test
@@ -221,7 +234,7 @@
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 +264,44 @@
}
@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 +360,7 @@
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 +369,7 @@
fun showNoteTask_bubblesIsNull_shouldDoNothing() {
createNoteTaskController(bubbles = null).showNoteTask(entryPoint = TAIL_BUTTON)
- verifyZeroInteractions(context, bubbles, eventLogger)
+ verifyZeroInteractions(bubbles, eventLogger)
}
@Test
@@ -330,14 +381,14 @@
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 +397,7 @@
createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON)
- verifyZeroInteractions(context, bubbles, eventLogger)
+ verifyZeroInteractions(bubbles, eventLogger)
}
@Test
@@ -466,7 +517,7 @@
createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE)
- verifyZeroInteractions(context, bubbles, eventLogger)
+ verifyZeroInteractions(bubbles, eventLogger)
}
@Test
@@ -482,7 +533,7 @@
createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE)
- verifyZeroInteractions(context, bubbles, eventLogger)
+ verifyZeroInteractions(bubbles, eventLogger)
}
@Test
@@ -594,7 +645,7 @@
createNoteTaskController(isEnabled = true).onRoleHoldersChanged("NOT_NOTES", user)
- verifyZeroInteractions(context)
+ verify(context, never()).startActivityAsUser(any(), any())
}
@Test
@@ -690,21 +741,6 @@
}
// endregion
- // startregion startNoteTaskProxyActivityForUser
- @Test
- fun startNoteTaskProxyActivityForUser_shouldStartLaunchNoteTaskProxyActivityWithExpectedUser() {
- val user0 = UserHandle.of(0)
- createNoteTaskController().startNoteTaskProxyActivityForUser(user0)
-
- val intentCaptor = argumentCaptor<Intent>()
- verify(context).startActivityAsUser(intentCaptor.capture(), eq(user0))
- assertThat(intentCaptor.value).run {
- hasComponentClass(LaunchNoteTaskManagedProfileProxyActivity::class.java)
- hasFlags(FLAG_ACTIVITY_NEW_TASK)
- }
- }
- // endregion
-
// region getUserForHandlingNotesTaking
@Test
fun getUserForHandlingNotesTaking_cope_quickAffordance_shouldReturnWorkProfileUser() {
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 a0c376f..627c4a8 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.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 @@
class LaunchNoteTaskActivityTest : SysuiTestCase() {
@Mock lateinit var noteTaskController: NoteTaskController
- @Mock lateinit var userManager: UserManager
- private val userTracker: FakeUserTracker = FakeUserTracker()
@Rule
@JvmField
@@ -60,8 +52,6 @@
override fun create(intent: Intent?) =
LaunchNoteTaskActivity(
controller = noteTaskController,
- userManager = userManager,
- userTracker = userTracker
)
},
/* initialTouchMode= */ false,
@@ -71,7 +61,6 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- whenever(userManager.isManagedProfile(eq(workProfileUser.id))).thenReturn(true)
}
@After
@@ -83,36 +72,7 @@
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 6347c34..0000000
--- 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
[email protected]
-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 67b1099..d8897e9 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.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.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 @@
@Mock private Optional<Bubbles> mOptionalBubbles;
@Mock private Bubbles mBubbles;
@Mock private DevicePolicyManager mDevicePolicyManager;
- @Mock private UserManager mUserManager;
private AppClipsService mAppClipsService;
@@ -81,51 +80,84 @@
}
@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 @@
}
@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);
-
- verify(mAppClipsService.mProxyConnectorToMainProfile).postForResult(any());
+ assertThat(getInterfaceWithRealContext()
+ .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
}
- @Test
- public void isManagedProfile_noMainUser_shouldReturnFalse() {
- when(mUserManager.isManagedProfile()).thenReturn(true);
- when(mUserManager.getMainUser()).thenReturn(null);
-
- getInterfaceWithRealContext();
-
- assertThat(mAppClipsService.mProxyConnectorToMainProfile).isNull();
+ private void mockForEmptyBubbles() {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(true);
}
+ 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);
+ }
+
+ 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);
+ }
+
+ 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 @@
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 e9007ff..7fad972 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_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.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.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 @@
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 @@
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 @@
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 @@
}
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 16277de..0393bef 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.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.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.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 @@
@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 @@
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 @@
pulsingGestureListener,
keyguardBouncerViewModel,
keyguardBouncerComponentFactory,
+ mock(KeyguardMessageAreaController.Factory::class.java),
keyguardTransitionInteractor,
primaryBouncerToGoneTransitionViewModel,
featureFlags,
@@ -189,6 +200,10 @@
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 16af208..5ae8ee1 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.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.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.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.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 @@
@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 @@
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 @@
pulsingGestureListener,
keyguardBouncerViewModel,
keyguardBouncerComponentFactory,
+ Mockito.mock(KeyguardMessageAreaController.Factory::class.java),
keyguardTransitionInteractor,
primaryBouncerToGoneTransitionViewModel,
featureFlags,
@@ -200,6 +212,14 @@
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 542e0cb..c810f0cb 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.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 @@
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 0000000..5221468
--- /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 0000000..3854ada
--- /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 662591e..9f3ab88 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.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 @@
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 @@
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 48c346a..61ca0cb 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 @@
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 @@
@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 @@
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 @@
} 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 @@
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 @@
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 @@
}
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 @@
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 @@
}
}
- 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 @@
deviceId, callbackState, isTriggeredByKeyPress);
}
}
+
+ if (DEBUG) {
+ Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel);
+ }
}
private void onKeyboardBacklightListenerDied(int pid) {
@@ -436,10 +434,7 @@
@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 @@
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 @@
}
}
- 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 @@
+ "}";
}
}
+
+ @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 d7eff52..56dcac8 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 @@
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 @@
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 @@
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 @@
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 77e70a2..d0ca8e3 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 @@
// 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 2583514..4a3d0c1 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 @@
}
} 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 ddf96c5..698c9ab 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 @@
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 249ead0..addb521 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 @@
}
}
- 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 @@
// 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 7b10c63..b49c5fb 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 @@
@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 322c11a..7230213 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 @@
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 f0d718a..2f1bf35 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::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 cfeaf0b..2b9a227 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -28,6 +28,7 @@
"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 0000000..7d45ea4
--- /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 64c05dc..2726792 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.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 @@
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 @@
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 @@
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 @@
@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)
+ 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())
+ 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))
+ 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 BRIGHTNESS_VALUE_FOR_LEVEL.size) {
- dataStore.setKeyboardBacklightBrightness(
+ 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",
+ Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
+ lightColorMap[LIGHT_ID]
+ )
+ keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID)
+ }
+ }
+ }
+
+ @Test
+ fun testRestoreBacklightOnInputDeviceChanged() {
+ 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()
+ )
+
+ `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() {
+ 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)
+
+ 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 " + (totalLevels - 1),
+ (totalLevels - 1),
+ lastBacklightState!!.maxBrightnessLevel
+ )
+ assertEquals(
+ "Backlight state isTriggeredByKeyPress should be true",
+ true,
+ lastBacklightState!!.isTriggeredByKeyPress
+ )
+
+ // Unregister listener
+ keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0)
+
+ lastBacklightState = null
+ incrementKeyboardBacklight(DEVICE_ID)
+
+ assertNull("Listener should not receive any updates", lastBacklightState)
+ }
+ }
+
+ @Test
+ fun testKeyboardBacklight_userActivity() {
+ 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(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
- lightColorMap[LIGHT_ID]
+ "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.onInputDeviceRemoved(DEVICE_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 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
- )
-
- 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]
- )
- }
-
- @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)
-
- // Register backlight listener
- val listener = KeyboardBacklightListener()
- keyboardBacklightController.registerKeyboardBacklightListener(listener, 0)
-
- 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
- )
-
- // Unregister listener
- keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0)
-
- lastBacklightState = null
- incrementKeyboardBacklight(DEVICE_ID)
-
- 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
- )
-
- 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]
- )
- }
-
- @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 @@
)
}
+ @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 @@
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 676bfb0..2015ae9 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 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 @@
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 0000000..fe8017e
--- /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 d383024..af48cbd 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.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.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.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 @@
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 @@
@Mock private StatusBarManagerInternal mStatusBarManagerInternal;
+ @Mock private KeyguardServiceDelegate mKeyguardServiceDelegate;
+
private StaticMockitoSession mMockitoSession;
private HandlerThread mHandlerThread;
private Handler mHandler;
@@ -151,6 +161,10 @@
Supplier<GlobalActions> getGlobalActionsFactory() {
return () -> mGlobalActions;
}
+
+ KeyguardServiceDelegate getKeyguardServiceDelegate() {
+ return mKeyguardServiceDelegate;
+ }
}
TestPhoneWindowManager(Context context) {
@@ -158,12 +172,12 @@
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 @@
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 @@
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 @@
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 cb984f8..77e944f 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 @@
// 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 @@
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 a8fc25f..204cbf7 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 @@
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 d1d83f6..8662607 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 @@
// 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 @@
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 0000000..c0c738b
--- /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)
[email protected](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 0000000..39ae8e2
--- /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)
[email protected](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 daecfe7..e019b2b 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 @@
}
/**
+ * 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 3a02cad..f0dfdfc 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 0000000..239aba5
--- /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 00f4c25..6e78750 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();
+ }
+ });
}
}