Merge "Fix relaunch of freeform tasks outside desktop" into main
diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
index a3beaf4..209f323 100644
--- a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
+++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
@@ -216,7 +216,11 @@
oldConfig.getLayoutDirection() != mConfiguration.getLayoutDirection();
if (densityChange || dirChange) {
- mImeSwitcherIcon = getDrawable(com.android.internal.R.drawable.ic_ime_switcher);
+ final int switcherResId = Flags.imeSwitcherRevamp()
+ ? com.android.internal.R.drawable.ic_ime_switcher_new
+ : com.android.internal.R.drawable.ic_ime_switcher;
+
+ mImeSwitcherIcon = getDrawable(switcherResId);
}
if (orientationChange || densityChange || dirChange) {
mBackIcon = getBackDrawable();
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 82a7e16..a23e383 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -30,6 +30,7 @@
import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED;
+import static android.view.accessibility.Flags.removeChildHoverCheckForTouchExploration;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_MISSING_WINDOW;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN;
@@ -17486,9 +17487,9 @@
* Dispatching hover events to {@link TouchDelegate} to improve accessibility.
* <p>
* This method is dispatching hover events to the delegate target to support explore by touch.
- * Similar to {@link ViewGroup#dispatchTouchEvent}, this method send proper hover events to
+ * Similar to {@link ViewGroup#dispatchTouchEvent}, this method sends proper hover events to
* the delegate target according to the pointer and the touch area of the delegate while touch
- * exploration enabled.
+ * exploration is enabled.
* </p>
*
* @param event The motion event dispatch to the delegate target.
@@ -17520,17 +17521,33 @@
// hover events but receive accessibility focus, it should also not delegate to these
// views when hovered.
if (!oldHoveringTouchDelegate) {
- if ((action == MotionEvent.ACTION_HOVER_ENTER
- || action == MotionEvent.ACTION_HOVER_MOVE)
- && !pointInHoveredChild(event)
- && pointInDelegateRegion) {
- mHoveringTouchDelegate = true;
+ if (removeChildHoverCheckForTouchExploration()) {
+ if ((action == MotionEvent.ACTION_HOVER_ENTER
+ || action == MotionEvent.ACTION_HOVER_MOVE) && pointInDelegateRegion) {
+ mHoveringTouchDelegate = true;
+ }
+ } else {
+ if ((action == MotionEvent.ACTION_HOVER_ENTER
+ || action == MotionEvent.ACTION_HOVER_MOVE)
+ && !pointInHoveredChild(event)
+ && pointInDelegateRegion) {
+ mHoveringTouchDelegate = true;
+ }
}
} else {
- if (action == MotionEvent.ACTION_HOVER_EXIT
- || (action == MotionEvent.ACTION_HOVER_MOVE
+ if (removeChildHoverCheckForTouchExploration()) {
+ if (action == MotionEvent.ACTION_HOVER_EXIT
+ || (action == MotionEvent.ACTION_HOVER_MOVE)) {
+ if (!pointInDelegateRegion) {
+ mHoveringTouchDelegate = false;
+ }
+ }
+ } else {
+ if (action == MotionEvent.ACTION_HOVER_EXIT
+ || (action == MotionEvent.ACTION_HOVER_MOVE
&& (pointInHoveredChild(event) || !pointInDelegateRegion))) {
- mHoveringTouchDelegate = false;
+ mHoveringTouchDelegate = false;
+ }
}
}
switch (action) {
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index d0bc57b..44c1acc 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -128,6 +128,13 @@
}
flag {
+ namespace: "accessibility"
+ name: "remove_child_hover_check_for_touch_exploration"
+ description: "Remove a check for a hovered child that prevents touch events from being delegated to non-direct descendants"
+ bug: "304770837"
+}
+
+flag {
name: "skip_accessibility_warning_dialog_for_trusted_services"
namespace: "accessibility"
description: "Skips showing the accessibility warning dialog for trusted services."
diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java
index 098f655..0e66f7a 100644
--- a/core/java/android/view/inputmethod/InputMethodInfo.java
+++ b/core/java/android/view/inputmethod/InputMethodInfo.java
@@ -891,12 +891,13 @@
@FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API)
@Nullable
public Intent createImeLanguageSettingsActivityIntent() {
- if (TextUtils.isEmpty(mLanguageSettingsActivityName)) {
+ final var activityName = !TextUtils.isEmpty(mLanguageSettingsActivityName)
+ ? mLanguageSettingsActivityName : mSettingsActivityName;
+ if (TextUtils.isEmpty(activityName)) {
return null;
}
return new Intent(ACTION_IME_LANGUAGE_SETTINGS).setComponent(
- new ComponentName(getServiceInfo().packageName,
- mLanguageSettingsActivityName)
+ new ComponentName(getServiceInfo().packageName, activityName)
);
}
diff --git a/core/java/com/android/internal/widget/MaxHeightFrameLayout.java b/core/java/com/android/internal/widget/MaxHeightFrameLayout.java
new file mode 100644
index 0000000..d65dddd
--- /dev/null
+++ b/core/java/com/android/internal/widget/MaxHeightFrameLayout.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Px;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import com.android.internal.R;
+
+/**
+ * This custom subclass of FrameLayout enforces that its calculated height be no larger than the
+ * given maximum height (if any).
+ *
+ * @hide
+ */
+public class MaxHeightFrameLayout extends FrameLayout {
+
+ private int mMaxHeight = Integer.MAX_VALUE;
+
+ public MaxHeightFrameLayout(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public MaxHeightFrameLayout(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.MaxHeightFrameLayout, defStyleAttr, defStyleRes);
+ saveAttributeDataForStyleable(context, R.styleable.MaxHeightFrameLayout,
+ attrs, a, defStyleAttr, defStyleRes);
+
+ setMaxHeight(a.getDimensionPixelSize(R.styleable.MaxHeightFrameLayout_maxHeight,
+ Integer.MAX_VALUE));
+ }
+
+ /**
+ * Gets the maximum height of this view, in pixels.
+ *
+ * @see #setMaxHeight(int)
+ *
+ * @attr ref android.R.styleable#MaxHeightFrameLayout_maxHeight
+ */
+ @Px
+ public int getMaxHeight() {
+ return mMaxHeight;
+ }
+
+ /**
+ * Sets the maximum height this view can have.
+ *
+ * @param maxHeight the maximum height, in pixels
+ *
+ * @see #getMaxHeight()
+ *
+ * @attr ref android.R.styleable#MaxHeightFrameLayout_maxHeight
+ */
+ public void setMaxHeight(@Px int maxHeight) {
+ mMaxHeight = maxHeight;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (MeasureSpec.getSize(heightMeasureSpec) > mMaxHeight) {
+ final int mode = MeasureSpec.getMode(heightMeasureSpec);
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, mode);
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp
index 30c926c..7e2a5ac 100644
--- a/core/jni/platform/host/HostRuntime.cpp
+++ b/core/jni/platform/host/HostRuntime.cpp
@@ -17,6 +17,8 @@
#include <android-base/logging.h>
#include <android-base/properties.h>
#include <android/graphics/jni_runtime.h>
+#include <android_runtime/AndroidRuntime.h>
+#include <jni_wrappers.h>
#include <nativehelper/JNIHelp.h>
#include <nativehelper/jni_macros.h>
#include <unicode/putil.h>
@@ -27,9 +29,6 @@
#include <unordered_map>
#include <vector>
-#include "android_view_InputDevice.h"
-#include "core_jni_helpers.h"
-#include "jni.h"
#ifdef _WIN32
#include <windows.h>
#else
@@ -38,8 +37,6 @@
#include <sys/stat.h>
#endif
-#include <iostream>
-
using namespace std;
/*
@@ -49,12 +46,6 @@
* (see AndroidRuntime.cpp).
*/
-static JavaVM* javaVM;
-static jclass bridge;
-static jclass layoutLog;
-static jmethodID getLogId;
-static jmethodID logMethodId;
-
extern int register_android_os_Binder(JNIEnv* env);
extern int register_libcore_util_NativeAllocationRegistry_Delegate(JNIEnv* env);
@@ -168,28 +159,9 @@
}
}
- if (register_android_graphics_classes(env) < 0) {
- return -1;
- }
-
return 0;
}
-int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className,
- const JNINativeMethod* gMethods, int numMethods) {
- return jniRegisterNativeMethods(env, className, gMethods, numMethods);
-}
-
-JNIEnv* AndroidRuntime::getJNIEnv() {
- JNIEnv* env;
- if (javaVM->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) return nullptr;
- return env;
-}
-
-JavaVM* AndroidRuntime::getJavaVM() {
- return javaVM;
-}
-
static vector<string> parseCsv(const string& csvString) {
vector<string> result;
istringstream stream(csvString);
@@ -200,29 +172,6 @@
return result;
}
-void LayoutlibLogger(base::LogId, base::LogSeverity severity, const char* tag, const char* file,
- unsigned int line, const char* message) {
- JNIEnv* env = AndroidRuntime::getJNIEnv();
- jint logPrio = severity;
- jstring tagString = env->NewStringUTF(tag);
- jstring messageString = env->NewStringUTF(message);
-
- jobject bridgeLog = env->CallStaticObjectMethod(bridge, getLogId);
-
- env->CallVoidMethod(bridgeLog, logMethodId, logPrio, tagString, messageString);
-
- env->DeleteLocalRef(tagString);
- env->DeleteLocalRef(messageString);
- env->DeleteLocalRef(bridgeLog);
-}
-
-void LayoutlibAborter(const char* abort_message) {
- // Layoutlib should not call abort() as it would terminate Studio.
- // Throw an exception back to Java instead.
- JNIEnv* env = AndroidRuntime::getJNIEnv();
- jniThrowRuntimeException(env, "The Android framework has encountered a fatal error");
-}
-
// This method has been copied/adapted from system/core/init/property_service.cpp
// If the ro.product.cpu.abilist* properties have not been explicitly
// set, derive them from ro.system.product.cpu.abilist* properties.
@@ -311,62 +260,49 @@
#endif
}
-static bool init_icu(const char* dataPath) {
- void* addr = mmapFile(dataPath);
- UErrorCode err = U_ZERO_ERROR;
- udata_setCommonData(addr, &err);
- if (err != U_ZERO_ERROR) {
- return false;
- }
- return true;
-}
-
-// Creates an array of InputDevice from key character map files
-static void init_keyboard(JNIEnv* env, const vector<string>& keyboardPaths) {
- jclass inputDevice = FindClassOrDie(env, "android/view/InputDevice");
- jobjectArray inputDevicesArray =
- env->NewObjectArray(keyboardPaths.size(), inputDevice, nullptr);
- int keyboardId = 1;
-
- for (const string& path : keyboardPaths) {
- base::Result<std::shared_ptr<KeyCharacterMap>> charMap =
- KeyCharacterMap::load(path, KeyCharacterMap::Format::BASE);
-
- InputDeviceInfo info = InputDeviceInfo();
- info.initialize(keyboardId, 0, 0, InputDeviceIdentifier(),
- "keyboard " + std::to_string(keyboardId), true, false,
- ui::LogicalDisplayId::DEFAULT);
- info.setKeyboardType(AINPUT_KEYBOARD_TYPE_ALPHABETIC);
- info.setKeyCharacterMap(*charMap);
-
- jobject inputDeviceObj = android_view_InputDevice_create(env, info);
- if (inputDeviceObj) {
- env->SetObjectArrayElement(inputDevicesArray, keyboardId - 1, inputDeviceObj);
- env->DeleteLocalRef(inputDeviceObj);
+// Loads the ICU data file from the location specified in the system property ro.icu.data.path
+static void loadIcuData() {
+ string icuPath = base::GetProperty("ro.icu.data.path", "");
+ if (!icuPath.empty()) {
+ // Set the location of ICU data
+ void* addr = mmapFile(icuPath.c_str());
+ UErrorCode err = U_ZERO_ERROR;
+ udata_setCommonData(addr, &err);
+ if (err != U_ZERO_ERROR) {
+ ALOGE("Unable to load ICU data\n");
}
- keyboardId++;
}
-
- if (bridge == nullptr) {
- bridge = FindClassOrDie(env, "com/android/layoutlib/bridge/Bridge");
- bridge = MakeGlobalRefOrDie(env, bridge);
- }
- jmethodID setInputManager = GetStaticMethodIDOrDie(env, bridge, "setInputManager",
- "([Landroid/view/InputDevice;)V");
- env->CallStaticVoidMethod(bridge, setInputManager, inputDevicesArray);
- env->DeleteLocalRef(inputDevicesArray);
}
-} // namespace android
+static int register_android_core_classes(JNIEnv* env) {
+ jclass system = FindClassOrDie(env, "java/lang/System");
+ jmethodID getPropertyMethod =
+ GetStaticMethodIDOrDie(env, system, "getProperty",
+ "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
-using namespace android;
+ // Get the names of classes that need to register their native methods
+ auto nativesClassesJString =
+ (jstring)env->CallStaticObjectMethod(system, getPropertyMethod,
+ env->NewStringUTF("core_native_classes"),
+ env->NewStringUTF(""));
+ const char* nativesClassesArray = env->GetStringUTFChars(nativesClassesJString, nullptr);
+ string nativesClassesString(nativesClassesArray);
+ vector<string> classesToRegister = parseCsv(nativesClassesString);
+ env->ReleaseStringUTFChars(nativesClassesJString, nativesClassesArray);
+
+ if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) {
+ return JNI_ERR;
+ }
+
+ return 0;
+}
// Called right before aborting by LOG_ALWAYS_FATAL. Print the pending exception.
void abort_handler(const char* abort_message) {
ALOGE("About to abort the process...");
- JNIEnv* env = NULL;
- if (javaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ JNIEnv* env = AndroidRuntime::getJNIEnv();
+ if (env == nullptr) {
ALOGE("vm->GetEnv() failed");
return;
}
@@ -377,107 +313,98 @@
ALOGE("Aborting because: %s", abort_message);
}
+// ------------------ Host implementation of AndroidRuntime ------------------
+
+/*static*/ JavaVM* AndroidRuntime::mJavaVM;
+
+/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className,
+ const JNINativeMethod* gMethods,
+ int numMethods) {
+ return jniRegisterNativeMethods(env, className, gMethods, numMethods);
+}
+
+/*static*/ JNIEnv* AndroidRuntime::getJNIEnv() {
+ JNIEnv* env;
+ JavaVM* vm = AndroidRuntime::getJavaVM();
+ if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
+ return nullptr;
+ }
+ return env;
+}
+
+/*static*/ JavaVM* AndroidRuntime::getJavaVM() {
+ return mJavaVM;
+}
+
+/*static*/ int AndroidRuntime::startReg(JNIEnv* env) {
+ if (register_android_core_classes(env) < 0) {
+ return JNI_ERR;
+ }
+ if (register_android_graphics_classes(env) < 0) {
+ return JNI_ERR;
+ }
+ return 0;
+}
+
+void AndroidRuntime::onVmCreated(JNIEnv* env) {
+ env->GetJavaVM(&mJavaVM);
+}
+
+void AndroidRuntime::onStarted() {
+ property_initialize_ro_cpu_abilist();
+ loadIcuData();
+
+ // Use English locale for number format to ensure correct parsing of floats when using strtof
+ setlocale(LC_NUMERIC, "en_US.UTF-8");
+}
+
+void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote) {
+ JNIEnv* env = AndroidRuntime::getJNIEnv();
+ // Register native functions.
+ if (startReg(env) < 0) {
+ ALOGE("Unable to register all android native methods\n");
+ }
+ onStarted();
+}
+
+AndroidRuntime::AndroidRuntime(char* argBlockStart, const size_t argBlockLength)
+ : mExitWithoutCleanup(false), mArgBlockStart(argBlockStart), mArgBlockLength(argBlockLength) {
+ init_android_graphics();
+}
+
+AndroidRuntime::~AndroidRuntime() {}
+
+// Version of AndroidRuntime to run on host
+class HostRuntime : public AndroidRuntime {
+public:
+ HostRuntime() : AndroidRuntime(nullptr, 0) {}
+
+ void onVmCreated(JNIEnv* env) override {
+ AndroidRuntime::onVmCreated(env);
+ // initialize logging, so ANDROD_LOG_TAGS env variable is respected
+ android::base::InitLogging(nullptr, android::base::StderrLogger, abort_handler);
+ }
+
+ void onStarted() override {
+ AndroidRuntime::onStarted();
+ }
+};
+
+} // namespace android
+
+using namespace android;
+
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
- javaVM = vm;
JNIEnv* env = nullptr;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
- __android_log_set_aborter(abort_handler);
+ Vector<String8> args;
+ HostRuntime runtime;
- init_android_graphics();
-
- // Configuration is stored as java System properties.
- // Get a reference to System.getProperty
- jclass system = FindClassOrDie(env, "java/lang/System");
- jmethodID getPropertyMethod =
- GetStaticMethodIDOrDie(env, system, "getProperty",
- "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
-
- // Java system properties that contain LayoutLib config. The initial values in the map
- // are the default values if the property is not specified.
- std::unordered_map<std::string, std::string> systemProperties =
- {{"core_native_classes", ""},
- {"register_properties_during_load", ""},
- {"icu.data.path", ""},
- {"use_bridge_for_logging", ""},
- {"keyboard_paths", ""}};
-
- for (auto& [name, defaultValue] : systemProperties) {
- jstring propertyString =
- (jstring)env->CallStaticObjectMethod(system, getPropertyMethod,
- env->NewStringUTF(name.c_str()),
- env->NewStringUTF(defaultValue.c_str()));
- const char* propertyChars = env->GetStringUTFChars(propertyString, 0);
- systemProperties[name] = string(propertyChars);
- env->ReleaseStringUTFChars(propertyString, propertyChars);
- }
- // Get the names of classes that need to register their native methods
- vector<string> classesToRegister = parseCsv(systemProperties["core_native_classes"]);
-
- if (systemProperties["register_properties_during_load"] == "true") {
- // Set the system properties first as they could be used in the static initialization of
- // other classes
- if (register_android_os_SystemProperties(env) < 0) {
- return JNI_ERR;
- }
- classesToRegister.erase(find(classesToRegister.begin(), classesToRegister.end(),
- "android.os.SystemProperties"));
- bridge = FindClassOrDie(env, "com/android/layoutlib/bridge/Bridge");
- bridge = MakeGlobalRefOrDie(env, bridge);
- jmethodID setSystemPropertiesMethod =
- GetStaticMethodIDOrDie(env, bridge, "setSystemProperties", "()V");
- env->CallStaticVoidMethod(bridge, setSystemPropertiesMethod);
- property_initialize_ro_cpu_abilist();
- }
-
- if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) {
- return JNI_ERR;
- }
-
- if (!systemProperties["icu.data.path"].empty()) {
- // Set the location of ICU data
- bool icuInitialized = init_icu(systemProperties["icu.data.path"].c_str());
- if (!icuInitialized) {
- return JNI_ERR;
- }
- }
-
- if (systemProperties["use_bridge_for_logging"] == "true") {
- layoutLog = FindClassOrDie(env, "com/android/ide/common/rendering/api/ILayoutLog");
- layoutLog = MakeGlobalRefOrDie(env, layoutLog);
- logMethodId = GetMethodIDOrDie(env, layoutLog, "logAndroidFramework",
- "(ILjava/lang/String;Ljava/lang/String;)V");
- if (bridge == nullptr) {
- bridge = FindClassOrDie(env, "com/android/layoutlib/bridge/Bridge");
- bridge = MakeGlobalRefOrDie(env, bridge);
- }
- getLogId = GetStaticMethodIDOrDie(env, bridge, "getLog",
- "()Lcom/android/ide/common/rendering/api/ILayoutLog;");
- android::base::SetLogger(LayoutlibLogger);
- android::base::SetAborter(LayoutlibAborter);
- } else {
- // initialize logging, so ANDROD_LOG_TAGS env variable is respected
- android::base::InitLogging(nullptr, android::base::StderrLogger);
- }
-
- // Use English locale for number format to ensure correct parsing of floats when using strtof
- setlocale(LC_NUMERIC, "en_US.UTF-8");
-
- if (!systemProperties["keyboard_paths"].empty()) {
- vector<string> keyboardPaths = parseCsv(systemProperties["keyboard_paths"]);
- init_keyboard(env, keyboardPaths);
- } else {
- fprintf(stderr, "Skip initializing keyboard\n");
- }
+ runtime.onVmCreated(env);
+ runtime.start("HostRuntime", args, false);
return JNI_VERSION_1_6;
}
-
-JNIEXPORT void JNI_OnUnload(JavaVM* vm, void*) {
- JNIEnv* env = nullptr;
- vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
- env->DeleteGlobalRef(bridge);
- env->DeleteGlobalRef(layoutLog);
-}
diff --git a/core/res/res/drawable/ic_ime_switcher_new.xml b/core/res/res/drawable/ic_ime_switcher_new.xml
new file mode 100644
index 0000000..04f4a25
--- /dev/null
+++ b/core/res/res/drawable/ic_ime_switcher_new.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z"
+ android:fillColor="#FFFFFFFF"/>
+</vector>
diff --git a/core/res/res/drawable/input_method_switch_button.xml b/core/res/res/drawable/input_method_switch_button.xml
new file mode 100644
index 0000000..396d81e
--- /dev/null
+++ b/core/res/res/drawable/input_method_switch_button.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 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.
+-->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:insetTop="6dp"
+ android:insetBottom="6dp">
+ <ripple android:color="?android:attr/colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners android:radius="28dp"/>
+ <solid android:color="@color/white"/>
+ </shape>
+ </item>
+
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="28dp"/>
+ <solid android:color="@color/transparent"/>
+ <stroke android:color="?attr/materialColorPrimary"
+ android:width="1dp"/>
+ <padding android:left="16dp"
+ android:top="8dp"
+ android:right="16dp"
+ android:bottom="8dp"/>
+ </shape>
+ </item>
+ </ripple>
+</inset>
diff --git a/core/res/res/drawable/input_method_switch_item_background.xml b/core/res/res/drawable/input_method_switch_item_background.xml
new file mode 100644
index 0000000..eb7a246
--- /dev/null
+++ b/core/res/res/drawable/input_method_switch_item_background.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 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.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/list_highlight_material">
+ <item android:id="@id/mask">
+ <shape android:shape="rectangle">
+ <corners android:radius="28dp"/>
+ <solid android:color="@color/white"/>
+ </shape>
+ </item>
+
+ <item>
+ <selector>
+ <item android:state_activated="true">
+ <shape android:shape="rectangle">
+ <corners android:radius="28dp"/>
+ <solid android:color="?attr/materialColorSecondaryContainer"/>
+ </shape>
+ </item>
+ </selector>
+ </item>
+</ripple>
diff --git a/core/res/res/layout/input_method_switch_dialog_new.xml b/core/res/res/layout/input_method_switch_dialog_new.xml
new file mode 100644
index 0000000..5a4d6b1
--- /dev/null
+++ b/core/res/res/layout/input_method_switch_dialog_new.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <com.android.internal.widget.MaxHeightFrameLayout
+ android:layout_width="320dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:maxHeight="373dp">
+
+ <com.android.internal.widget.RecyclerView
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="8dp"
+ android:clipToPadding="false"
+ android:layoutManager="com.android.internal.widget.LinearLayoutManager"/>
+
+ </com.android.internal.widget.MaxHeightFrameLayout>
+
+ <LinearLayout
+ style="?android:attr/buttonBarStyle"
+ android:id="@+id/button_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingHorizontal="16dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="16dp"
+ android:visibility="gone">
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"/>
+
+ <Button
+ style="?attr/buttonBarButtonStyle"
+ android:id="@+id/button1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/input_method_switch_button"
+ android:layout_gravity="end"
+ android:text="@string/input_method_language_settings"
+ android:fontFamily="google-sans-text"
+ android:textAppearance="?attr/textAppearance"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/core/res/res/layout/input_method_switch_item_new.xml b/core/res/res/layout/input_method_switch_item_new.xml
new file mode 100644
index 0000000..16a97c4
--- /dev/null
+++ b/core/res/res/layout/input_method_switch_item_new.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingHorizontal="16dp"
+ android:paddingBottom="8dp">
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?attr/materialColorSurfaceVariant"
+ android:layout_marginStart="20dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="24dp"
+ android:layout_marginBottom="12dp"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/header_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:fontFamily="google-sans-text"
+ android:textAppearance="?attr/textAppearance"
+ android:textColor="?attr/materialColorPrimary"
+ android:visibility="gone"/>
+
+ <LinearLayout
+ android:id="@+id/list_item"
+ android:layout_width="match_parent"
+ android:layout_height="72dp"
+ android:background="@drawable/input_method_switch_item_background"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:paddingStart="20dp"
+ android:paddingEnd="24dp">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="start|center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:fontFamily="google-sans-text"
+ android:textAppearance="?attr/textAppearanceListItem"/>
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:gravity="center_vertical"
+ android:layout_marginStart="12dp"
+ android:src="@drawable/ic_check_24dp"
+ android:tint="?attr/materialColorOnSurface"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 0975eda..7cc9e13 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5243,6 +5243,11 @@
the VISIBLE or INVISIBLE state when measuring. Defaults to false. -->
<attr name="measureAllChildren" format="boolean" />
</declare-styleable>
+ <!-- @hide -->
+ <declare-styleable name="MaxHeightFrameLayout">
+ <!-- An optional argument to supply a maximum height for this view. -->
+ <attr name="maxHeight" format="dimension" />
+ </declare-styleable>
<declare-styleable name="ExpandableListView">
<!-- Indicator shown beside the group View. This can be a stateful Drawable. -->
<attr name="groupIndicator" format="reference" />
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 46b15416..ec865f6 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3880,6 +3880,8 @@
<!-- Title of the pop-up dialog in which the user switches keyboard, also known as input method. -->
<string name="select_input_method">Choose input method</string>
+ <!-- Button to access the language settings of the current input method. [CHAR LIMIT=50]-->
+ <string name="input_method_language_settings">Language Settings</string>
<!-- Summary text of a toggle switch to enable/disable use of the IME while a physical
keyboard is connected -->
<string name="show_ime">Keep it on screen while physical keyboard is active</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c50b961..fcafdae 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1577,6 +1577,8 @@
<java-symbol type="layout" name="input_method" />
<java-symbol type="layout" name="input_method_extract_view" />
<java-symbol type="layout" name="input_method_switch_item" />
+ <java-symbol type="layout" name="input_method_switch_item_new" />
+ <java-symbol type="layout" name="input_method_switch_dialog_new" />
<java-symbol type="layout" name="input_method_switch_dialog_title" />
<java-symbol type="layout" name="js_prompt" />
<java-symbol type="layout" name="list_content_simple" />
@@ -2552,6 +2554,7 @@
<java-symbol type="dimen" name="input_method_nav_key_button_ripple_max_width" />
<java-symbol type="drawable" name="ic_ime_nav_back" />
<java-symbol type="drawable" name="ic_ime_switcher" />
+ <java-symbol type="drawable" name="ic_ime_switcher_new" />
<java-symbol type="id" name="input_method_nav_back" />
<java-symbol type="id" name="input_method_nav_buttons" />
<java-symbol type="id" name="input_method_nav_center_group" />
@@ -5400,6 +5403,7 @@
<java-symbol type="style" name="Theme.DeviceDefault.DialogWhenLarge" />
<java-symbol type="style" name="Theme.DeviceDefault.DocumentsUI" />
<java-symbol type="style" name="Theme.DeviceDefault.InputMethod" />
+ <java-symbol type="style" name="Theme.DeviceDefault.InputMethodSwitcherDialog" />
<java-symbol type="style" name="Theme.DeviceDefault.Light.DarkActionBar" />
<java-symbol type="style" name="Theme.DeviceDefault.Light.Dialog.FixedSize" />
<java-symbol type="style" name="Theme.DeviceDefault.Light.Dialog.MinWidth" />
diff --git a/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml
index 382ff04..f5c6738 100644
--- a/core/res/res/values/themes_device_defaults.xml
+++ b/core/res/res/values/themes_device_defaults.xml
@@ -6179,4 +6179,10 @@
<item name="colorListDivider">@color/list_divider_opacity_device_default_light</item>
<item name="opacityListDivider">@color/list_divider_opacity_device_default_light</item>
</style>
+
+ <!-- Device default theme for the Input Method Switcher dialog. -->
+ <style name="Theme.DeviceDefault.InputMethodSwitcherDialog" parent="Theme.DeviceDefault.Dialog.Alert.DayNight">
+ <item name="windowMinWidthMajor">@null</item>
+ <item name="windowMinWidthMinor">@null</item>
+ </style>
</resources>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt
index 235b9bf..fc3dc14 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt
@@ -168,6 +168,16 @@
}
}
+ /** Whether any animation is currently running. */
+ @JvmStatic
+ fun isAnyAnimationRunning(): Boolean {
+ for (target in allAnimatedObjects) {
+ val animator = PhysicsAnimator.getInstance(target)
+ if (animator.isRunning()) return true
+ }
+ return false
+ }
+
/**
* Blocks the calling thread until the first animation frame in which predicate returns true. If
* the given object isn't animating, returns without blocking.
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 8e2f7c1..97a45fb 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1196,4 +1196,14 @@
metadata {
purpose: PURPOSE_BUGFIX
}
+}
+
+flag {
+ name: "sim_pin_talkback_fix_for_double_submit"
+ namespace: "systemui"
+ description: "The SIM PIN entry screens show the wrong message due"
+ bug: "346932439"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 20b1303..78ba7de 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -64,6 +64,7 @@
internal val orientation: Orientation,
internal val coroutineScope: CoroutineScope,
) : DraggableHandler {
+ internal val nestedScrollKey = Any()
/** The [DraggableHandler] can only have one active [DragController] at a time. */
private var dragController: DragControllerImpl? = null
@@ -912,9 +913,9 @@
internal class NestedScrollHandlerImpl(
private val layoutImpl: SceneTransitionLayoutImpl,
private val orientation: Orientation,
- private val topOrLeftBehavior: NestedScrollBehavior,
- private val bottomOrRightBehavior: NestedScrollBehavior,
- private val isExternalOverscrollGesture: () -> Boolean,
+ internal var topOrLeftBehavior: NestedScrollBehavior,
+ internal var bottomOrRightBehavior: NestedScrollBehavior,
+ internal var isExternalOverscrollGesture: () -> Boolean,
private val pointersInfoOwner: PointersInfoOwner,
) {
private val layoutState = layoutImpl.state
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 615d393..2b78b5a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -41,7 +41,6 @@
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.PointerInputModifierNode
-import androidx.compose.ui.node.TraversableNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.findNearestAncestor
import androidx.compose.ui.node.observeReads
@@ -139,16 +138,12 @@
DelegatingNode(),
PointerInputModifierNode,
CompositionLocalConsumerModifierNode,
- TraversableNode,
- PointersInfoOwner,
ObserverModifierNode {
private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() }
private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler))
private val velocityTracker = VelocityTracker()
private var previousEnabled: Boolean = false
- override val traverseKey: Any = TRAVERSE_KEY
-
var enabled: () -> Boolean = enabled
set(value) {
// Reset the pointer input whenever enabled changed.
@@ -208,7 +203,7 @@
private var startedPosition: Offset? = null
private var pointersDown: Int = 0
- override fun pointersInfo(): PointersInfo {
+ internal fun pointersInfo(): PointersInfo {
return PointersInfo(
startedPosition = startedPosition,
// Note: We could have 0 pointers during fling or for other reasons.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
index ddff2f7..945043d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
@@ -18,12 +18,13 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
-import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
-import com.android.compose.nestedscroll.PriorityNestedScrollConnection
/**
* Defines the behavior of the [SceneTransitionLayout] when a scrollable component is scrolled.
@@ -67,7 +68,11 @@
* In addition, during scene transitions, scroll events are consumed by the
* [SceneTransitionLayout] instead of the scrollable component.
*/
- EdgeAlways(canStartOnPostFling = true),
+ EdgeAlways(canStartOnPostFling = true);
+
+ companion object {
+ val Default = EdgeNoPreview
+ }
}
internal fun Modifier.nestedScrollToScene(
@@ -122,37 +127,60 @@
}
private class NestedScrollToSceneNode(
- layoutImpl: SceneTransitionLayoutImpl,
- orientation: Orientation,
- topOrLeftBehavior: NestedScrollBehavior,
- bottomOrRightBehavior: NestedScrollBehavior,
- isExternalOverscrollGesture: () -> Boolean,
+ private var layoutImpl: SceneTransitionLayoutImpl,
+ private var orientation: Orientation,
+ private var topOrLeftBehavior: NestedScrollBehavior,
+ private var bottomOrRightBehavior: NestedScrollBehavior,
+ private var isExternalOverscrollGesture: () -> Boolean,
) : DelegatingNode() {
- lateinit var pointersInfoOwner: PointersInfoOwner
- private var priorityNestedScrollConnection: PriorityNestedScrollConnection =
- scenePriorityNestedScrollConnection(
- layoutImpl = layoutImpl,
- orientation = orientation,
- topOrLeftBehavior = topOrLeftBehavior,
- bottomOrRightBehavior = bottomOrRightBehavior,
- isExternalOverscrollGesture = isExternalOverscrollGesture,
- pointersInfoOwner = { pointersInfoOwner.pointersInfo() }
- )
+ private var scrollBehaviorOwner: ScrollBehaviorOwner? = null
- private var nestedScrollNode: DelegatableNode =
- nestedScrollModifierNode(
- connection = priorityNestedScrollConnection,
- dispatcher = null,
- )
+ private fun requireScrollBehaviorOwner(): ScrollBehaviorOwner {
+ var behaviorOwner = scrollBehaviorOwner
+ if (behaviorOwner == null) {
+ behaviorOwner = requireScrollBehaviorOwner(layoutImpl.draggableHandler(orientation))
+ scrollBehaviorOwner = behaviorOwner
+ }
+ return behaviorOwner
+ }
- override fun onAttach() {
- pointersInfoOwner = requireAncestorPointersInfoOwner()
- delegate(nestedScrollNode)
+ private val updateScrollBehaviorsConnection =
+ object : NestedScrollConnection {
+ /**
+ * When using [NestedScrollConnection.onPostScroll], we can specify the desired behavior
+ * before our parent components. This gives them the option to override our behavior if
+ * they choose.
+ *
+ * The behavior can be communicated at every scroll gesture to ensure that the hierarchy
+ * is respected, even if one of our descendant nodes changes behavior after we set it.
+ */
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource,
+ ): Offset {
+ // If we have some remaining scroll, that scroll can be used to initiate a
+ // transition between scenes. We can assume that the behavior is only needed if
+ // there is some remaining amount.
+ if (available != Offset.Zero) {
+ requireScrollBehaviorOwner()
+ .updateScrollBehaviors(
+ topOrLeftBehavior = topOrLeftBehavior,
+ bottomOrRightBehavior = bottomOrRightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
+ )
+ }
+
+ return Offset.Zero
+ }
+ }
+
+ init {
+ delegate(nestedScrollModifierNode(updateScrollBehaviorsConnection, dispatcher = null))
}
override fun onDetach() {
- // Make sure we reset the scroll connection when this modifier is removed from composition
- priorityNestedScrollConnection.reset()
+ scrollBehaviorOwner = null
}
fun update(
@@ -162,43 +190,10 @@
bottomOrRightBehavior: NestedScrollBehavior,
isExternalOverscrollGesture: () -> Boolean,
) {
- // Clean up the old nested scroll connection
- priorityNestedScrollConnection.reset()
- undelegate(nestedScrollNode)
-
- // Create a new nested scroll connection
- priorityNestedScrollConnection =
- scenePriorityNestedScrollConnection(
- layoutImpl = layoutImpl,
- orientation = orientation,
- topOrLeftBehavior = topOrLeftBehavior,
- bottomOrRightBehavior = bottomOrRightBehavior,
- isExternalOverscrollGesture = isExternalOverscrollGesture,
- pointersInfoOwner = pointersInfoOwner,
- )
- nestedScrollNode =
- nestedScrollModifierNode(
- connection = priorityNestedScrollConnection,
- dispatcher = null,
- )
- delegate(nestedScrollNode)
+ this.layoutImpl = layoutImpl
+ this.orientation = orientation
+ this.topOrLeftBehavior = topOrLeftBehavior
+ this.bottomOrRightBehavior = bottomOrRightBehavior
+ this.isExternalOverscrollGesture = isExternalOverscrollGesture
}
}
-
-private fun scenePriorityNestedScrollConnection(
- layoutImpl: SceneTransitionLayoutImpl,
- orientation: Orientation,
- topOrLeftBehavior: NestedScrollBehavior,
- bottomOrRightBehavior: NestedScrollBehavior,
- isExternalOverscrollGesture: () -> Boolean,
- pointersInfoOwner: PointersInfoOwner,
-) =
- NestedScrollHandlerImpl(
- layoutImpl = layoutImpl,
- orientation = orientation,
- topOrLeftBehavior = topOrLeftBehavior,
- bottomOrRightBehavior = bottomOrRightBehavior,
- isExternalOverscrollGesture = isExternalOverscrollGesture,
- pointersInfoOwner = pointersInfoOwner,
- )
- .connection
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 0c467b1..82275a9 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -207,8 +207,8 @@
* @param rightBehavior when we should perform the overscroll animation at the right.
*/
fun Modifier.horizontalNestedScrollToScene(
- leftBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
- rightBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
+ leftBehavior: NestedScrollBehavior = NestedScrollBehavior.Default,
+ rightBehavior: NestedScrollBehavior = NestedScrollBehavior.Default,
isExternalOverscrollGesture: () -> Boolean = { false },
): Modifier
@@ -220,8 +220,8 @@
* @param bottomBehavior when we should perform the overscroll animation at the bottom.
*/
fun Modifier.verticalNestedScrollToScene(
- topBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
- bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
+ topBehavior: NestedScrollBehavior = NestedScrollBehavior.Default,
+ bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.Default,
isExternalOverscrollGesture: () -> Boolean = { false },
): Modifier
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index aeb6262..b8010f2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -20,11 +20,15 @@
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.PointerInputModifierNode
+import androidx.compose.ui.node.TraversableNode
+import androidx.compose.ui.node.findNearestAncestor
import androidx.compose.ui.unit.IntSize
/**
@@ -53,7 +57,7 @@
draggableHandler: DraggableHandlerImpl,
swipeDetector: SwipeDetector,
) : DelegatingNode(), PointerInputModifierNode {
- private val delegate =
+ private val multiPointerDraggableNode =
delegate(
MultiPointerDraggableNode(
orientation = draggableHandler.orientation,
@@ -74,21 +78,41 @@
// Make sure to update the delegate orientation. Note that this will automatically
// reset the underlying pointer input handler, so previous gestures will be
// cancelled.
- delegate.orientation = value.orientation
+ multiPointerDraggableNode.orientation = value.orientation
}
}
+ private val nestedScrollHandlerImpl =
+ NestedScrollHandlerImpl(
+ layoutImpl = draggableHandler.layoutImpl,
+ orientation = draggableHandler.orientation,
+ topOrLeftBehavior = NestedScrollBehavior.Default,
+ bottomOrRightBehavior = NestedScrollBehavior.Default,
+ isExternalOverscrollGesture = { false },
+ pointersInfoOwner = { multiPointerDraggableNode.pointersInfo() },
+ )
+
+ init {
+ delegate(nestedScrollModifierNode(nestedScrollHandlerImpl.connection, dispatcher = null))
+ delegate(ScrollBehaviorOwnerNode(draggableHandler.nestedScrollKey, nestedScrollHandlerImpl))
+ }
+
+ override fun onDetach() {
+ // Make sure we reset the scroll connection when this modifier is removed from composition
+ nestedScrollHandlerImpl.connection.reset()
+ }
+
override fun onPointerEvent(
pointerEvent: PointerEvent,
pass: PointerEventPass,
bounds: IntSize,
- ) = delegate.onPointerEvent(pointerEvent, pass, bounds)
+ ) = multiPointerDraggableNode.onPointerEvent(pointerEvent, pass, bounds)
- override fun onCancelPointerInput() = delegate.onCancelPointerInput()
+ override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput()
private fun enabled(): Boolean {
return draggableHandler.isDrivingTransition ||
- currentScene().shouldEnableSwipes(delegate.orientation)
+ currentScene().shouldEnableSwipes(multiPointerDraggableNode.orientation)
}
private fun currentScene(): Scene {
@@ -118,3 +142,43 @@
return currentScene().shouldEnableSwipes(oppositeOrientation)
}
}
+
+/** Find the [ScrollBehaviorOwner] for the current orientation. */
+internal fun DelegatableNode.requireScrollBehaviorOwner(
+ draggableHandler: DraggableHandlerImpl
+): ScrollBehaviorOwner {
+ val ancestorNode =
+ checkNotNull(findNearestAncestor(draggableHandler.nestedScrollKey)) {
+ "This should never happen! Couldn't find a ScrollBehaviorOwner. " +
+ "Are we inside an SceneTransitionLayout?"
+ }
+ return ancestorNode as ScrollBehaviorOwner
+}
+
+internal fun interface ScrollBehaviorOwner {
+ fun updateScrollBehaviors(
+ topOrLeftBehavior: NestedScrollBehavior,
+ bottomOrRightBehavior: NestedScrollBehavior,
+ isExternalOverscrollGesture: () -> Boolean,
+ )
+}
+
+/**
+ * We need a node that receives the desired behavior.
+ *
+ * TODO(b/353234530) move this logic into [SwipeToSceneNode]
+ */
+private class ScrollBehaviorOwnerNode(
+ override val traverseKey: Any,
+ val nestedScrollHandlerImpl: NestedScrollHandlerImpl
+) : Modifier.Node(), TraversableNode, ScrollBehaviorOwner {
+ override fun updateScrollBehaviors(
+ topOrLeftBehavior: NestedScrollBehavior,
+ bottomOrRightBehavior: NestedScrollBehavior,
+ isExternalOverscrollGesture: () -> Boolean
+ ) {
+ nestedScrollHandlerImpl.topOrLeftBehavior = topOrLeftBehavior
+ nestedScrollHandlerImpl.bottomOrRightBehavior = bottomOrRightBehavior
+ nestedScrollHandlerImpl.isExternalOverscrollGesture = isExternalOverscrollGesture
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 7988e0e..c91151e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -797,8 +797,6 @@
scene(SceneB, userActions = mapOf(Swipe.Up to SceneA)) {
Box(
Modifier
- // Unconsumed scroll gesture will be intercepted by STL
- .verticalNestedScrollToScene()
// A scrollable that does not consume the scroll gesture
.scrollable(
rememberScrollableState(consumeScrollDelta = { 0f }),
@@ -875,8 +873,6 @@
) {
Box(
Modifier
- // Unconsumed scroll gesture will be intercepted by STL
- .verticalNestedScrollToScene()
// A scrollable that does not consume the scroll gesture
.scrollable(
rememberScrollableState(consumeScrollDelta = { 0f }),
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
new file mode 100644
index 0000000..311a580
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
@@ -0,0 +1,269 @@
+/*
+ * 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.compose.animation.scene
+
+import androidx.compose.foundation.gestures.Orientation.Vertical
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestScenes.SceneA
+import com.android.compose.animation.scene.TestScenes.SceneB
+import com.android.compose.animation.scene.subjects.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NestedScrollToSceneTest {
+ @get:Rule val rule = createComposeRule()
+
+ private var touchSlop = 0f
+ private val layoutWidth: Dp = 200.dp
+ private val layoutHeight = 400.dp
+
+ private fun setup2ScenesAndScrollTouchSlop(
+ modifierSceneA: @Composable SceneScope.() -> Modifier = { Modifier },
+ ): MutableSceneTransitionLayoutState {
+ val state =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutState(SceneA, transitions = EmptyTestTransitions)
+ }
+
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ SceneTransitionLayout(
+ state = state,
+ modifier = Modifier.size(layoutWidth, layoutHeight)
+ ) {
+ scene(SceneA, userActions = mapOf(Swipe.Up to SceneB)) {
+ Spacer(modifierSceneA().fillMaxSize())
+ }
+ scene(SceneB, userActions = mapOf(Swipe.Down to SceneA)) {
+ Spacer(Modifier.fillMaxSize())
+ }
+ }
+ }
+
+ pointerDownAndScrollTouchSlop()
+
+ assertThat(state.transitionState).isIdle()
+
+ return state
+ }
+
+ private fun pointerDownAndScrollTouchSlop() {
+ rule.onRoot().performTouchInput {
+ val middleTop = Offset((layoutWidth / 2).toPx(), 0f)
+ down(middleTop)
+ // Scroll touchSlop
+ moveBy(Offset(0f, touchSlop), delayMillis = 1_000)
+ }
+ }
+
+ private fun scrollDown(percent: Float = 1f) {
+ rule.onRoot().performTouchInput {
+ moveBy(Offset(0f, layoutHeight.toPx() * percent), delayMillis = 1_000)
+ }
+ }
+
+ private fun scrollUp(percent: Float = 1f) = scrollDown(-percent)
+
+ private fun pointerUp() {
+ rule.onRoot().performTouchInput { up() }
+ }
+
+ @Test
+ fun scrollableElementsInSTL_shouldHavePriority() {
+ val state = setup2ScenesAndScrollTouchSlop {
+ Modifier
+ // A scrollable that consumes the scroll gesture
+ .scrollable(rememberScrollableState { it }, Vertical)
+ }
+
+ scrollUp(percent = 0.5f)
+
+ // Consumed by the scrollable element
+ assertThat(state.transitionState).isIdle()
+ }
+
+ @Test
+ fun unconsumedScrollEvents_canBeConsumedBySTLByDefault() {
+ val state = setup2ScenesAndScrollTouchSlop {
+ Modifier
+ // A scrollable that does not consume the scroll gesture
+ .scrollable(rememberScrollableState { 0f }, Vertical)
+ }
+
+ scrollUp(percent = 0.5f)
+ // STL will start a transition with the remaining scroll
+ val transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasProgress(0.5f)
+
+ scrollUp(percent = 1f)
+ assertThat(transition).hasProgress(1.5f)
+ }
+
+ @Test
+ fun customizeStlNestedScrollBehavior_DuringTransitionBetweenScenes() {
+ var canScroll = true
+ val state = setup2ScenesAndScrollTouchSlop {
+ Modifier.verticalNestedScrollToScene(
+ bottomBehavior = NestedScrollBehavior.DuringTransitionBetweenScenes
+ )
+ .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical)
+ }
+
+ scrollUp(percent = 0.5f)
+ assertThat(state.transitionState).isIdle()
+
+ // Reach the end of the scrollable element
+ canScroll = false
+ scrollUp(percent = 0.5f)
+ assertThat(state.transitionState).isIdle()
+
+ pointerUp()
+ assertThat(state.transitionState).isIdle()
+
+ // Start a new gesture
+ pointerDownAndScrollTouchSlop()
+ scrollUp(percent = 0.5f)
+ assertThat(state.transitionState).isIdle()
+ }
+
+ @Test
+ fun customizeStlNestedScrollBehavior_EdgeNoPreview() {
+ var canScroll = true
+ val state = setup2ScenesAndScrollTouchSlop {
+ Modifier.verticalNestedScrollToScene(
+ bottomBehavior = NestedScrollBehavior.EdgeNoPreview
+ )
+ .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical)
+ }
+
+ scrollUp(percent = 0.5f)
+ assertThat(state.transitionState).isIdle()
+
+ // Reach the end of the scrollable element
+ canScroll = false
+ scrollUp(percent = 0.5f)
+ assertThat(state.transitionState).isIdle()
+
+ pointerUp()
+ assertThat(state.transitionState).isIdle()
+
+ // Start a new gesture
+ pointerDownAndScrollTouchSlop()
+ scrollUp(percent = 0.5f)
+ val transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasProgress(0.5f)
+
+ pointerUp()
+ rule.waitForIdle()
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneB)
+ }
+
+ @Test
+ fun customizeStlNestedScrollBehavior_EdgeWithPreview() {
+ var canScroll = true
+ val state = setup2ScenesAndScrollTouchSlop {
+ Modifier.verticalNestedScrollToScene(
+ bottomBehavior = NestedScrollBehavior.EdgeWithPreview
+ )
+ .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical)
+ }
+
+ scrollUp(percent = 0.5f)
+ assertThat(state.transitionState).isIdle()
+
+ // Reach the end of the scrollable element
+ canScroll = false
+ scrollUp(percent = 0.5f)
+ val transition1 = assertThat(state.transitionState).isTransition()
+ assertThat(transition1).hasProgress(0.5f)
+
+ pointerUp()
+ rule.waitForIdle()
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneA)
+
+ // Start a new gesture
+ pointerDownAndScrollTouchSlop()
+ scrollUp(percent = 0.5f)
+ val transition2 = assertThat(state.transitionState).isTransition()
+ assertThat(transition2).hasProgress(0.5f)
+
+ pointerUp()
+ rule.waitForIdle()
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneB)
+ }
+
+ @Test
+ fun customizeStlNestedScrollBehavior_EdgeAlways() {
+ var canScroll = true
+ val state = setup2ScenesAndScrollTouchSlop {
+ Modifier.verticalNestedScrollToScene(bottomBehavior = NestedScrollBehavior.EdgeAlways)
+ .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical)
+ }
+
+ scrollUp(percent = 0.5f)
+ assertThat(state.transitionState).isIdle()
+
+ // Reach the end of the scrollable element
+ canScroll = false
+ scrollUp(percent = 0.5f)
+ val transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasProgress(0.5f)
+
+ pointerUp()
+ rule.waitForIdle()
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneB)
+ }
+
+ @Test
+ fun customizeStlNestedScrollBehavior_multipleRequests() {
+ val state = setup2ScenesAndScrollTouchSlop {
+ Modifier
+ // This verticalNestedScrollToScene is closer the STL (an ancestor node)
+ .verticalNestedScrollToScene(bottomBehavior = NestedScrollBehavior.EdgeAlways)
+ // Another verticalNestedScrollToScene modifier
+ .verticalNestedScrollToScene(
+ bottomBehavior = NestedScrollBehavior.DuringTransitionBetweenScenes
+ )
+ .scrollable(rememberScrollableState { 0f }, Vertical)
+ }
+
+ scrollUp(percent = 0.5f)
+ // EdgeAlways always consume the remaining scroll, DuringTransitionBetweenScenes does not.
+ val transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasProgress(0.5f)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt
index be0d899..9e69601 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt
@@ -18,8 +18,11 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -74,9 +77,9 @@
@Test
@DisableSceneContainer
+ @DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun alpha_WhenNotGone_clockMigrationFlagIsOff_emitsKeyguardAlpha() =
testScope.runTest {
- mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
val alpha by collectLastValue(underTest.alpha)
keyguardTransitionRepository.sendTransitionSteps(
@@ -186,9 +189,9 @@
@Test
@DisableSceneContainer
+ @EnableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun alpha_whenGone_equalsZero() =
testScope.runTest {
- mSetFlagsRule.enableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
val alpha by collectLastValue(underTest.alpha)
keyguardTransitionRepository.sendTransitionStep(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
index 63d06a4..41c5b73 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
@@ -18,6 +18,8 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags as AConfigFlags
@@ -69,10 +71,11 @@
private val burnInFlow = MutableStateFlow(BurnInModel())
@Before
+ @DisableFlags(
+ AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+ AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
+ )
fun setUp() {
- mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
- mSetFlagsRule.disableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
-
MockitoAnnotations.initMocks(this)
whenever(burnInInteractor.burnIn(anyInt(), anyInt())).thenReturn(burnInFlow)
kosmos.burnInInteractor = burnInInteractor
@@ -174,10 +177,9 @@
}
@Test
+ @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun translationAndScale_whenFullyDozing_MigrationFlagOff_staysOutOfTopInset() =
testScope.runTest {
- mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-
burnInParameters =
burnInParameters.copy(
minViewY = 100,
@@ -226,10 +228,9 @@
}
@Test
+ @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun translationAndScale_whenFullyDozing_MigrationFlagOn_staysOutOfTopInset() =
testScope.runTest {
- mSetFlagsRule.enableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-
burnInParameters =
burnInParameters.copy(
minViewY = 100,
@@ -310,104 +311,99 @@
}
@Test
+ @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+ @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun translationAndScale_composeFlagOff_weatherLargeClock() =
testBurnInViewModelForClocks(
isSmallClock = false,
isWeatherClock = true,
expectedScaleOnly = false,
- enableMigrateClocksToBlueprintFlag = true,
- enableComposeLockscreenFlag = false
)
@Test
+ @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+ @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun translationAndScale_composeFlagOff_weatherSmallClock() =
testBurnInViewModelForClocks(
isSmallClock = true,
isWeatherClock = true,
expectedScaleOnly = false,
- enableMigrateClocksToBlueprintFlag = true,
- enableComposeLockscreenFlag = false
)
@Test
+ @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+ @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun translationAndScale_composeFlagOff_nonWeatherLargeClock() =
testBurnInViewModelForClocks(
isSmallClock = false,
isWeatherClock = false,
expectedScaleOnly = true,
- enableMigrateClocksToBlueprintFlag = true,
- enableComposeLockscreenFlag = false
)
@Test
+ @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+ @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun translationAndScale_composeFlagOff_nonWeatherSmallClock() =
testBurnInViewModelForClocks(
isSmallClock = true,
isWeatherClock = false,
expectedScaleOnly = false,
- enableMigrateClocksToBlueprintFlag = true,
- enableComposeLockscreenFlag = false
)
@Test
+ @EnableFlags(
+ AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+ AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
+ )
fun translationAndScale_composeFlagOn_weatherLargeClock() =
testBurnInViewModelForClocks(
isSmallClock = false,
isWeatherClock = true,
expectedScaleOnly = false,
- enableMigrateClocksToBlueprintFlag = true,
- enableComposeLockscreenFlag = true
)
@Test
+ @EnableFlags(
+ AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+ AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
+ )
fun translationAndScale_composeFlagOn_weatherSmallClock() =
testBurnInViewModelForClocks(
isSmallClock = true,
isWeatherClock = true,
expectedScaleOnly = false,
- enableMigrateClocksToBlueprintFlag = true,
- enableComposeLockscreenFlag = true
)
@Test
+ @EnableFlags(
+ AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+ AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
+ )
fun translationAndScale_composeFlagOn_nonWeatherLargeClock() =
testBurnInViewModelForClocks(
isSmallClock = false,
isWeatherClock = false,
expectedScaleOnly = true,
- enableMigrateClocksToBlueprintFlag = true,
- enableComposeLockscreenFlag = true
)
@Test
+ @EnableFlags(
+ AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+ AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
+ )
fun translationAndScale_composeFlagOn_nonWeatherSmallClock() =
testBurnInViewModelForClocks(
isSmallClock = true,
isWeatherClock = false,
expectedScaleOnly = false,
- enableMigrateClocksToBlueprintFlag = true,
- enableComposeLockscreenFlag = true
)
private fun testBurnInViewModelForClocks(
isSmallClock: Boolean,
isWeatherClock: Boolean,
expectedScaleOnly: Boolean,
- enableMigrateClocksToBlueprintFlag: Boolean,
- enableComposeLockscreenFlag: Boolean
) =
testScope.runTest {
- if (enableMigrateClocksToBlueprintFlag) {
- mSetFlagsRule.enableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
- } else {
- mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
- }
-
- if (enableComposeLockscreenFlag) {
- mSetFlagsRule.enableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
- } else {
- mSetFlagsRule.disableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
- }
if (isSmallClock) {
keyguardClockRepository.setClockSize(ClockSize.SMALL)
// we need the following step to update stateFlow value
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 10d1891..0f61233 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -34,6 +34,7 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
+import com.android.systemui.Flags;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.res.R;
@@ -130,7 +131,10 @@
verifyPasswordAndUnlock();
}
});
- okButton.setOnHoverListener(mLiftToActivateListener);
+
+ if (!Flags.simPinTalkbackFixForDoubleSubmit()) {
+ okButton.setOnHoverListener(mLiftToActivateListener);
+ }
}
if (pinInputFieldStyledFocusState()) {
collectFlow(mPasswordEntry, mKeyguardKeyboardInteractor.isAnyKeyboardConnected(),
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
index 1dbd500..c4abcd2 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
@@ -54,6 +54,7 @@
import android.view.WindowManagerGlobal;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.inputmethod.Flags;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
@@ -285,8 +286,11 @@
// Set up the context group of buttons
mContextualButtonGroup = new ContextualButtonGroup(R.id.menu_container);
+ final int switcherResId = Flags.imeSwitcherRevamp()
+ ? com.android.internal.R.drawable.ic_ime_switcher_new
+ : R.drawable.ic_ime_switcher_default;
final ContextualButton imeSwitcherButton = new ContextualButton(R.id.ime_switcher,
- mLightContext, R.drawable.ic_ime_switcher_default);
+ mLightContext, switcherResId);
final ContextualButton accessibilityButton =
new ContextualButton(R.id.accessibility_button, mLightContext,
R.drawable.ic_sysbar_accessibility_button);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
index 11ccdff..59fd0ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
@@ -57,7 +57,7 @@
interactor.ongoingCallState
.map { state ->
when (state) {
- is OngoingCallModel.NoCall -> OngoingActivityChipModel.Hidden
+ is OngoingCallModel.NoCall -> OngoingActivityChipModel.Hidden()
is OngoingCallModel.InCall -> {
// This block mimics OngoingCallController#updateChip.
if (state.startTimeMs <= 0L) {
@@ -82,7 +82,7 @@
}
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden())
private fun getOnClickListener(state: OngoingCallModel.InCall): View.OnClickListener? {
if (state.intent == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
index bafec38..6ea72b9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
@@ -44,9 +44,10 @@
// No custom on-click, because the dialog will automatically be dismissed when the
// button is clicked anyway.
setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
- setPositiveButton(R.string.cast_to_other_device_stop_dialog_button) { _, _ ->
- stopAction.invoke()
- }
+ setPositiveButton(
+ R.string.cast_to_other_device_stop_dialog_button,
+ endMediaProjectionDialogHelper.wrapStopAction(stopAction),
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
index 7dc9b25..b0c8321 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
@@ -55,9 +55,10 @@
// No custom on-click, because the dialog will automatically be dismissed when the
// button is clicked anyway.
setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
- setPositiveButton(R.string.cast_to_other_device_stop_dialog_button) { _, _ ->
- stopAction.invoke()
- }
+ setPositiveButton(
+ R.string.cast_to_other_device_stop_dialog_button,
+ endMediaProjectionDialogHelper.wrapStopAction(stopAction),
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index afa9cce..d9b0504 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -18,6 +18,9 @@
import android.content.Context
import androidx.annotation.DrawableRes
+import com.android.internal.jank.Cuj
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
@@ -35,6 +38,7 @@
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener
import com.android.systemui.util.time.SystemClock
@@ -60,6 +64,7 @@
private val mediaProjectionChipInteractor: MediaProjectionChipInteractor,
private val mediaRouterChipInteractor: MediaRouterChipInteractor,
private val systemClock: SystemClock,
+ private val dialogTransitionAnimator: DialogTransitionAnimator,
private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
@StatusBarChipsLog private val logger: LogBuffer,
) : OngoingActivityChipViewModel {
@@ -74,18 +79,18 @@
mediaProjectionChipInteractor.projection
.map { projectionModel ->
when (projectionModel) {
- is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden
+ is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden()
is ProjectionChipModel.Projecting -> {
if (projectionModel.type != ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE) {
- OngoingActivityChipModel.Hidden
+ OngoingActivityChipModel.Hidden()
} else {
createCastScreenToOtherDeviceChip(projectionModel)
}
}
}
}
- // See b/347726238.
- .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
+ // See b/347726238 for [SharingStarted.Lazily] reasoning.
+ .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden())
/**
* The cast chip to show, based only on MediaRouter API events.
@@ -109,7 +114,7 @@
mediaRouterChipInteractor.mediaRouterCastingState
.map { routerModel ->
when (routerModel) {
- is MediaRouterCastModel.DoingNothing -> OngoingActivityChipModel.Hidden
+ is MediaRouterCastModel.DoingNothing -> OngoingActivityChipModel.Hidden()
is MediaRouterCastModel.Casting -> {
// A consequence of b/269975671 is that MediaRouter will mark a device as
// casting before casting has actually started. To alleviate this bug a bit,
@@ -123,9 +128,9 @@
}
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden())
- override val chip: StateFlow<OngoingActivityChipModel> =
+ private val internalChip: StateFlow<OngoingActivityChipModel> =
combine(projectionChip, routerChip) { projection, router ->
logger.log(
TAG,
@@ -159,17 +164,24 @@
router
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden())
+
+ private val hideChipDuringDialogTransitionHelper = ChipTransitionHelper(scope)
+
+ override val chip: StateFlow<OngoingActivityChipModel> =
+ hideChipDuringDialogTransitionHelper.createChipFlow(internalChip)
/** Stops the currently active projection. */
- private fun stopProjecting() {
- logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested (projection)" })
+ private fun stopProjectingFromDialog() {
+ logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested from dialog (projection)" })
+ hideChipDuringDialogTransitionHelper.onActivityStoppedFromDialog()
mediaProjectionChipInteractor.stopProjecting()
}
/** Stops the currently active media route. */
- private fun stopMediaRouterCasting() {
- logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested (router)" })
+ private fun stopMediaRouterCastingFromDialog() {
+ logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested from dialog (router)" })
+ hideChipDuringDialogTransitionHelper.onActivityStoppedFromDialog()
mediaRouterChipInteractor.stopCasting()
}
@@ -190,6 +202,8 @@
startTimeMs = systemClock.elapsedRealtime(),
createDialogLaunchOnClickListener(
createCastScreenToOtherDeviceDialogDelegate(state),
+ dialogTransitionAnimator,
+ DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Cast to other device"),
logger,
TAG,
),
@@ -207,6 +221,11 @@
colors = ColorsModel.Red,
createDialogLaunchOnClickListener(
createGenericCastToOtherDeviceDialogDelegate(deviceName),
+ dialogTransitionAnimator,
+ DialogCuj(
+ Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP,
+ tag = "Cast to other device audio only",
+ ),
logger,
TAG,
),
@@ -219,7 +238,7 @@
EndCastScreenToOtherDeviceDialogDelegate(
endMediaProjectionDialogHelper,
context,
- stopAction = this::stopProjecting,
+ stopAction = this::stopProjectingFromDialog,
state,
)
@@ -228,7 +247,7 @@
endMediaProjectionDialogHelper,
context,
deviceName,
- stopAction = this::stopMediaRouterCasting,
+ stopAction = this::stopMediaRouterCastingFromDialog,
)
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
index 6004365..2d9ccb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
@@ -17,7 +17,9 @@
package com.android.systemui.statusbar.chips.mediaprojection.ui.view
import android.app.ActivityManager
+import android.content.DialogInterface
import android.content.pm.PackageManager
+import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -29,6 +31,7 @@
@Inject
constructor(
private val dialogFactory: SystemUIDialog.Factory,
+ private val dialogTransitionAnimator: DialogTransitionAnimator,
private val packageManager: PackageManager,
) {
/** Creates a new [SystemUIDialog] using the given delegate. */
@@ -36,6 +39,28 @@
return dialogFactory.create(delegate)
}
+ /**
+ * Returns the click listener that should be invoked if a user clicks "Stop" on the end media
+ * projection dialog.
+ *
+ * The click listener will invoke [stopAction] and also do some UI manipulation.
+ *
+ * @param stopAction an action that, when invoked, should notify system API(s) that the media
+ * projection should be stopped.
+ */
+ fun wrapStopAction(stopAction: () -> Unit): DialogInterface.OnClickListener {
+ return DialogInterface.OnClickListener { _, _ ->
+ // If the projection is stopped, then the chip will disappear, so we don't want the
+ // dialog to animate back into the chip just for the chip to disappear in a few frames.
+ dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations()
+ stopAction.invoke()
+ // TODO(b/332662551): If the projection is stopped, there's a brief moment where the
+ // dialog closes and the chip re-shows because the system APIs haven't come back and
+ // told SysUI that the projection has officially stopped. It would be great for the chip
+ // to not re-show at all.
+ }
+ }
+
fun getAppName(state: MediaProjectionState.Projecting): CharSequence? {
val specificTaskInfo =
if (state is MediaProjectionState.Projecting.SingleTask) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
index 1eca827..72656ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
@@ -52,9 +52,10 @@
// No custom on-click, because the dialog will automatically be dismissed when the
// button is clicked anyway.
setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
- setPositiveButton(R.string.screenrecord_stop_dialog_button) { _, _ ->
- stopAction.invoke()
- }
+ setPositiveButton(
+ R.string.screenrecord_stop_dialog_button,
+ endMediaProjectionDialogHelper.wrapStopAction(stopAction),
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
index 0c34981..fcf3de4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
@@ -19,6 +19,9 @@
import android.app.ActivityManager
import android.content.Context
import androidx.annotation.DrawableRes
+import com.android.internal.jank.Cuj
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
@@ -32,8 +35,10 @@
import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener
import com.android.systemui.util.time.SystemClock
@@ -52,15 +57,18 @@
@Application private val scope: CoroutineScope,
private val context: Context,
private val interactor: ScreenRecordChipInteractor,
+ private val shareToAppChipViewModel: ShareToAppChipViewModel,
private val systemClock: SystemClock,
private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
+ private val dialogTransitionAnimator: DialogTransitionAnimator,
@StatusBarChipsLog private val logger: LogBuffer,
) : OngoingActivityChipViewModel {
- override val chip: StateFlow<OngoingActivityChipModel> =
+
+ private val internalChip =
interactor.screenRecordState
.map { state ->
when (state) {
- is ScreenRecordChipModel.DoingNothing -> OngoingActivityChipModel.Hidden
+ is ScreenRecordChipModel.DoingNothing -> OngoingActivityChipModel.Hidden()
is ScreenRecordChipModel.Starting -> {
OngoingActivityChipModel.Shown.Countdown(
colors = ColorsModel.Red,
@@ -80,6 +88,11 @@
startTimeMs = systemClock.elapsedRealtime(),
createDialogLaunchOnClickListener(
createDelegate(state.recordedTask),
+ dialogTransitionAnimator,
+ DialogCuj(
+ Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP,
+ tag = "Screen record",
+ ),
logger,
TAG,
),
@@ -87,8 +100,13 @@
}
}
}
- // See b/347726238.
- .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
+ // See b/347726238 for [SharingStarted.Lazily] reasoning.
+ .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden())
+
+ private val chipTransitionHelper = ChipTransitionHelper(scope)
+
+ override val chip: StateFlow<OngoingActivityChipModel> =
+ chipTransitionHelper.createChipFlow(internalChip)
private fun createDelegate(
recordedTask: ActivityManager.RunningTaskInfo?
@@ -96,13 +114,15 @@
return EndScreenRecordingDialogDelegate(
endMediaProjectionDialogHelper,
context,
- stopAction = this::stopRecording,
+ stopAction = this::stopRecordingFromDialog,
recordedTask,
)
}
- private fun stopRecording() {
- logger.log(TAG, LogLevel.INFO, {}, { "Stop recording requested" })
+ private fun stopRecordingFromDialog() {
+ logger.log(TAG, LogLevel.INFO, {}, { "Stop recording requested from dialog" })
+ chipTransitionHelper.onActivityStoppedFromDialog()
+ shareToAppChipViewModel.onRecordingStoppedFromDialog()
interactor.stopRecording()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
index 564f20e..d10bd77 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
@@ -44,9 +44,10 @@
// No custom on-click, because the dialog will automatically be dismissed when the
// button is clicked anyway.
setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
- setPositiveButton(R.string.share_to_app_stop_dialog_button) { _, _ ->
- stopAction.invoke()
- }
+ setPositiveButton(
+ R.string.share_to_app_stop_dialog_button,
+ endMediaProjectionDialogHelper.wrapStopAction(stopAction),
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
index ddebd3a..85973fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
@@ -18,6 +18,9 @@
import android.content.Context
import androidx.annotation.DrawableRes
+import com.android.internal.jank.Cuj
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
@@ -32,6 +35,7 @@
import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener
import com.android.systemui.util.time.SystemClock
@@ -55,28 +59,49 @@
private val mediaProjectionChipInteractor: MediaProjectionChipInteractor,
private val systemClock: SystemClock,
private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
+ private val dialogTransitionAnimator: DialogTransitionAnimator,
@StatusBarChipsLog private val logger: LogBuffer,
) : OngoingActivityChipViewModel {
- override val chip: StateFlow<OngoingActivityChipModel> =
+ private val internalChip =
mediaProjectionChipInteractor.projection
.map { projectionModel ->
when (projectionModel) {
- is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden
+ is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden()
is ProjectionChipModel.Projecting -> {
if (projectionModel.type != ProjectionChipModel.Type.SHARE_TO_APP) {
- OngoingActivityChipModel.Hidden
+ OngoingActivityChipModel.Hidden()
} else {
createShareToAppChip(projectionModel)
}
}
}
}
- // See b/347726238.
- .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
+ // See b/347726238 for [SharingStarted.Lazily] reasoning.
+ .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden())
+
+ private val chipTransitionHelper = ChipTransitionHelper(scope)
+
+ override val chip: StateFlow<OngoingActivityChipModel> =
+ chipTransitionHelper.createChipFlow(internalChip)
+
+ /**
+ * Notifies this class that the user just stopped a screen recording from the dialog that's
+ * shown when you tap the recording chip.
+ */
+ fun onRecordingStoppedFromDialog() {
+ // When a screen recording is active, share-to-app is also active (screen recording is just
+ // a special case of share-to-app, where the specific app receiving the share is System UI).
+ // When a screen recording is stopped, we immediately hide the screen recording chip in
+ // [com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel].
+ // We *also* need to immediately hide the share-to-app chip so it doesn't briefly show.
+ // See b/350891338.
+ chipTransitionHelper.onActivityStoppedFromDialog()
+ }
/** Stops the currently active projection. */
- private fun stopProjecting() {
- logger.log(TAG, LogLevel.INFO, {}, { "Stop sharing requested" })
+ private fun stopProjectingFromDialog() {
+ logger.log(TAG, LogLevel.INFO, {}, { "Stop sharing requested from dialog" })
+ chipTransitionHelper.onActivityStoppedFromDialog()
mediaProjectionChipInteractor.stopProjecting()
}
@@ -92,7 +117,16 @@
colors = ColorsModel.Red,
// TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
startTimeMs = systemClock.elapsedRealtime(),
- createDialogLaunchOnClickListener(createShareToAppDialogDelegate(state), logger, TAG),
+ createDialogLaunchOnClickListener(
+ createShareToAppDialogDelegate(state),
+ dialogTransitionAnimator,
+ DialogCuj(
+ Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP,
+ tag = "Share to app",
+ ),
+ logger,
+ TAG,
+ ),
)
}
@@ -100,7 +134,7 @@
EndShareToAppDialogDelegate(
endMediaProjectionDialogHelper,
context,
- stopAction = this::stopProjecting,
+ stopAction = this::stopProjectingFromDialog,
state,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
index 40f86f9..17cf60b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
@@ -24,9 +24,15 @@
/** Condensed name representing the model, used for logs. */
abstract val logName: String
- /** This chip shouldn't be shown. */
- data object Hidden : OngoingActivityChipModel() {
- override val logName = "Hidden"
+ /**
+ * This chip shouldn't be shown.
+ *
+ * @property shouldAnimate true if the transition from [Shown] to [Hidden] should be animated,
+ * and false if that transition should *not* be animated (i.e. the chip view should
+ * immediately disappear).
+ */
+ data class Hidden(val shouldAnimate: Boolean = true) : OngoingActivityChipModel() {
+ override val logName = "Hidden(anim=$shouldAnimate)"
}
/** This chip should be shown with the given information. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelper.kt
new file mode 100644
index 0000000..92e72c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelper.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 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.statusbar.chips.ui.viewmodel
+
+import android.annotation.SuppressLint
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transformLatest
+import kotlinx.coroutines.launch
+
+/**
+ * A class that can help [OngoingActivityChipViewModel] instances with various transition states.
+ *
+ * For now, this class's only functionality is immediately hiding the chip if the user has tapped an
+ * activity chip and then clicked "Stop" on the resulting dialog. There's a bit of a delay between
+ * when the user clicks "Stop" and when the system services notify SysUI that the activity has
+ * indeed stopped. We don't want the chip to briefly show for a few frames during that delay, so
+ * this class helps us immediately hide the chip as soon as the user clicks "Stop" in the dialog.
+ * See b/353249803#comment4.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class ChipTransitionHelper(@Application private val scope: CoroutineScope) {
+ /** A flow that emits each time the user has clicked "Stop" on the dialog. */
+ @SuppressLint("SharedFlowCreation")
+ private val activityStoppedFromDialogEvent = MutableSharedFlow<Unit>()
+
+ /** True if the user recently stopped the activity from the dialog. */
+ private val wasActivityRecentlyStoppedFromDialog: Flow<Boolean> =
+ activityStoppedFromDialogEvent
+ .transformLatest {
+ // Give system services 500ms to stop the activity and notify SysUI. Once more than
+ // 500ms has elapsed, we should go back to using the current system service
+ // information as the source of truth.
+ emit(true)
+ delay(500)
+ emit(false)
+ }
+ // Use stateIn so that the flow created in [createChipFlow] is guaranteed to
+ // emit. (`combine`s require that all input flows have emitted.)
+ .stateIn(scope, SharingStarted.Lazily, false)
+
+ /**
+ * Notifies this class that the user just clicked "Stop" on the stop dialog that's shown when
+ * the chip is tapped.
+ *
+ * Call this method in order to immediately hide the chip.
+ */
+ fun onActivityStoppedFromDialog() {
+ // Because this event causes UI changes, make sure it's launched on the main thread scope.
+ scope.launch { activityStoppedFromDialogEvent.emit(Unit) }
+ }
+
+ /**
+ * Creates a flow that will forcibly hide the chip if the user recently stopped the activity
+ * (see [onActivityStoppedFromDialog]). In general, this flow just uses value in [chip].
+ */
+ fun createChipFlow(chip: Flow<OngoingActivityChipModel>): StateFlow<OngoingActivityChipModel> {
+ return combine(
+ chip,
+ wasActivityRecentlyStoppedFromDialog,
+ ) { chipModel, activityRecentlyStopped ->
+ if (activityRecentlyStopped) {
+ // There's a bit of a delay between when the user stops an activity via
+ // SysUI and when the system services notify SysUI that the activity has
+ // indeed stopped. Prevent the chip from showing during this delay by
+ // immediately hiding it without any animation.
+ OngoingActivityChipModel.Hidden(shouldAnimate = false)
+ } else {
+ chipModel
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden())
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt
index ee010f7..2fc366b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt
@@ -17,10 +17,14 @@
package com.android.systemui.statusbar.chips.ui.viewmodel
import android.view.View
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
+import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.StatusBarChipsLog
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.phone.SystemUIDialog
import kotlinx.coroutines.flow.StateFlow
@@ -36,13 +40,19 @@
/** Creates a chip click listener that launches a dialog created by [dialogDelegate]. */
fun createDialogLaunchOnClickListener(
dialogDelegate: SystemUIDialog.Delegate,
+ dialogTransitionAnimator: DialogTransitionAnimator,
+ cuj: DialogCuj,
@StatusBarChipsLog logger: LogBuffer,
tag: String,
): View.OnClickListener {
- return View.OnClickListener { _ ->
+ return View.OnClickListener { view ->
logger.log(tag, LogLevel.INFO, {}, { "Chip clicked" })
val dialog = dialogDelegate.createDialog()
- dialog.show()
+ val launchableView =
+ view.requireViewById<ChipBackgroundContainer>(
+ R.id.ongoing_activity_chip_background
+ )
+ dialogTransitionAnimator.showFromView(dialog, launchableView, cuj)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index 15c348e..b0d897d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -26,11 +26,14 @@
import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel
import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/**
@@ -50,49 +53,132 @@
callChipViewModel: CallChipViewModel,
@StatusBarChipsLog private val logger: LogBuffer,
) {
+ private enum class ChipType {
+ ScreenRecord,
+ ShareToApp,
+ CastToOtherDevice,
+ Call,
+ }
+
+ /** Model that helps us internally track the various chip states from each of the types. */
+ private sealed interface InternalChipModel {
+ /**
+ * Represents that we've internally decided to show the chip with type [type] with the given
+ * [model] information.
+ */
+ data class Shown(val type: ChipType, val model: OngoingActivityChipModel.Shown) :
+ InternalChipModel
+
+ /**
+ * Represents that all chip types would like to be hidden. Each value specifies *how* that
+ * chip type should get hidden.
+ */
+ data class Hidden(
+ val screenRecord: OngoingActivityChipModel.Hidden,
+ val shareToApp: OngoingActivityChipModel.Hidden,
+ val castToOtherDevice: OngoingActivityChipModel.Hidden,
+ val call: OngoingActivityChipModel.Hidden,
+ ) : InternalChipModel
+ }
+
+ private val internalChip: Flow<InternalChipModel> =
+ combine(
+ screenRecordChipViewModel.chip,
+ shareToAppChipViewModel.chip,
+ castToOtherDeviceChipViewModel.chip,
+ callChipViewModel.chip,
+ ) { screenRecord, shareToApp, castToOtherDevice, call ->
+ logger.log(
+ TAG,
+ LogLevel.INFO,
+ {
+ str1 = screenRecord.logName
+ str2 = shareToApp.logName
+ str3 = castToOtherDevice.logName
+ },
+ { "Chips: ScreenRecord=$str1 > ShareToApp=$str2 > CastToOther=$str3..." },
+ )
+ logger.log(TAG, LogLevel.INFO, { str1 = call.logName }, { "... > Call=$str1" })
+ // This `when` statement shows the priority order of the chips.
+ when {
+ // Screen recording also activates the media projection APIs, so whenever the
+ // screen recording chip is active, the media projection chip would also be
+ // active. We want the screen-recording-specific chip shown in this case, so we
+ // give the screen recording chip priority. See b/296461748.
+ screenRecord is OngoingActivityChipModel.Shown ->
+ InternalChipModel.Shown(ChipType.ScreenRecord, screenRecord)
+ shareToApp is OngoingActivityChipModel.Shown ->
+ InternalChipModel.Shown(ChipType.ShareToApp, shareToApp)
+ castToOtherDevice is OngoingActivityChipModel.Shown ->
+ InternalChipModel.Shown(ChipType.CastToOtherDevice, castToOtherDevice)
+ call is OngoingActivityChipModel.Shown ->
+ InternalChipModel.Shown(ChipType.Call, call)
+ else -> {
+ // We should only get here if all chip types are hidden
+ check(screenRecord is OngoingActivityChipModel.Hidden)
+ check(shareToApp is OngoingActivityChipModel.Hidden)
+ check(castToOtherDevice is OngoingActivityChipModel.Hidden)
+ check(call is OngoingActivityChipModel.Hidden)
+ InternalChipModel.Hidden(
+ screenRecord = screenRecord,
+ shareToApp = shareToApp,
+ castToOtherDevice = castToOtherDevice,
+ call = call,
+ )
+ }
+ }
+ }
+
/**
* A flow modeling the chip that should be shown in the status bar after accounting for possibly
- * multiple ongoing activities.
+ * multiple ongoing activities and animation requirements.
*
* [com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment] is responsible for
* actually displaying the chip.
*/
val chip: StateFlow<OngoingActivityChipModel> =
- combine(
- screenRecordChipViewModel.chip,
- shareToAppChipViewModel.chip,
- castToOtherDeviceChipViewModel.chip,
- callChipViewModel.chip,
- ) { screenRecord, shareToApp, castToOtherDevice, call ->
- logger.log(
- TAG,
- LogLevel.INFO,
- {
- str1 = screenRecord.logName
- str2 = shareToApp.logName
- str3 = castToOtherDevice.logName
- },
- { "Chips: ScreenRecord=$str1 > ShareToApp=$str2 > CastToOther=$str3..." },
- )
- logger.log(TAG, LogLevel.INFO, { str1 = call.logName }, { "... > Call=$str1" })
- // This `when` statement shows the priority order of the chips
- when {
- // Screen recording also activates the media projection APIs, so whenever the
- // screen recording chip is active, the media projection chip would also be
- // active. We want the screen-recording-specific chip shown in this case, so we
- // give the screen recording chip priority. See b/296461748.
- screenRecord is OngoingActivityChipModel.Shown -> screenRecord
- shareToApp is OngoingActivityChipModel.Shown -> shareToApp
- castToOtherDevice is OngoingActivityChipModel.Shown -> castToOtherDevice
- else -> call
+ internalChip
+ .pairwise(initialValue = DEFAULT_INTERNAL_HIDDEN_MODEL)
+ .map { (old, new) ->
+ if (old is InternalChipModel.Shown && new is InternalChipModel.Hidden) {
+ // If we're transitioning from showing the chip to hiding the chip, different
+ // chips require different animation behaviors. For example, the screen share
+ // chips shouldn't animate if the user stopped the screen share from the dialog
+ // (see b/353249803#comment4), but the call chip should always animate.
+ //
+ // This `when` block makes sure that when we're transitioning from Shown to
+ // Hidden, we check what chip type was previously showing and we use that chip
+ // type's hide animation behavior.
+ when (old.type) {
+ ChipType.ScreenRecord -> new.screenRecord
+ ChipType.ShareToApp -> new.shareToApp
+ ChipType.CastToOtherDevice -> new.castToOtherDevice
+ ChipType.Call -> new.call
+ }
+ } else if (new is InternalChipModel.Shown) {
+ // If we have a chip to show, always show it.
+ new.model
+ } else {
+ // In the Hidden -> Hidden transition, it shouldn't matter which hidden model we
+ // choose because no animation should happen regardless.
+ OngoingActivityChipModel.Hidden()
}
}
// Some of the chips could have timers in them and we don't want the start time
// for those timers to get reset for any reason. So, as soon as any subscriber has
- // requested the chip information, we need to maintain it forever. See b/347726238.
- .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
+ // requested the chip information, we maintain it forever by using
+ // [SharingStarted.Lazily]. See b/347726238.
+ .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden())
companion object {
private const val TAG = "ChipsViewModel"
+
+ private val DEFAULT_INTERNAL_HIDDEN_MODEL =
+ InternalChipModel.Hidden(
+ screenRecord = OngoingActivityChipModel.Hidden(),
+ shareToApp = OngoingActivityChipModel.Hidden(),
+ castToOtherDevice = OngoingActivityChipModel.Hidden(),
+ call = OngoingActivityChipModel.Hidden(),
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index aced0be..0320a7a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -528,9 +528,10 @@
}
@Override
- public void onOngoingActivityStatusChanged(boolean hasOngoingActivity) {
+ public void onOngoingActivityStatusChanged(
+ boolean hasOngoingActivity, boolean shouldAnimate) {
mHasOngoingActivity = hasOngoingActivity;
- updateStatusBarVisibilities(/* animate= */ true);
+ updateStatusBarVisibilities(shouldAnimate);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index ae1898b..4c97854 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
@@ -122,7 +122,8 @@
// Notify listeners
listener.onOngoingActivityStatusChanged(
- hasOngoingActivity = true
+ hasOngoingActivity = true,
+ shouldAnimate = true,
)
}
is OngoingActivityChipModel.Hidden -> {
@@ -130,7 +131,8 @@
// b/192243808 and [Chronometer.start].
chipTimeView.stop()
listener.onOngoingActivityStatusChanged(
- hasOngoingActivity = false
+ hasOngoingActivity = false,
+ shouldAnimate = chipModel.shouldAnimate,
)
}
}
@@ -266,8 +268,13 @@
/** Called when a transition from lockscreen to dream has started. */
fun onTransitionFromLockscreenToDreamStarted()
- /** Called when the status of the ongoing activity chip (active or not active) has changed. */
- fun onOngoingActivityStatusChanged(hasOngoingActivity: Boolean)
+ /**
+ * Called when the status of the ongoing activity chip (active or not active) has changed.
+ *
+ * @param shouldAnimate true if the chip should animate in/out, and false if the chip should
+ * immediately appear/disappear.
+ */
+ fun onOngoingActivityStatusChanged(hasOngoingActivity: Boolean, shouldAnimate: Boolean)
/**
* Called when the scene state has changed such that the home status bar is newly allowed or no
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 79933ee..a94ef36 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -16,6 +16,7 @@
package com.android.keyguard
import android.content.BroadcastReceiver
+import android.platform.test.annotations.DisableFlags
import android.view.View
import android.view.ViewTreeObserver
import android.widget.FrameLayout
@@ -263,9 +264,9 @@
}
@Test
+ @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun keyguardCallback_visibilityChanged_clockDozeCalled() =
runBlocking(IMMEDIATE) {
- mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(captor))
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 1d78168..892375d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -29,6 +29,7 @@
import android.database.ContentObserver;
import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
import android.provider.Settings;
import android.view.View;
@@ -48,11 +49,10 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
+@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchControllerBaseTest {
@Test
public void testInit_viewAlreadyAttached() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
mController.init();
verifyAttachment(times(1));
@@ -60,8 +60,6 @@
@Test
public void testInit_viewNotYetAttached() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor =
ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
@@ -78,16 +76,12 @@
@Test
public void testInitSubControllers() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
mController.init();
verify(mKeyguardSliceViewController).init();
}
@Test
public void testInit_viewDetached() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor =
ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
mController.init();
@@ -101,8 +95,6 @@
@Test
public void testPluginPassesStatusBarState() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor =
ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class);
@@ -116,8 +108,6 @@
@Test
public void testSmartspaceEnabledRemovesKeyguardStatusArea() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mSmartspaceController.isEnabled()).thenReturn(true);
mController.init();
@@ -126,8 +116,6 @@
@Test
public void onLocaleListChangedRebuildsSmartspaceView() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mSmartspaceController.isEnabled()).thenReturn(true);
mController.init();
@@ -138,8 +126,6 @@
@Test
public void onLocaleListChanged_rebuildsSmartspaceViews_whenDecouplingEnabled() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mSmartspaceController.isEnabled()).thenReturn(true);
when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true);
mController.init();
@@ -153,8 +139,6 @@
@Test
public void testSmartspaceDisabledShowsKeyguardStatusArea() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mSmartspaceController.isEnabled()).thenReturn(false);
mController.init();
@@ -163,8 +147,6 @@
@Test
public void testRefresh() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
mController.refresh();
verify(mSmartspaceController).requestSmartspaceUpdate();
@@ -172,8 +154,6 @@
@Test
public void testChangeToDoubleLineClockSetsSmallClock() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mSecureSettings.getIntForUser(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 1,
UserHandle.USER_CURRENT))
.thenReturn(0);
@@ -197,15 +177,11 @@
@Test
public void testGetClock_ForwardsToClock() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
assertEquals(mClockController, mController.getClock());
}
@Test
public void testGetLargeClockBottom_returnsExpectedValue() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mLargeClockFrame.getVisibility()).thenReturn(View.VISIBLE);
when(mLargeClockFrame.getHeight()).thenReturn(100);
when(mSmallClockFrame.getHeight()).thenReturn(50);
@@ -218,8 +194,6 @@
@Test
public void testGetSmallLargeClockBottom_returnsExpectedValue() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mLargeClockFrame.getVisibility()).thenReturn(View.GONE);
when(mLargeClockFrame.getHeight()).thenReturn(100);
when(mSmallClockFrame.getHeight()).thenReturn(50);
@@ -232,16 +206,12 @@
@Test
public void testGetClockBottom_nullClock_returnsZero() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mClockEventController.getClock()).thenReturn(null);
assertEquals(0, mController.getClockBottom(10));
}
@Test
public void testChangeLockscreenWeatherEnabledSetsWeatherViewVisible() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mSmartspaceController.isWeatherEnabled()).thenReturn(true);
ArgumentCaptor<ContentObserver> observerCaptor =
ArgumentCaptor.forClass(ContentObserver.class);
@@ -260,8 +230,6 @@
@Test
public void testChangeClockDateWeatherEnabled_SetsDateWeatherViewVisibility() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor =
ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class);
when(mSmartspaceController.isEnabled()).thenReturn(true);
@@ -284,15 +252,11 @@
@Test
public void testGetClock_nullClock_returnsNull() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mClockEventController.getClock()).thenReturn(null);
assertNull(mController.getClock());
}
private void verifyAttachment(VerificationMode times) {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
verify(mClockRegistry, times).registerClockChangeListener(
any(ClockRegistry.ClockChangeListener.class));
verify(mClockEventController, times).registerListeners(mView);
@@ -300,8 +264,6 @@
@Test
public void testSplitShadeEnabledSetToSmartspaceController() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
mController.setSplitShadeEnabled(true);
verify(mSmartspaceController, times(1)).setSplitShadeEnabled(true);
verify(mSmartspaceController, times(0)).setSplitShadeEnabled(false);
@@ -309,8 +271,6 @@
@Test
public void testSplitShadeDisabledSetToSmartspaceController() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
mController.setSplitShadeEnabled(false);
verify(mSmartspaceController, times(1)).setSplitShadeEnabled(false);
verify(mSmartspaceController, times(0)).setSplitShadeEnabled(true);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
index 83443be..0bf9d12 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
@@ -30,6 +30,7 @@
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.platform.test.annotations.DisableFlags;
import android.testing.TestableLooper.RunWithLooper;
import android.util.AttributeSet;
import android.view.LayoutInflater;
@@ -60,6 +61,7 @@
// the main thread before acquiring a wake lock. This class is constructed when
// the keyguard_clock_switch layout is inflated.
@RunWithLooper(setAsMainLooper = true)
+@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public class KeyguardClockSwitchTest extends SysuiTestCase {
@Mock
ViewGroup mMockKeyguardSliceView;
@@ -81,8 +83,6 @@
@Before
public void setUp() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
MockitoAnnotations.initMocks(this);
when(mMockKeyguardSliceView.getContext()).thenReturn(mContext);
when(mMockKeyguardSliceView.findViewById(R.id.keyguard_status_area))
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LegacyLockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LegacyLockIconViewControllerBaseTest.java
index b09357f..c51aa04 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LegacyLockIconViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LegacyLockIconViewControllerBaseTest.java
@@ -151,6 +151,7 @@
if (!SceneContainerFlag.isEnabled()) {
mSetFlagsRule.disableFlags(Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR);
+ //TODO move this to use @DisableFlags annotation if needed
mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
index 693a877..7cc9185 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
@@ -18,6 +18,7 @@
package com.android.systemui.keyguard.ui.view.layout.sections
import android.graphics.Point
+import android.platform.test.annotations.DisableFlags
import android.view.WindowManager
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
@@ -106,8 +107,8 @@
}
@Test
+ @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun addViewsConditionally_migrateFlagOff() {
- mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
mSetFlagsRule.disableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
val constraintLayout = ConstraintLayout(context, null)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
index 201ee88..1c99eff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.ui.view.layout.sections
+import android.platform.test.annotations.EnableFlags
import android.view.View
import android.widget.LinearLayout
import androidx.constraintlayout.widget.ConstraintLayout
@@ -48,6 +49,7 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
+@EnableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
class SmartspaceSectionTest : SysuiTestCase() {
private lateinit var underTest: SmartspaceSection
@Mock private lateinit var keyguardClockViewModel: KeyguardClockViewModel
@@ -70,7 +72,6 @@
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
underTest =
SmartspaceSection(
mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index b80d1a4..8a6b68f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -18,7 +18,6 @@
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static com.android.keyguard.KeyguardClockSwitch.LARGE;
import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
import static com.google.common.truth.Truth.assertThat;
@@ -404,7 +403,6 @@
mFeatureFlags.set(Flags.QS_USER_DETAIL_SHORTCUT, false);
mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR);
- mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR);
mMainDispatcher = getMainDispatcher();
@@ -801,7 +799,6 @@
.setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
verify(mNotificationStackScrollLayoutController)
.setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture());
- verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true);
reset(mKeyguardStatusViewController);
when(mNotificationPanelViewControllerLazy.get())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 90e8ea5f..905cc4c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -92,6 +92,7 @@
* When the Back gesture starts (progress 0%), the scrim will stay at 100% scale (1.0f).
*/
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testBackGesture_min_scrimAtMaxScale() {
mNotificationPanelViewController.onBackProgressed(0.0f);
verify(mScrimController).applyBackScaling(1.0f);
@@ -101,6 +102,7 @@
* When the Back gesture is at max (progress 100%), the scrim will be scaled to its minimum.
*/
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testBackGesture_max_scrimAtMinScale() {
mNotificationPanelViewController.onBackProgressed(1.0f);
verify(mScrimController).applyBackScaling(
@@ -108,6 +110,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onNotificationHeightChangeWhileOnKeyguardWillComputeMaxKeyguardNotifications() {
mStatusBarStateController.setState(KEYGUARD);
ArgumentCaptor<OnHeightChangedListener> captor =
@@ -124,6 +127,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onNotificationHeightChangeWhileInShadeWillNotComputeMaxKeyguardNotifications() {
mStatusBarStateController.setState(SHADE);
ArgumentCaptor<OnHeightChangedListener> captor =
@@ -140,6 +144,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void computeMaxKeyguardNotifications_lockscreenToShade_returnsExistingMax() {
when(mAmbientState.getFractionToShade()).thenReturn(0.5f);
mNotificationPanelViewController.setMaxDisplayedNotifications(-1);
@@ -150,6 +155,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void computeMaxKeyguardNotifications_noTransition_updatesMax() {
when(mAmbientState.getFractionToShade()).thenReturn(0f);
mNotificationPanelViewController.setMaxDisplayedNotifications(-1);
@@ -196,6 +202,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void getVerticalSpaceForLockscreenShelf_useLockIconBottomPadding_returnsShelfHeight() {
enableSplitShade(/* enabled= */ false);
setBottomPadding(/* stackScrollLayoutBottom= */ 100,
@@ -213,6 +220,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void getVerticalSpaceForLockscreenShelf_useIndicationBottomPadding_returnsZero() {
enableSplitShade(/* enabled= */ false);
setBottomPadding(/* stackScrollLayoutBottom= */ 100,
@@ -230,6 +238,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void getVerticalSpaceForLockscreenShelf_useAmbientBottomPadding_returnsZero() {
enableSplitShade(/* enabled= */ false);
setBottomPadding(/* stackScrollLayoutBottom= */ 100,
@@ -247,6 +256,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void getVerticalSpaceForLockscreenShelf_useLockIconPadding_returnsLessThanShelfHeight() {
enableSplitShade(/* enabled= */ false);
setBottomPadding(/* stackScrollLayoutBottom= */ 100,
@@ -264,6 +274,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void getVerticalSpaceForLockscreenShelf_splitShade() {
enableSplitShade(/* enabled= */ true);
setBottomPadding(/* stackScrollLayoutBottom= */ 100,
@@ -281,6 +292,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testSetPanelScrimMinFractionWhenHeadsUpIsDragged() {
mNotificationPanelViewController.setHeadsUpDraggingStartingHeight(
mNotificationPanelViewController.getMaxPanelHeight() / 2);
@@ -288,6 +300,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testSetDozing_notifiesNsslAndStateController() {
mNotificationPanelViewController.setDozing(true /* dozing */, false /* animate */);
verify(mNotificationStackScrollLayoutController).setDozing(eq(true), eq(false));
@@ -295,6 +308,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testOnDozeAmountChanged_positionClockAndNotificationsUsesUdfpsLocation() {
// GIVEN UDFPS is enrolled and we're on the keyguard
final Point udfpsLocationCenter = new Point(0, 100);
@@ -332,12 +346,14 @@
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testSetExpandedHeight() {
mNotificationPanelViewController.setExpandedHeight(200);
assertThat((int) mNotificationPanelViewController.getExpandedHeight()).isEqualTo(200);
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testOnTouchEvent_expansionCanBeBlocked() {
onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0));
onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 200f, 0));
@@ -350,6 +366,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void test_pulsing_onTouchEvent_noTracking() {
// GIVEN device is pulsing
mNotificationPanelViewController.setPulsing(true);
@@ -367,6 +384,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void alternateBouncerVisible_onTouchEvent_notHandled() {
mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR);
// GIVEN alternate bouncer is visible
@@ -385,6 +403,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void test_onTouchEvent_startTracking() {
// GIVEN device is NOT pulsing
mNotificationPanelViewController.setPulsing(false);
@@ -402,9 +421,8 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onInterceptTouchEvent_nsslMigrationOff_userActivity() {
- mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
mTouchHandler.onInterceptTouchEvent(MotionEvent.obtain(0L /* downTime */,
0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */,
0 /* metaState */));
@@ -413,9 +431,8 @@
}
@Test
+ @EnableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onInterceptTouchEvent_nsslMigrationOn_userActivity_not_called() {
- mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
mTouchHandler.onInterceptTouchEvent(MotionEvent.obtain(0L /* downTime */,
0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */,
0 /* metaState */));
@@ -424,6 +441,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testOnTouchEvent_expansionResumesAfterBriefTouch() {
mFalsingManager.setIsClassifierEnabled(true);
mFalsingManager.setIsFalseTouch(false);
@@ -460,6 +478,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testA11y_initializeNode() {
AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo();
mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mView, nodeInfo);
@@ -473,6 +492,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testA11y_scrollForward() {
mAccessibilityDelegate.performAccessibilityAction(
mView,
@@ -483,6 +503,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testA11y_scrollUp() {
mAccessibilityDelegate.performAccessibilityAction(
mView,
@@ -493,6 +514,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testKeyguardStatusViewInSplitShade_changesConstraintsDependingOnNotifications() {
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -511,6 +533,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void keyguardStatusView_splitShade_dozing_alwaysDozingOn_isCentered() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -523,6 +546,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void keyguardStatusView_splitShade_dozing_alwaysDozingOff_isNotCentered() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -535,6 +559,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void keyguardStatusView_splitShade_notDozing_alwaysDozingOn_isNotCentered() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -547,6 +572,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void keyguardStatusView_splitShade_pulsing_isNotCentered() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -560,6 +586,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void keyguardStatusView_splitShade_notPulsing_isNotCentered() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -573,6 +600,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void keyguardStatusView_singleShade_isCentered() {
enableSplitShade(/* enabled= */ false);
// The conditions below would make the clock NOT be centered on split shade.
@@ -587,6 +615,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void keyguardStatusView_willPlayDelayedDoze_isCentered_thenNot() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -602,6 +631,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void keyguardStatusView_willPlayDelayedDoze_notifiesKeyguardMediaController() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -614,6 +644,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void keyguardStatusView_willPlayDelayedDoze_isCentered_thenStillCenteredIfNoNotifs() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false);
@@ -629,6 +660,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onKeyguardStatusViewHeightChange_animatesNextTopPaddingChangeForNSSL() {
ArgumentCaptor<View.OnLayoutChangeListener> captor =
ArgumentCaptor.forClass(View.OnLayoutChangeListener.class);
@@ -646,6 +678,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testCanCollapsePanelOnTouch_trueForKeyGuard() {
mStatusBarStateController.setState(KEYGUARD);
@@ -653,6 +686,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testCanCollapsePanelOnTouch_trueWhenScrolledToBottom() {
mStatusBarStateController.setState(SHADE);
when(mNotificationStackScrollLayoutController.isScrolledToBottom()).thenReturn(true);
@@ -661,6 +695,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testCanCollapsePanelOnTouch_trueWhenInSettings() {
mStatusBarStateController.setState(SHADE);
when(mQsController.getExpanded()).thenReturn(true);
@@ -669,6 +704,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testCanCollapsePanelOnTouch_falseInDualPaneShade() {
mStatusBarStateController.setState(SHADE);
enableSplitShade(/* enabled= */ true);
@@ -695,6 +731,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testCancelSwipeWhileLocked_notifiesKeyguardState() {
mStatusBarStateController.setState(KEYGUARD);
@@ -707,6 +744,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testSwipe_exactlyToTarget_notifiesNssl() {
// No over-expansion
mNotificationPanelViewController.setOverExpansion(0f);
@@ -722,6 +760,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testRotatingToSplitShadeWithQsExpanded_transitionsToShadeLocked() {
mStatusBarStateController.setState(KEYGUARD);
when(mQsController.getExpanded()).thenReturn(true);
@@ -732,6 +771,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testUnlockedSplitShadeTransitioningToKeyguard_closesQS() {
enableSplitShade(true);
mStatusBarStateController.setState(SHADE);
@@ -741,6 +781,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testLockedSplitShadeTransitioningToKeyguard_closesQS() {
enableSplitShade(true);
mStatusBarStateController.setState(SHADE_LOCKED);
@@ -750,6 +791,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testSwitchesToCorrectClockInSinglePaneShade() {
mStatusBarStateController.setState(KEYGUARD);
@@ -765,6 +807,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testSwitchesToCorrectClockInSplitShade() {
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -785,6 +828,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testHasNotifications_switchesToLargeClockWhenEnteringSplitShade() {
mStatusBarStateController.setState(KEYGUARD);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
@@ -796,6 +840,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testNoNotifications_switchesToLargeClockWhenEnteringSplitShade() {
mStatusBarStateController.setState(KEYGUARD);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
@@ -807,6 +852,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testHasNotifications_switchesToSmallClockWhenExitingSplitShade() {
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -820,6 +866,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testNoNotifications_switchesToLargeClockWhenExitingSplitShade() {
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -833,6 +880,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void clockSize_mediaShowing_inSplitShade_onAod_isLarge() {
when(mDozeParameters.getAlwaysOn()).thenReturn(true);
mStatusBarStateController.setState(KEYGUARD);
@@ -848,6 +896,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void clockSize_mediaShowing_inSplitShade_screenOff_notAod_isSmall() {
when(mDozeParameters.getAlwaysOn()).thenReturn(false);
mStatusBarStateController.setState(KEYGUARD);
@@ -863,6 +912,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onQsSetExpansionHeightCalled_qsFullyExpandedOnKeyguard_showNSSL() {
// GIVEN
mStatusBarStateController.setState(KEYGUARD);
@@ -883,6 +933,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onQsSetExpansionHeightCalled_qsFullyExpandedOnKeyguard_hideNSSL() {
// GIVEN
mStatusBarStateController.setState(KEYGUARD);
@@ -904,6 +955,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testSwitchesToBigClockInSplitShadeOnAodAnimateDisabled() {
when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(false);
mStatusBarStateController.setState(KEYGUARD);
@@ -919,6 +971,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void switchesToBigClockInSplitShadeOn_landFlagOn_ForceSmallClock() {
when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(false);
mStatusBarStateController.setState(KEYGUARD);
@@ -938,6 +991,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void switchesToBigClockInSplitShadeOn_landFlagOff_DontForceSmallClock() {
when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(false);
mStatusBarStateController.setState(KEYGUARD);
@@ -957,6 +1011,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testDisplaysSmallClockOnLockscreenInSplitShadeWhenMediaIsPlaying() {
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -978,6 +1033,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testFoldToAodAnimationCleansupInAnimationEnd() {
ArgumentCaptor<Animator.AnimatorListener> animCaptor =
ArgumentCaptor.forClass(Animator.AnimatorListener.class);
@@ -997,6 +1053,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testExpandWithQsMethodIsUsingLockscreenTransitionController() {
enableSplitShade(/* enabled= */ true);
mStatusBarStateController.setState(KEYGUARD);
@@ -1008,6 +1065,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void setKeyguardStatusBarAlpha_setsAlphaOnKeyguardStatusBarController() {
float statusBarAlpha = 0.5f;
@@ -1017,6 +1075,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testQsToBeImmediatelyExpandedWhenOpeningPanelInSplitShade() {
enableSplitShade(/* enabled= */ true);
mShadeExpansionStateManager.updateState(STATE_OPEN);
@@ -1030,6 +1089,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testQsNotToBeImmediatelyExpandedWhenGoingFromUnlockedToLocked() {
enableSplitShade(/* enabled= */ true);
mShadeExpansionStateManager.updateState(STATE_CLOSED);
@@ -1042,6 +1102,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testQsImmediateResetsWhenPanelOpensOrCloses() {
mShadeExpansionStateManager.updateState(STATE_OPEN);
mShadeExpansionStateManager.updateState(STATE_CLOSED);
@@ -1049,6 +1110,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testQsExpansionChangedToDefaultWhenRotatingFromOrToSplitShade() {
when(mCommandQueue.panelsEnabled()).thenReturn(true);
@@ -1065,6 +1127,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testPanelClosedWhenClosingQsInSplitShade() {
mShadeExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
/* expanded= */ true, /* tracking= */ false);
@@ -1078,6 +1141,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void getMaxPanelTransitionDistance_expanding_inSplitShade_returnsSplitShadeFullTransitionDistance() {
enableSplitShade(true);
mNotificationPanelViewController.expandToQs();
@@ -1088,6 +1152,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void isExpandingOrCollapsing_returnsTrue_whenQsLockscreenDragInProgress() {
when(mQsController.getLockscreenShadeDragProgress()).thenReturn(0.5f);
assertThat(mNotificationPanelViewController.isExpandingOrCollapsing()).isTrue();
@@ -1095,6 +1160,7 @@
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void getMaxPanelTransitionDistance_inSplitShade_withHeadsUp_returnsBiggerValue() {
enableSplitShade(true);
mNotificationPanelViewController.expandToQs();
@@ -1111,6 +1177,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void getMaxPanelTransitionDistance_expandingSplitShade_keyguard_returnsNonSplitShadeValue() {
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(true);
@@ -1122,6 +1189,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void getMaxPanelTransitionDistance_expanding_notSplitShade_returnsNonSplitShadeValue() {
enableSplitShade(false);
mNotificationPanelViewController.expandToQs();
@@ -1132,6 +1200,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onLayoutChange_fullWidth_updatesQSWithFullWithTrue() {
setIsFullWidth(true);
@@ -1139,6 +1208,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onLayoutChange_notFullWidth_updatesQSWithFullWithFalse() {
setIsFullWidth(false);
@@ -1146,6 +1216,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onLayoutChange_qsNotSet_doesNotCrash() {
mQuickSettingsController.setQs(null);
@@ -1153,6 +1224,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onEmptySpaceClicked_notDozingAndOnKeyguard_requestsFaceAuth() {
StatusBarStateController.StateListener statusBarStateListener =
mNotificationPanelViewController.getStatusBarStateListener();
@@ -1167,8 +1239,8 @@
}
@Test
+ @EnableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void nsslFlagEnabled_allowOnlyExternalTouches() {
- mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
// This sets the dozing state that is read when onMiddleClicked is eventually invoked.
mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
@@ -1179,6 +1251,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onSplitShadeChanged_duringShadeExpansion_resetsOverScrollState() {
// There was a bug where there was left-over overscroll state after going from split shade
// to single shade.
@@ -1200,6 +1273,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onSplitShadeChanged_alwaysResetsOverScrollState() {
enableSplitShade(true);
enableSplitShade(false);
@@ -1217,6 +1291,7 @@
* to ensure scrollY can be correctly set to be 0
*/
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onShadeFlingClosingEnd_mAmbientStateSetClose_thenOnExpansionStopped() {
// Given: Shade is expanded
mNotificationPanelViewController.notifyExpandingFinished();
@@ -1237,6 +1312,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onShadeFlingEnd_mExpandImmediateShouldBeReset() {
mNotificationPanelViewController.onFlingEnd(false);
@@ -1244,6 +1320,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void inUnlockedSplitShade_transitioningMaxTransitionDistance_makesShadeFullyExpanded() {
mStatusBarStateController.setState(SHADE);
enableSplitShade(true);
@@ -1253,6 +1330,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void shadeFullyExpanded_inShadeState() {
mStatusBarStateController.setState(SHADE);
@@ -1265,6 +1343,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void shadeFullyExpanded_onKeyguard() {
mStatusBarStateController.setState(KEYGUARD);
@@ -1274,12 +1353,14 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void shadeFullyExpanded_onShadeLocked() {
mStatusBarStateController.setState(SHADE_LOCKED);
assertThat(mNotificationPanelViewController.isShadeFullyExpanded()).isTrue();
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void shadeExpanded_whenHasHeight() {
int transitionDistance = mNotificationPanelViewController.getMaxPanelTransitionDistance();
mNotificationPanelViewController.setExpandedHeight(transitionDistance);
@@ -1287,6 +1368,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void shadeExpanded_whenInstantExpanding() {
mNotificationPanelViewController.expand(true);
assertThat(mNotificationPanelViewController.isExpanded()).isTrue();
@@ -1300,12 +1382,14 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void shadeExpanded_whenUnlockedOffscreenAnimationRunning() {
when(mUnlockedScreenOffAnimationController.isAnimationPlaying()).thenReturn(true);
assertThat(mNotificationPanelViewController.isExpanded()).isTrue();
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void shadeExpanded_whenInputFocusTransferStarted() {
when(mCommandQueue.panelsEnabled()).thenReturn(true);
@@ -1315,6 +1399,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void shadeNotExpanded_whenInputFocusTransferStartedButPanelsDisabled() {
when(mCommandQueue.panelsEnabled()).thenReturn(false);
@@ -1324,6 +1409,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void cancelInputFocusTransfer_shadeCollapsed() {
when(mCommandQueue.panelsEnabled()).thenReturn(true);
mNotificationPanelViewController.startInputFocusTransfer();
@@ -1334,6 +1420,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void finishInputFocusTransfer_shadeFlingingOpen() {
when(mCommandQueue.panelsEnabled()).thenReturn(true);
mNotificationPanelViewController.startInputFocusTransfer();
@@ -1344,6 +1431,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void getFalsingThreshold_deviceNotInteractive_isQsThreshold() {
PowerInteractor.Companion.setAsleepForTest(
mPowerInteractor, PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON);
@@ -1353,6 +1441,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void getFalsingThreshold_lastWakeNotDueToTouch_isQsThreshold() {
PowerInteractor.Companion.setAwakeForTest(
mPowerInteractor, PowerManager.WAKE_REASON_POWER_BUTTON);
@@ -1362,6 +1451,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void getFalsingThreshold_lastWakeDueToTouch_greaterThanQsThreshold() {
PowerInteractor.Companion.setAwakeForTest(mPowerInteractor, PowerManager.WAKE_REASON_TAP);
when(mQsController.getFalsingThreshold()).thenReturn(14);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
index e1d92e7..52af907 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -18,6 +18,7 @@
package com.android.systemui.shade
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper
import android.view.HapticFeedbackConstants
@@ -27,6 +28,7 @@
import androidx.test.filters.SmallTest
import com.android.internal.util.CollectionUtils
import com.android.keyguard.KeyguardClockSwitch.LARGE
+import com.android.systemui.Flags
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarState.KEYGUARD
@@ -58,6 +60,7 @@
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
+@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
class NotificationPanelViewControllerWithCoroutinesTest :
NotificationPanelViewControllerBaseTest() {
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 74a2999..6f2302a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -17,6 +17,8 @@
package com.android.systemui.shade
import android.content.Context
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.flag.junit.FlagsParameterization
import android.testing.TestableLooper
@@ -31,6 +33,7 @@
import com.android.keyguard.LegacyLockIconViewController
import com.android.keyguard.dagger.KeyguardBouncerComponent
import com.android.systemui.Flags
+import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
@@ -398,8 +401,8 @@
@Test
@DisableSceneContainer
+ @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun handleDispatchTouchEvent_nsslMigrationOff_userActivity_not_called() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
underTest.setStatusBarViewController(phoneStatusBarViewController)
interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)
@@ -408,8 +411,8 @@
}
@Test
+ @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun handleDispatchTouchEvent_nsslMigrationOn_userActivity() {
- enableMigrateClocksFlag()
underTest.setStatusBarViewController(phoneStatusBarViewController)
interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)
@@ -440,6 +443,7 @@
}
@Test
+ @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun shouldInterceptTouchEvent_dozing_touchInLockIconArea_touchNotIntercepted() {
// GIVEN dozing
whenever(sysuiStatusBarStateController.isDozing).thenReturn(true)
@@ -452,13 +456,12 @@
// AND the lock icon wants the touch
whenever(lockIconViewController.willHandleTouchWhileDozing(DOWN_EVENT)).thenReturn(true)
- enableMigrateClocksFlag()
-
// THEN touch should NOT be intercepted by NotificationShade
assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isFalse()
}
@Test
+ @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun shouldInterceptTouchEvent_dozing_touchNotInLockIconArea_touchIntercepted() {
// GIVEN dozing
whenever(sysuiStatusBarStateController.isDozing).thenReturn(true)
@@ -471,13 +474,12 @@
whenever(quickSettingsController.shouldQuickSettingsIntercept(any(), any(), any()))
.thenReturn(false)
- enableMigrateClocksFlag()
-
// THEN touch should be intercepted by NotificationShade
assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue()
}
@Test
+ @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun shouldInterceptTouchEvent_dozing_touchInStatusBar_touchIntercepted() {
// GIVEN dozing
whenever(sysuiStatusBarStateController.isDozing).thenReturn(true)
@@ -490,13 +492,12 @@
whenever(quickSettingsController.shouldQuickSettingsIntercept(any(), any(), any()))
.thenReturn(true)
- enableMigrateClocksFlag()
-
// THEN touch should be intercepted by NotificationShade
assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue()
}
@Test
+ @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun shouldInterceptTouchEvent_dozingAndPulsing_touchIntercepted() {
// GIVEN dozing
whenever(sysuiStatusBarStateController.isDozing).thenReturn(true)
@@ -517,8 +518,6 @@
whenever(shadeViewController.handleExternalInterceptTouch(DOWN_EVENT))
.thenReturn(true)
- enableMigrateClocksFlag()
-
// THEN touch should be intercepted by NotificationShade
assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue()
}
@@ -652,19 +651,13 @@
}
@Test
+ @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun cancelCurrentTouch_callsDragDownHelper() {
- enableMigrateClocksFlag()
underTest.cancelCurrentTouch()
verify(dragDownHelper).stopDragging()
}
- private fun enableMigrateClocksFlag() {
- if (!Flags.migrateClocksToBlueprint()) {
- mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
- }
- }
-
companion object {
private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
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 fec7424..ca29dd9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade
import android.os.SystemClock
+import android.platform.test.annotations.DisableFlags
import android.testing.TestableLooper.RunWithLooper
import android.view.MotionEvent
import android.widget.FrameLayout
@@ -208,9 +209,9 @@
}
@Test
+ @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun testDragDownHelperCalledWhenDraggingDown() =
testScope.runTest {
- mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
whenever(dragDownHelper.isDraggingDown).thenReturn(true)
val now = SystemClock.elapsedRealtime()
val ev = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, 0f, 0f, 0 /* meta */)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index c9a7c82..02764f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
@@ -16,9 +16,13 @@
package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel
+import android.content.DialogInterface
import android.view.View
import androidx.test.filters.SmallTest
+import com.android.internal.jank.Cuj
import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.mockDialogTransitionAnimator
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
@@ -37,6 +41,8 @@
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.android.systemui.statusbar.policy.CastDevice
@@ -45,7 +51,10 @@
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
import org.junit.Before
+import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -60,6 +69,16 @@
private val mockScreenCastDialog = mock<SystemUIDialog>()
private val mockGenericCastDialog = mock<SystemUIDialog>()
+ private val chipBackgroundView = mock<ChipBackgroundContainer>()
+ private val chipView =
+ mock<View>().apply {
+ whenever(
+ this.requireViewById<ChipBackgroundContainer>(
+ R.id.ongoing_activity_chip_background
+ )
+ )
+ .thenReturn(chipBackgroundView)
+ }
private val underTest = kosmos.castToOtherDeviceChipViewModel
@@ -193,6 +212,63 @@
}
@Test
+ fun chip_projectionStoppedFromDialog_chipImmediatelyHidden() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+
+ // WHEN the stop action on the dialog is clicked
+ val dialogStopAction =
+ getStopActionFromDialog(latest, chipView, mockScreenCastDialog, kosmos)
+ dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+ // THEN the chip is immediately hidden...
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+ // ...even though the repo still says it's projecting
+ assertThat(mediaProjectionRepo.mediaProjectionState.value)
+ .isInstanceOf(MediaProjectionState.Projecting::class.java)
+
+ // AND we specify no animation
+ assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse()
+ }
+
+ @Test
+ fun chip_routeStoppedFromDialog_chipImmediatelyHidden() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaRouterRepo.castDevices.value =
+ listOf(
+ CastDevice(
+ state = CastDevice.CastState.Connected,
+ id = "id",
+ name = "name",
+ description = "desc",
+ origin = CastDevice.CastOrigin.MediaRouter,
+ )
+ )
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+
+ // WHEN the stop action on the dialog is clicked
+ val dialogStopAction =
+ getStopActionFromDialog(latest, chipView, mockGenericCastDialog, kosmos)
+ dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+ // THEN the chip is immediately hidden...
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+ // ...even though the repo still says it's projecting
+ assertThat(mediaRouterRepo.castDevices.value).isNotEmpty()
+
+ // AND we specify no animation
+ assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse()
+ }
+
+ @Test
fun chip_colorsAreRed() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -297,8 +373,14 @@
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
assertThat(clickListener).isNotNull()
- clickListener!!.onClick(mock<View>())
- verify(mockScreenCastDialog).show()
+ clickListener!!.onClick(chipView)
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockScreenCastDialog),
+ eq(chipBackgroundView),
+ any(),
+ anyBoolean(),
+ )
}
@Test
@@ -316,8 +398,14 @@
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
assertThat(clickListener).isNotNull()
- clickListener!!.onClick(mock<View>())
- verify(mockScreenCastDialog).show()
+ clickListener!!.onClick(chipView)
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockScreenCastDialog),
+ eq(chipBackgroundView),
+ any(),
+ anyBoolean(),
+ )
}
@Test
@@ -339,7 +427,70 @@
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
assertThat(clickListener).isNotNull()
- clickListener!!.onClick(mock<View>())
- verify(mockGenericCastDialog).show()
+ clickListener!!.onClick(chipView)
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockGenericCastDialog),
+ eq(chipBackgroundView),
+ any(),
+ anyBoolean(),
+ )
+ }
+
+ @Test
+ fun chip_projectionStateCasting_clickListenerHasCuj() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
+
+ val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ clickListener!!.onClick(chipView)
+
+ val cujCaptor = argumentCaptor<DialogCuj>()
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ any(),
+ any(),
+ cujCaptor.capture(),
+ anyBoolean(),
+ )
+
+ assertThat(cujCaptor.firstValue.cujType)
+ .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
+ assertThat(cujCaptor.firstValue.tag).contains("Cast")
+ }
+
+ @Test
+ fun chip_routerStateCasting_clickListenerHasCuj() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaRouterRepo.castDevices.value =
+ listOf(
+ CastDevice(
+ state = CastDevice.CastState.Connected,
+ id = "id",
+ name = "name",
+ description = "desc",
+ origin = CastDevice.CastOrigin.MediaRouter,
+ )
+ )
+
+ val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ clickListener!!.onClick(chipView)
+
+ val cujCaptor = argumentCaptor<DialogCuj>()
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ any(),
+ any(),
+ cujCaptor.capture(),
+ anyBoolean(),
+ )
+
+ assertThat(cujCaptor.firstValue.cujType)
+ .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
+ assertThat(cujCaptor.firstValue.tag).contains("Cast")
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
index 4728c64..b4a37ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
@@ -16,9 +16,13 @@
package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel
+import android.content.DialogInterface
import android.view.View
import androidx.test.filters.SmallTest
+import com.android.internal.jank.Cuj
import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.mockDialogTransitionAnimator
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
@@ -30,9 +34,13 @@
import com.android.systemui.res.R
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
import com.android.systemui.screenrecord.data.repository.screenRecordRepository
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.android.systemui.util.time.fakeSystemClock
@@ -40,7 +48,10 @@
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
import org.junit.Before
+import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -53,11 +64,22 @@
private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository
private val systemClock = kosmos.fakeSystemClock
private val mockSystemUIDialog = mock<SystemUIDialog>()
+ private val chipBackgroundView = mock<ChipBackgroundContainer>()
+ private val chipView =
+ mock<View>().apply {
+ whenever(
+ this.requireViewById<ChipBackgroundContainer>(
+ R.id.ongoing_activity_chip_background
+ )
+ )
+ .thenReturn(chipBackgroundView)
+ }
private val underTest = kosmos.screenRecordChipViewModel
@Before
fun setUp() {
+ setUpPackageManagerForMediaProjection(kosmos)
whenever(kosmos.mockSystemUIDialogFactory.create(any<EndScreenRecordingDialogDelegate>()))
.thenReturn(mockSystemUIDialog)
}
@@ -132,6 +154,40 @@
}
@Test
+ fun chip_recordingStoppedFromDialog_screenRecordAndShareToAppChipImmediatelyHidden() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ val latestShareToApp by collectLastValue(kosmos.shareToAppChipViewModel.chip)
+
+ // On real devices, when screen recording is active then share-to-app is also active
+ // because screen record is just a special case of share-to-app where the app receiving
+ // the share is SysUI
+ screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen("fake.package")
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+ assertThat(latestShareToApp).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+
+ // WHEN the stop action on the dialog is clicked
+ val dialogStopAction =
+ getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos)
+ dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+ // THEN both the screen record chip and the share-to-app chip are immediately hidden...
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+ assertThat(latestShareToApp).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+ // ...even though the repos still say it's recording
+ assertThat(screenRecordRepo.screenRecordState.value)
+ .isEqualTo(ScreenRecordModel.Recording)
+ assertThat(mediaProjectionRepo.mediaProjectionState.value)
+ .isInstanceOf(MediaProjectionState.Projecting::class.java)
+
+ // AND we specify no animation
+ assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse()
+ }
+
+ @Test
fun chip_startingState_colorsAreRed() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -182,9 +238,15 @@
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
assertThat(clickListener).isNotNull()
- clickListener!!.onClick(mock<View>())
+ clickListener!!.onClick(chipView)
// EndScreenRecordingDialogDelegate will test that the dialog has the right message
- verify(mockSystemUIDialog).show()
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockSystemUIDialog),
+ eq(chipBackgroundView),
+ any(),
+ anyBoolean(),
+ )
}
@Test
@@ -198,9 +260,15 @@
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
assertThat(clickListener).isNotNull()
- clickListener!!.onClick(mock<View>())
+ clickListener!!.onClick(chipView)
// EndScreenRecordingDialogDelegate will test that the dialog has the right message
- verify(mockSystemUIDialog).show()
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockSystemUIDialog),
+ eq(chipBackgroundView),
+ any(),
+ anyBoolean(),
+ )
}
@Test
@@ -218,8 +286,39 @@
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
assertThat(clickListener).isNotNull()
- clickListener!!.onClick(mock<View>())
+ clickListener!!.onClick(chipView)
// EndScreenRecordingDialogDelegate will test that the dialog has the right message
- verify(mockSystemUIDialog).show()
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockSystemUIDialog),
+ eq(chipBackgroundView),
+ any(),
+ anyBoolean(),
+ )
+ }
+
+ @Test
+ fun chip_clickListenerHasCuj() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen("host.package")
+
+ val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ clickListener!!.onClick(chipView)
+
+ val cujCaptor = argumentCaptor<DialogCuj>()
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ any(),
+ any(),
+ cujCaptor.capture(),
+ anyBoolean(),
+ )
+
+ assertThat(cujCaptor.firstValue.cujType)
+ .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
+ assertThat(cujCaptor.firstValue.tag).contains("Screen record")
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index f87b17d..2658679 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
@@ -16,9 +16,13 @@
package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel
+import android.content.DialogInterface
import android.view.View
import androidx.test.filters.SmallTest
+import com.android.internal.jank.Cuj
import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.mockDialogTransitionAnimator
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
@@ -34,6 +38,8 @@
import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.android.systemui.util.time.fakeSystemClock
@@ -41,7 +47,10 @@
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
import org.junit.Before
+import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -54,6 +63,16 @@
private val systemClock = kosmos.fakeSystemClock
private val mockShareDialog = mock<SystemUIDialog>()
+ private val chipBackgroundView = mock<ChipBackgroundContainer>()
+ private val chipView =
+ mock<View>().apply {
+ whenever(
+ this.requireViewById<ChipBackgroundContainer>(
+ R.id.ongoing_activity_chip_background
+ )
+ )
+ .thenReturn(chipBackgroundView)
+ }
private val underTest = kosmos.shareToAppChipViewModel
@@ -134,6 +153,31 @@
}
@Test
+ fun chip_shareStoppedFromDialog_chipImmediatelyHidden() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+
+ // WHEN the stop action on the dialog is clicked
+ val dialogStopAction =
+ getStopActionFromDialog(latest, chipView, mockShareDialog, kosmos)
+ dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+ // THEN the chip is immediately hidden...
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+ // ...even though the repo still says it's projecting
+ assertThat(mediaProjectionRepo.mediaProjectionState.value)
+ .isInstanceOf(MediaProjectionState.Projecting::class.java)
+
+ // AND we specify no animation
+ assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse()
+ }
+
+ @Test
fun chip_colorsAreRed() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -181,8 +225,14 @@
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
assertThat(clickListener).isNotNull()
- clickListener!!.onClick(mock<View>())
- verify(mockShareDialog).show()
+ clickListener!!.onClick(chipView)
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockShareDialog),
+ eq(chipBackgroundView),
+ any(),
+ anyBoolean(),
+ )
}
@Test
@@ -199,7 +249,41 @@
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
assertThat(clickListener).isNotNull()
- clickListener!!.onClick(mock<View>())
- verify(mockShareDialog).show()
+ clickListener!!.onClick(chipView)
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockShareDialog),
+ eq(chipBackgroundView),
+ any(),
+ anyBoolean(),
+ )
+ }
+
+ @Test
+ fun chip_clickListenerHasCuj() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ NORMAL_PACKAGE,
+ hostDeviceName = null,
+ createTask(taskId = 1),
+ )
+
+ val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ clickListener!!.onClick(chipView)
+
+ val cujCaptor = argumentCaptor<DialogCuj>()
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ any(),
+ any(),
+ cujCaptor.capture(),
+ anyBoolean(),
+ )
+
+ assertThat(cujCaptor.firstValue.cujType)
+ .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
+ assertThat(cujCaptor.firstValue.tag).contains("Share")
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
new file mode 100644
index 0000000..b9049e8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2024 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.statusbar.chips.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.ui.model.ColorsModel
+import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class ChipTransitionHelperTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val testScope = kosmos.testScope
+
+ @Test
+ fun createChipFlow_typicallyFollowsInputFlow() =
+ testScope.runTest {
+ val underTest = ChipTransitionHelper(kosmos.applicationCoroutineScope)
+ val inputChipFlow =
+ MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden())
+ val latest by collectLastValue(underTest.createChipFlow(inputChipFlow))
+
+ val newChip =
+ OngoingActivityChipModel.Shown.Timer(
+ icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ colors = ColorsModel.Themed,
+ startTimeMs = 100L,
+ onClickListener = null,
+ )
+
+ inputChipFlow.value = newChip
+
+ assertThat(latest).isEqualTo(newChip)
+
+ val newerChip =
+ OngoingActivityChipModel.Shown.IconOnly(
+ icon = Icon.Resource(R.drawable.ic_hotspot, contentDescription = null),
+ colors = ColorsModel.Themed,
+ onClickListener = null,
+ )
+
+ inputChipFlow.value = newerChip
+
+ assertThat(latest).isEqualTo(newerChip)
+ }
+
+ @Test
+ fun activityStopped_chipHiddenWithoutAnimationFor500ms() =
+ testScope.runTest {
+ val underTest = ChipTransitionHelper(kosmos.applicationCoroutineScope)
+ val inputChipFlow =
+ MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden())
+ val latest by collectLastValue(underTest.createChipFlow(inputChipFlow))
+
+ val shownChip =
+ OngoingActivityChipModel.Shown.Timer(
+ icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ colors = ColorsModel.Themed,
+ startTimeMs = 100L,
+ onClickListener = null,
+ )
+
+ inputChipFlow.value = shownChip
+
+ assertThat(latest).isEqualTo(shownChip)
+
+ // WHEN #onActivityStopped is invoked
+ underTest.onActivityStoppedFromDialog()
+ runCurrent()
+
+ // THEN the chip is hidden and has no animation
+ assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+
+ // WHEN only 250ms have elapsed
+ advanceTimeBy(250)
+
+ // THEN the chip is still hidden
+ assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+
+ // WHEN over 500ms have elapsed
+ advanceTimeBy(251)
+
+ // THEN the chip returns to the original input flow value
+ assertThat(latest).isEqualTo(shownChip)
+ }
+
+ @Test
+ fun activityStopped_stoppedAgainBefore500ms_chipReshownAfterSecond500ms() =
+ testScope.runTest {
+ val underTest = ChipTransitionHelper(kosmos.applicationCoroutineScope)
+ val inputChipFlow =
+ MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden())
+ val latest by collectLastValue(underTest.createChipFlow(inputChipFlow))
+
+ val shownChip =
+ OngoingActivityChipModel.Shown.Timer(
+ icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ colors = ColorsModel.Themed,
+ startTimeMs = 100L,
+ onClickListener = null,
+ )
+
+ inputChipFlow.value = shownChip
+
+ assertThat(latest).isEqualTo(shownChip)
+
+ // WHEN #onActivityStopped is invoked
+ underTest.onActivityStoppedFromDialog()
+ runCurrent()
+
+ // THEN the chip is hidden and has no animation
+ assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+
+ // WHEN 250ms have elapsed, get another stop event
+ advanceTimeBy(250)
+ underTest.onActivityStoppedFromDialog()
+ runCurrent()
+
+ // THEN the chip is still hidden for another 500ms afterwards
+ assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+ advanceTimeBy(499)
+ assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+ advanceTimeBy(2)
+ assertThat(latest).isEqualTo(shownChip)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
index ca043f1..6e4d886 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
@@ -18,32 +18,58 @@
import android.view.View
import androidx.test.filters.SmallTest
+import com.android.internal.jank.Cuj
import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener
import com.android.systemui.statusbar.phone.SystemUIDialog
import kotlin.test.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@SmallTest
class OngoingActivityChipViewModelTest : SysuiTestCase() {
private val mockSystemUIDialog = mock<SystemUIDialog>()
private val dialogDelegate = SystemUIDialog.Delegate { mockSystemUIDialog }
+ private val dialogTransitionAnimator = mock<DialogTransitionAnimator>()
+
+ private val chipBackgroundView = mock<ChipBackgroundContainer>()
+ private val chipView =
+ mock<View>().apply {
+ whenever(
+ this.requireViewById<ChipBackgroundContainer>(
+ R.id.ongoing_activity_chip_background
+ )
+ )
+ .thenReturn(chipBackgroundView)
+ }
@Test
fun createDialogLaunchOnClickListener_showsDialogOnClick() {
+ val cuj = DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Test")
val clickListener =
createDialogLaunchOnClickListener(
dialogDelegate,
+ dialogTransitionAnimator,
+ cuj,
logcatLogBuffer("OngoingActivityChipViewModelTest"),
"tag",
)
- // Dialogs must be created on the main thread
- context.mainExecutor.execute {
- clickListener.onClick(mock<View>())
- verify(mockSystemUIDialog).show()
- }
+ clickListener.onClick(chipView)
+ verify(dialogTransitionAnimator)
+ .showFromView(
+ eq(mockSystemUIDialog),
+ eq(chipBackgroundView),
+ eq(cuj),
+ anyBoolean(),
+ )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index b1a8d0b..ee249f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -16,6 +16,10 @@
package com.android.systemui.statusbar.chips.ui.viewmodel
+import android.content.DialogInterface
+import android.content.packageManager
+import android.content.pm.PackageManager
+import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -33,10 +37,14 @@
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.runCurrent
@@ -44,9 +52,17 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
class OngoingActivityChipsViewModelTest : SysuiTestCase() {
private val kosmos = Kosmos().also { it.testCase = this }
private val testScope = kosmos.testScope
@@ -56,6 +72,18 @@
private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState
private val callRepo = kosmos.ongoingCallRepository
+ private val mockSystemUIDialog = mock<SystemUIDialog>()
+ private val chipBackgroundView = mock<ChipBackgroundContainer>()
+ private val chipView =
+ mock<View>().apply {
+ whenever(
+ this.requireViewById<ChipBackgroundContainer>(
+ R.id.ongoing_activity_chip_background
+ )
+ )
+ .thenReturn(chipBackgroundView)
+ }
+
private val underTest = kosmos.ongoingActivityChipsViewModel
@Before
@@ -72,7 +100,7 @@
val latest by collectLastValue(underTest.chip)
- assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden)
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
}
@Test
@@ -230,7 +258,81 @@
job2.cancel()
}
+ @Test
+ fun chip_screenRecordStoppedViaDialog_chipHiddenWithoutAnimation() =
+ testScope.runTest {
+ screenRecordState.value = ScreenRecordModel.Recording
+ mediaProjectionState.value = MediaProjectionState.NotProjecting
+ callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+ val latest by collectLastValue(underTest.chip)
+
+ assertIsScreenRecordChip(latest)
+
+ // WHEN screen record gets stopped via dialog
+ val dialogStopAction =
+ getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos)
+ dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+ // THEN the chip is immediately hidden with no animation
+ assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+ }
+
+ @Test
+ fun chip_projectionStoppedViaDialog_chipHiddenWithoutAnimation() =
+ testScope.runTest {
+ mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+ screenRecordState.value = ScreenRecordModel.DoingNothing
+ callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+ val latest by collectLastValue(underTest.chip)
+
+ assertIsShareToAppChip(latest)
+
+ // WHEN media projection gets stopped via dialog
+ val dialogStopAction =
+ getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos)
+ dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+ // THEN the chip is immediately hidden with no animation
+ assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+ }
+
companion object {
+ /**
+ * Assuming that the click listener in [latest] opens a dialog, this fetches the action
+ * associated with the positive button, which we assume is the "Stop sharing" action.
+ */
+ fun getStopActionFromDialog(
+ latest: OngoingActivityChipModel?,
+ chipView: View,
+ dialog: SystemUIDialog,
+ kosmos: Kosmos
+ ): DialogInterface.OnClickListener {
+ // Capture the action that would get invoked when the user clicks "Stop" on the dialog
+ lateinit var dialogStopAction: DialogInterface.OnClickListener
+ Mockito.doAnswer {
+ val delegate = it.arguments[0] as SystemUIDialog.Delegate
+ delegate.beforeCreate(dialog, /* savedInstanceState= */ null)
+
+ val stopActionCaptor = argumentCaptor<DialogInterface.OnClickListener>()
+ verify(dialog).setPositiveButton(any(), stopActionCaptor.capture())
+ dialogStopAction = stopActionCaptor.firstValue
+
+ return@doAnswer dialog
+ }
+ .whenever(kosmos.mockSystemUIDialogFactory)
+ .create(any<SystemUIDialog.Delegate>())
+ whenever(kosmos.packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>()))
+ .thenThrow(PackageManager.NameNotFoundException())
+ // Click the chip so that we open the dialog and we fill in [dialogStopAction]
+ val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ clickListener!!.onClick(chipView)
+
+ return dialogStopAction
+ }
+
fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) {
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
val icon = (latest as OngoingActivityChipModel.Shown).icon
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
index fea0e72..8dfbb37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
@@ -118,8 +118,8 @@
}
@Test
+ @DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testAppearResetsTranslation() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
mController.setupAodIcons(mAodIcons);
when(mDozeParameters.shouldControlScreenOff()).thenReturn(false);
mController.appearAodIcons();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 01540e7..58ad835 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -536,7 +536,7 @@
// WHEN there's *no* ongoing activity via new callback
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
- /* hasOngoingActivity= */ false);
+ /* hasOngoingActivity= */ false, /* shouldAnimate= */ false);
// THEN the old callback value is used, so the view is shown
assertEquals(View.VISIBLE,
@@ -548,7 +548,7 @@
// WHEN there *is* an ongoing activity via new callback
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
- /* hasOngoingActivity= */ true);
+ /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
// THEN the old callback value is used, so the view is hidden
assertEquals(View.GONE,
@@ -565,7 +565,7 @@
// listener, but I'm unable to get the fragment to get attached so that the binder starts
// listening to flows.
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
- /* hasOngoingActivity= */ false);
+ /* hasOngoingActivity= */ false, /* shouldAnimate= */ false);
assertEquals(View.GONE,
mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility());
@@ -577,7 +577,7 @@
resumeAndGetFragment();
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
- /* hasOngoingActivity= */ true);
+ /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
assertEquals(View.VISIBLE,
mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility());
@@ -590,7 +590,7 @@
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
- /* hasOngoingActivity= */ true);
+ /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
fragment.disable(DEFAULT_DISPLAY,
StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false);
@@ -605,7 +605,7 @@
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
- /* hasOngoingActivity= */ true);
+ /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(true);
fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
@@ -621,14 +621,14 @@
// Ongoing activity started
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
- /* hasOngoingActivity= */ true);
+ /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
assertEquals(View.VISIBLE,
mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility());
// Ongoing activity ended
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
- /* hasOngoingActivity= */ false);
+ /* hasOngoingActivity= */ false, /* shouldAnimate= */ false);
assertEquals(View.GONE,
mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility());
@@ -643,7 +643,7 @@
// Ongoing call started
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
- /* hasOngoingActivity= */ true);
+ /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
// Notification area is hidden without delay
assertEquals(0f, getNotificationAreaView().getAlpha(), 0.01);
@@ -661,7 +661,7 @@
// WHEN there's *no* ongoing activity via new callback
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
- /* hasOngoingActivity= */ false);
+ /* hasOngoingActivity= */ false, /* shouldAnimate= */ false);
// THEN the new callback value is used, so the view is hidden
assertEquals(View.GONE,
@@ -673,7 +673,7 @@
// WHEN there *is* an ongoing activity via new callback
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
- /* hasOngoingActivity= */ true);
+ /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
// THEN the new callback value is used, so the view is shown
assertEquals(View.VISIBLE,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
index 94159bc..60750cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
@@ -425,7 +425,7 @@
kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing
- assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden)
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
kosmos.fakeMediaProjectionRepository.mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt
index d3f1125..cefdf7e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt
@@ -29,7 +29,7 @@
override val transitionFromLockscreenToDreamStartedEvent = MutableSharedFlow<Unit>()
override val ongoingActivityChip: MutableStateFlow<OngoingActivityChipModel> =
- MutableStateFlow(OngoingActivityChipModel.Hidden)
+ MutableStateFlow(OngoingActivityChipModel.Hidden())
override val isHomeStatusBarAllowedByScene = MutableStateFlow(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index ac42319..60b5b5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -86,6 +86,7 @@
import android.service.notification.NotificationListenerService;
import android.service.notification.ZenModeConfig;
import android.testing.TestableLooper;
+import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.view.Display;
@@ -183,6 +184,7 @@
import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.onehanded.OneHandedController;
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -192,7 +194,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -216,6 +217,9 @@
@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class BubblesTest extends SysuiTestCase {
+
+ private static final String TAG = "BubblesTest";
+
@Mock
private CommonNotifCollection mCommonNotifCollection;
@Mock
@@ -241,8 +245,6 @@
@Mock
private KeyguardBypassController mKeyguardBypassController;
@Mock
- private FloatingContentCoordinator mFloatingContentCoordinator;
- @Mock
private BubbleDataRepository mDataRepository;
@Mock
private NotificationShadeWindowView mNotificationShadeWindowView;
@@ -372,6 +374,7 @@
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+ PhysicsAnimatorTestUtils.prepareForTest();
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
doReturn(true).when(mTransitions).isRegistered();
@@ -494,7 +497,7 @@
mShellCommandHandler,
mShellController,
mBubbleData,
- mFloatingContentCoordinator,
+ new FloatingContentCoordinator(),
mDataRepository,
mStatusBarService,
mWindowManager,
@@ -571,12 +574,32 @@
}
@After
- public void tearDown() {
+ public void tearDown() throws Exception {
ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles());
for (int i = 0; i < bubbles.size(); i++) {
mBubbleController.removeBubble(bubbles.get(i).getKey(),
Bubbles.DISMISS_NO_LONGER_BUBBLE);
}
+ mTestableLooper.processAllMessages();
+
+ // check that no animations are running before finishing the test to make sure that the
+ // state gets cleaned up correctly between tests.
+ int retryCount = 0;
+ while (PhysicsAnimatorTestUtils.isAnyAnimationRunning() && retryCount <= 10) {
+ Log.d(
+ TAG,
+ String.format("waiting for animations to complete. attempt %d", retryCount));
+ // post a message to the looper and wait for it to be processed
+ mTestableLooper.runWithLooper(() -> {});
+ retryCount++;
+ }
+ mTestableLooper.processAllMessages();
+ if (PhysicsAnimatorTestUtils.isAnyAnimationRunning()) {
+ Log.d(TAG, "finished waiting for animations to complete but animations are still "
+ + "running");
+ } else {
+ Log.d(TAG, "no animations are running");
+ }
}
@Test
@@ -1853,7 +1876,6 @@
any(Bubble.class), anyBoolean(), anyBoolean());
}
- @Ignore("reason = b/351977103")
@Test
public void testShowStackEdu_isNotConversationBubble() {
// Setup
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
index 3d85a4a..c7dfd5c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
@@ -17,6 +17,8 @@
package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel
import android.content.applicationContext
+import com.android.systemui.animation.dialogTransitionAnimator
+import com.android.systemui.animation.mockDialogTransitionAnimator
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.mediaRouterChipInteractor
@@ -34,6 +36,7 @@
mediaRouterChipInteractor = mediaRouterChipInteractor,
systemClock = fakeSystemClock,
endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
+ dialogTransitionAnimator = mockDialogTransitionAnimator,
logger = statusBarChipsLogger,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
index 1ed7a47..651a0f7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.chips.mediaprojection.ui.view
import android.content.packageManager
+import com.android.systemui.animation.mockDialogTransitionAnimator
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
@@ -24,6 +25,7 @@
Kosmos.Fixture {
EndMediaProjectionDialogHelper(
dialogFactory = mockSystemUIDialogFactory,
+ dialogTransitionAnimator = mockDialogTransitionAnimator,
packageManager = packageManager,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt
index e4bb166..c2a6f7d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt
@@ -17,10 +17,12 @@
package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel
import android.content.applicationContext
+import com.android.systemui.animation.mockDialogTransitionAnimator
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper
import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.screenRecordChipInteractor
+import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel
import com.android.systemui.statusbar.chips.statusBarChipsLogger
import com.android.systemui.util.time.fakeSystemClock
@@ -30,7 +32,9 @@
scope = applicationCoroutineScope,
context = applicationContext,
interactor = screenRecordChipInteractor,
+ shareToAppChipViewModel = shareToAppChipViewModel,
endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
+ dialogTransitionAnimator = mockDialogTransitionAnimator,
systemClock = fakeSystemClock,
logger = statusBarChipsLogger,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt
index 8ed7f96..0770009 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel
import android.content.applicationContext
+import com.android.systemui.animation.mockDialogTransitionAnimator
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
@@ -32,6 +33,7 @@
mediaProjectionChipInteractor = mediaProjectionChipInteractor,
systemClock = fakeSystemClock,
endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
+ dialogTransitionAnimator = mockDialogTransitionAnimator,
logger = statusBarChipsLogger,
)
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index f61ca61..c82e5be 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -16,6 +16,8 @@
package com.android.server.inputmethod;
+import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID;
+
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.NonNull;
@@ -110,7 +112,7 @@
InlineSuggestionsRequestInfo requestInfo, InlineSuggestionsRequestCallback cb);
/**
- * Force switch to the enabled input method by {@code imeId} for current user. If the input
+ * Force switch to the enabled input method by {@code imeId} for the current user. If the input
* method with {@code imeId} is not enabled or not installed, do nothing.
*
* @param imeId the input method ID to be switched to
@@ -119,7 +121,25 @@
* method by {@code imeId}; {@code false} the input method with {@code imeId} is not available
* to be switched.
*/
- public abstract boolean switchToInputMethod(String imeId, @UserIdInt int userId);
+ public boolean switchToInputMethod(@NonNull String imeId, @UserIdInt int userId) {
+ return switchToInputMethod(imeId, NOT_A_SUBTYPE_ID, userId);
+ }
+
+ /**
+ * Force switch to the enabled input method by {@code imeId} for the current user. If the input
+ * method with {@code imeId} is not enabled or not installed, do nothing. If {@code subtypeId}
+ * is also supplied (not {@link InputMethodUtils#NOT_A_SUBTYPE_ID}) and valid, also switches to
+ * it, otherwise the system decides the most sensible default subtype to use.
+ *
+ * @param imeId the input method ID to be switched to
+ * @param subtypeId the input method subtype ID to be switched to
+ * @param userId the user ID to be queried
+ * @return {@code true} if the current input method was successfully switched to the input
+ * method by {@code imeId}; {@code false} the input method with {@code imeId} is not available
+ * to be switched.
+ */
+ public abstract boolean switchToInputMethod(@NonNull String imeId, int subtypeId,
+ @UserIdInt int userId);
/**
* Force enable or disable the input method associated with {@code imeId} for given user. If
@@ -211,6 +231,15 @@
public abstract void updateImeWindowStatus(boolean disableImeIcon, int displayId);
/**
+ * Updates and reports whether the IME switcher button should be shown, regardless whether
+ * SystemUI or the IME is responsible for drawing it and the corresponding navigation bar.
+ *
+ * @param displayId the display for which to update the IME switcher button visibility.
+ * @param userId the user for which to update the IME switcher button visibility.
+ */
+ public abstract void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId);
+
+ /**
* Finish stylus handwriting by calling {@link InputMethodService#finishStylusHandwriting()} if
* there is an ongoing handwriting session.
*/
@@ -290,7 +319,8 @@
}
@Override
- public boolean switchToInputMethod(String imeId, @UserIdInt int userId) {
+ public boolean switchToInputMethod(@NonNull String imeId, int subtypeId,
+ @UserIdInt int userId) {
return false;
}
@@ -335,6 +365,10 @@
}
@Override
+ public void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId) {
+ }
+
+ @Override
public void onSessionForAccessibilityCreated(int accessibilityConnectionId,
IAccessibilityInputMethodSession session, @UserIdInt int userId) {
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 85af7ab..fbd9ac0 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -181,6 +181,7 @@
import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.android.server.input.InputManagerInternal;
import com.android.server.inputmethod.InputMethodManagerInternal.InputMethodListListener;
+import com.android.server.inputmethod.InputMethodMenuControllerNew.MenuItem;
import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
import com.android.server.pm.UserManagerInternal;
import com.android.server.statusbar.StatusBarManagerInternal;
@@ -360,6 +361,7 @@
private final UserManagerInternal mUserManagerInternal;
@MultiUserUnawareField
private final InputMethodMenuController mMenuController;
+ private final InputMethodMenuControllerNew mMenuControllerNew;
@GuardedBy("ImfLock.class")
@MultiUserUnawareField
@@ -566,7 +568,9 @@
}
switch (key) {
case Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD: {
- mMenuController.updateKeyboardFromSettingsLocked();
+ if (!Flags.imeSwitcherRevamp()) {
+ mMenuController.updateKeyboardFromSettingsLocked();
+ }
break;
}
case Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE: {
@@ -631,7 +635,15 @@
}
}
}
- mMenuController.hideInputMethodMenu();
+ if (Flags.imeSwitcherRevamp()) {
+ synchronized (ImfLock.class) {
+ final var bindingController = getInputMethodBindingController(senderUserId);
+ mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(),
+ senderUserId);
+ }
+ } else {
+ mMenuController.hideInputMethodMenu();
+ }
} else {
Slog.w(TAG, "Unexpected intent " + intent);
}
@@ -1171,6 +1183,8 @@
: bindingControllerFactory);
mMenuController = new InputMethodMenuController(this);
+ mMenuControllerNew = Flags.imeSwitcherRevamp()
+ ? new InputMethodMenuControllerNew() : null;
mVisibilityStateComputer = new ImeVisibilityStateComputer(this);
mVisibilityApplier = new DefaultImeVisibilityApplier(this);
@@ -1782,7 +1796,11 @@
ImeTracker.PHASE_SERVER_WAIT_IME);
userData.mCurStatsToken = null;
// TODO: Make mMenuController multi-user aware
- mMenuController.hideInputMethodMenuLocked();
+ if (Flags.imeSwitcherRevamp()) {
+ mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId);
+ } else {
+ mMenuController.hideInputMethodMenuLocked();
+ }
}
}
@@ -2599,7 +2617,12 @@
if (!mShowOngoingImeSwitcherForPhones) return false;
// When the IME switcher dialog is shown, the IME switcher button should be hidden.
// TODO(b/305849394): Make mMenuController multi-user aware.
- if (mMenuController.getSwitchingDialogLocked() != null) return false;
+ final boolean switcherMenuShowing = Flags.imeSwitcherRevamp()
+ ? mMenuControllerNew.isShowing()
+ : mMenuController.getSwitchingDialogLocked() != null;
+ if (switcherMenuShowing) {
+ return false;
+ }
// When we are switching IMEs, the IME switcher button should be hidden.
final var bindingController = getInputMethodBindingController(userId);
if (!Objects.equals(bindingController.getCurId(),
@@ -2614,7 +2637,7 @@
|| (visibility & InputMethodService.IME_INVISIBLE) != 0) {
return false;
}
- if (mWindowManagerInternal.isHardKeyboardAvailable()) {
+ if (mWindowManagerInternal.isHardKeyboardAvailable() && !Flags.imeSwitcherRevamp()) {
// When physical keyboard is attached, we show the ime switcher (or notification if
// NavBar is not available) because SHOW_IME_WITH_HARD_KEYBOARD settings currently
// exists in the IME switcher dialog. Might be OK to remove this condition once
@@ -2625,6 +2648,15 @@
}
final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
+ if (Flags.imeSwitcherRevamp()) {
+ // The IME switcher button should be shown when the current IME specified a
+ // language settings activity.
+ final var curImi = settings.getMethodMap().get(settings.getSelectedInputMethod());
+ if (curImi != null && curImi.createImeLanguageSettingsActivityIntent() != null) {
+ return true;
+ }
+ }
+
return hasMultipleSubtypesForSwitcher(false /* nonAuxOnly */, settings);
}
@@ -2794,7 +2826,10 @@
}
final var curId = bindingController.getCurId();
// TODO(b/305849394): Make mMenuController multi-user aware.
- if (mMenuController.getSwitchingDialogLocked() != null
+ final boolean switcherMenuShowing = Flags.imeSwitcherRevamp()
+ ? mMenuControllerNew.isShowing()
+ : mMenuController.getSwitchingDialogLocked() != null;
+ if (switcherMenuShowing
|| !Objects.equals(curId, bindingController.getSelectedMethodId())) {
// When the IME switcher dialog is shown, or we are switching IMEs,
// the back button should be in the default state (as if the IME is not shown).
@@ -2813,7 +2848,9 @@
@GuardedBy("ImfLock.class")
void updateFromSettingsLocked(boolean enabledMayChange, @UserIdInt int userId) {
updateInputMethodsFromSettingsLocked(enabledMayChange, userId);
- mMenuController.updateKeyboardFromSettingsLocked();
+ if (!Flags.imeSwitcherRevamp()) {
+ mMenuController.updateKeyboardFromSettingsLocked();
+ }
}
/**
@@ -3979,10 +4016,70 @@
@IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD)
public boolean isInputMethodPickerShownForTest() {
synchronized (ImfLock.class) {
- return mMenuController.isisInputMethodPickerShownForTestLocked();
+ return Flags.imeSwitcherRevamp()
+ ? mMenuControllerNew.isShowing()
+ : mMenuController.isisInputMethodPickerShownForTestLocked();
}
}
+ /**
+ * Gets the list of Input Method Switcher Menu items and the index of the selected item.
+ *
+ * @param items the list of input method and subtype items.
+ * @param selectedImeId the ID of the selected input method.
+ * @param selectedSubtypeId the ID of the selected input method subtype,
+ * or {@link #NOT_A_SUBTYPE_ID} if no subtype is selected.
+ * @param userId the ID of the user for which to get the menu items.
+ * @return the list of menu items, and the index of the selected item,
+ * or {@code -1} if no item is selected.
+ */
+ @GuardedBy("ImfLock.class")
+ @NonNull
+ private Pair<List<MenuItem>, Integer> getInputMethodPickerItems(
+ @NonNull List<ImeSubtypeListItem> items, @Nullable String selectedImeId,
+ int selectedSubtypeId, @UserIdInt int userId) {
+ final var bindingController = getInputMethodBindingController(userId);
+ final var settings = InputMethodSettingsRepository.get(userId);
+
+ if (selectedSubtypeId == NOT_A_SUBTYPE_ID) {
+ // TODO(b/351124299): Check if this fallback logic is still necessary.
+ final var curSubtype = bindingController.getCurrentInputMethodSubtype();
+ if (curSubtype != null) {
+ final var curMethodId = bindingController.getSelectedMethodId();
+ final var curImi = settings.getMethodMap().get(curMethodId);
+ selectedSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(
+ curImi, curSubtype.hashCode());
+ }
+ }
+
+ // No item is selected by default. When we have a list of explicitly enabled
+ // subtypes, the implicit subtype is no longer listed. If the implicit one
+ // is still selected, no items will be shown as selected.
+ int selectedIndex = -1;
+ String prevImeId = null;
+ final var menuItems = new ArrayList<MenuItem>();
+ for (int i = 0; i < items.size(); i++) {
+ final var item = items.get(i);
+ final var imeId = item.mImi.getId();
+ if (imeId.equals(selectedImeId)) {
+ final int subtypeId = item.mSubtypeId;
+ // Check if this is the selected IME-subtype pair.
+ if ((subtypeId == 0 && selectedSubtypeId == NOT_A_SUBTYPE_ID)
+ || subtypeId == NOT_A_SUBTYPE_ID
+ || subtypeId == selectedSubtypeId) {
+ selectedIndex = i;
+ }
+ }
+ final boolean hasHeader = !imeId.equals(prevImeId);
+ final boolean hasDivider = hasHeader && prevImeId != null;
+ prevImeId = imeId;
+ menuItems.add(new MenuItem(item.mImeName, item.mSubtypeName, item.mImi, item.mSubtypeId,
+ hasHeader, hasDivider));
+ }
+
+ return new Pair<>(menuItems, selectedIndex);
+ }
+
@BinderThread
private void onImeSwitchButtonClickFromClient(@NonNull IBinder token, int displayId,
@NonNull UserData userData) {
@@ -4625,7 +4722,10 @@
proto.write(IS_INTERACTIVE, mIsInteractive);
proto.write(BACK_DISPOSITION, bindingController.getBackDisposition());
proto.write(IME_WINDOW_VISIBILITY, bindingController.getImeWindowVis());
- proto.write(SHOW_IME_WITH_HARD_KEYBOARD, mMenuController.getShowImeWithHardKeyboard());
+ if (!Flags.imeSwitcherRevamp()) {
+ proto.write(SHOW_IME_WITH_HARD_KEYBOARD,
+ mMenuController.getShowImeWithHardKeyboard());
+ }
proto.write(CONCURRENT_MULTI_USER_MODE_ENABLED, mConcurrentMultiUserModeEnabled);
proto.end(token);
}
@@ -4931,8 +5031,9 @@
synchronized (ImfLock.class) {
final InputMethodSettings settings =
InputMethodSettingsRepository.get(mCurrentUserId);
+ final int userId = settings.getUserId();
final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked()
- && mWindowManagerInternal.isKeyguardSecure(settings.getUserId());
+ && mWindowManagerInternal.isKeyguardSecure(userId);
final String lastInputMethodId = settings.getSelectedInputMethod();
int lastInputMethodSubtypeId =
settings.getSelectedInputMethodSubtypeId(lastInputMethodId);
@@ -4945,12 +5046,35 @@
Slog.w(TAG, "Show switching menu failed, imList is empty,"
+ " showAuxSubtypes: " + showAuxSubtypes
+ " isScreenLocked: " + isScreenLocked
- + " userId: " + settings.getUserId());
+ + " userId: " + userId);
return false;
}
- mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
- lastInputMethodId, lastInputMethodSubtypeId, imList);
+ if (Flags.imeSwitcherRevamp()) {
+ if (DEBUG) {
+ Slog.v(TAG, "Show IME switcher menu,"
+ + " showAuxSubtypes=" + showAuxSubtypes
+ + " displayId=" + displayId
+ + " preferredInputMethodId=" + lastInputMethodId
+ + " preferredInputMethodSubtypeId=" + lastInputMethodSubtypeId);
+ }
+
+ final var itemsAndIndex = getInputMethodPickerItems(imList,
+ lastInputMethodId, lastInputMethodSubtypeId, userId);
+ final var menuItems = itemsAndIndex.first;
+ final int selectedIndex = itemsAndIndex.second;
+
+ if (selectedIndex == -1) {
+ Slog.w(TAG, "Switching menu shown with no item selected"
+ + ", IME id: " + lastInputMethodId
+ + ", subtype index: " + lastInputMethodSubtypeId);
+ }
+
+ mMenuControllerNew.show(menuItems, selectedIndex, displayId, userId);
+ } else {
+ mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
+ lastInputMethodId, lastInputMethodSubtypeId, imList);
+ }
}
return true;
@@ -5021,7 +5145,9 @@
// --------------------------------------------------------------
case MSG_HARD_KEYBOARD_SWITCH_CHANGED:
- mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1);
+ if (!Flags.imeSwitcherRevamp()) {
+ mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1);
+ }
synchronized (ImfLock.class) {
sendOnNavButtonFlagsChangedToAllImesLocked();
}
@@ -5591,7 +5717,8 @@
}
@GuardedBy("ImfLock.class")
- private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) {
+ private boolean switchToInputMethodLocked(@NonNull String imeId, int subtypeId,
+ @UserIdInt int userId) {
final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
if (mConcurrentMultiUserModeEnabled || userId == mCurrentUserId) {
if (!settings.getMethodMap().containsKey(imeId)
@@ -5599,7 +5726,7 @@
.contains(settings.getMethodMap().get(imeId))) {
return false; // IME is not found or not enabled.
}
- setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID, userId);
+ setInputMethodLocked(imeId, subtypeId, userId);
return true;
}
if (!settings.getMethodMap().containsKey(imeId)
@@ -5608,6 +5735,7 @@
return false; // IME is not found or not enabled.
}
settings.putSelectedInputMethod(imeId);
+ // For non-current user, only reset subtypeId (instead of setting the given one).
settings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
return true;
}
@@ -5753,9 +5881,10 @@
}
@Override
- public boolean switchToInputMethod(String imeId, @UserIdInt int userId) {
+ public boolean switchToInputMethod(@NonNull String imeId, int subtypeId,
+ @UserIdInt int userId) {
synchronized (ImfLock.class) {
- return switchToInputMethodLocked(imeId, userId);
+ return switchToInputMethodLocked(imeId, subtypeId, userId);
}
}
@@ -5852,7 +5981,12 @@
// input target changed, in case seeing the dialog dismiss flickering during
// the next focused window starting the input connection.
if (mLastImeTargetWindow != userData.mImeBindingState.mFocusedWindow) {
- mMenuController.hideInputMethodMenuLocked();
+ if (Flags.imeSwitcherRevamp()) {
+ final var bindingController = getInputMethodBindingController(userId);
+ mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId);
+ } else {
+ mMenuController.hideInputMethodMenuLocked();
+ }
}
}
}
@@ -5871,6 +6005,15 @@
}
@Override
+ public void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId) {
+ synchronized (ImfLock.class) {
+ updateSystemUiLocked(userId);
+ final var userData = getUserData(userId);
+ sendOnNavButtonFlagsChangedLocked(userData);
+ }
+ }
+
+ @Override
public void onSessionForAccessibilityCreated(int accessibilityConnectionId,
IAccessibilityInputMethodSession session, @UserIdInt int userId) {
synchronized (ImfLock.class) {
@@ -6192,6 +6335,10 @@
};
mUserDataRepository.forAllUserData(userDataDump);
+ if (Flags.imeSwitcherRevamp()) {
+ p.println(" menuControllerNew:");
+ mMenuControllerNew.dump(p, " ");
+ }
p.println(" mCurToken=" + bindingController.getCurToken());
p.println(" mCurTokenDisplayId=" + bindingController.getCurTokenDisplayId());
p.println(" mCurHostInputToken=" + bindingController.getCurHostInputToken());
@@ -6638,7 +6785,7 @@
continue;
}
boolean failedToSelectUnknownIme = !switchToInputMethodLocked(imeId,
- userId);
+ NOT_A_SUBTYPE_ID, userId);
if (failedToSelectUnknownIme) {
error.print("Unknown input method ");
error.print(imeId);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
new file mode 100644
index 0000000..cbb1807
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2024 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.inputmethod;
+
+
+import static com.android.server.inputmethod.InputMethodManagerService.DEBUG;
+import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.Printer;
+import android.util.Slog;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodInfo;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.internal.widget.RecyclerView;
+
+import java.util.List;
+
+/**
+ * Controller for showing and hiding the Input Method Switcher Menu.
+ */
+final class InputMethodMenuControllerNew {
+
+ private static final String TAG = InputMethodMenuControllerNew.class.getSimpleName();
+
+ /**
+ * The horizontal offset from the menu to the edge of the screen corresponding
+ * to {@link Gravity#END}.
+ */
+ private static final int HORIZONTAL_OFFSET = 16;
+
+ /** The title of the window, used for debugging. */
+ private static final String WINDOW_TITLE = "IME Switcher Menu";
+
+ private final InputMethodDialogWindowContext mDialogWindowContext =
+ new InputMethodDialogWindowContext();
+
+ @Nullable
+ private AlertDialog mDialog;
+
+ @Nullable
+ private List<MenuItem> mMenuItems;
+
+ /**
+ * Shows the Input Method Switcher Menu, with a list of IMEs and their subtypes.
+ *
+ * @param items the list of menu items.
+ * @param selectedIndex the index of the menu item that is selected.
+ * If no other IMEs are enabled, this index will be out of reach.
+ * @param displayId the ID of the display where the menu was requested.
+ * @param userId the ID of the user that requested the menu.
+ */
+ void show(@NonNull List<MenuItem> items, int selectedIndex, int displayId,
+ @UserIdInt int userId) {
+ // Hide the menu in case it was already showing.
+ hide(displayId, userId);
+
+ final Context dialogWindowContext = mDialogWindowContext.get(displayId);
+ final var builder = new AlertDialog.Builder(dialogWindowContext,
+ com.android.internal.R.style.Theme_DeviceDefault_InputMethodSwitcherDialog);
+ final var inflater = LayoutInflater.from(builder.getContext());
+
+ // Create the content view.
+ final View contentView = inflater
+ .inflate(com.android.internal.R.layout.input_method_switch_dialog_new, null);
+ contentView.setAccessibilityPaneTitle(
+ dialogWindowContext.getText(com.android.internal.R.string.select_input_method));
+ builder.setView(contentView);
+
+ final DialogInterface.OnClickListener onClickListener = (dialog, which) -> {
+ if (which != selectedIndex) {
+ final var item = items.get(which);
+ InputMethodManagerInternal.get()
+ .switchToInputMethod(item.mImi.getId(), item.mSubtypeId, userId);
+ }
+ hide(displayId, userId);
+ };
+
+ final var selectedImi = selectedIndex >= 0 ? items.get(selectedIndex).mImi : null;
+ final var languageSettingsIntent = selectedImi != null
+ ? selectedImi.createImeLanguageSettingsActivityIntent() : null;
+ final boolean hasLanguageSettingsButton = languageSettingsIntent != null;
+ if (hasLanguageSettingsButton) {
+ final View buttonBar = contentView
+ .requireViewById(com.android.internal.R.id.button_bar);
+ buttonBar.setVisibility(View.VISIBLE);
+
+ languageSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ final Button languageSettingsButton = contentView
+ .requireViewById(com.android.internal.R.id.button1);
+ languageSettingsButton.setVisibility(View.VISIBLE);
+ languageSettingsButton.setOnClickListener(v -> {
+ v.getContext().startActivity(languageSettingsIntent);
+ hide(displayId, userId);
+ });
+ }
+
+ // Create the current IME subtypes list.
+ final RecyclerView recyclerView = contentView
+ .requireViewById(com.android.internal.R.id.list);
+ recyclerView.setAdapter(new Adapter(items, selectedIndex, inflater, onClickListener));
+ // Scroll to the currently selected IME.
+ recyclerView.scrollToPosition(selectedIndex);
+ // Indicate that the list can be scrolled.
+ recyclerView.setScrollIndicators(
+ hasLanguageSettingsButton ? View.SCROLL_INDICATOR_BOTTOM : 0);
+
+ builder.setOnCancelListener(dialog -> hide(displayId, userId));
+ mMenuItems = items;
+ mDialog = builder.create();
+ mDialog.setCanceledOnTouchOutside(true);
+ final Window w = mDialog.getWindow();
+ w.setHideOverlayWindows(true);
+ final WindowManager.LayoutParams attrs = w.getAttributes();
+ // Use an alternate token for the dialog for that window manager can group the token
+ // with other IME windows based on type vs. grouping based on whichever token happens
+ // to get selected by the system later on.
+ attrs.token = dialogWindowContext.getWindowContextToken();
+ attrs.gravity = Gravity.getAbsoluteGravity(Gravity.BOTTOM | Gravity.END,
+ dialogWindowContext.getResources().getConfiguration().getLayoutDirection());
+ attrs.x = HORIZONTAL_OFFSET;
+ attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ attrs.type = WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
+ // Used for debugging only, not user visible.
+ attrs.setTitle(WINDOW_TITLE);
+ w.setAttributes(attrs);
+
+ mDialog.show();
+ InputMethodManagerInternal.get().updateShouldShowImeSwitcher(displayId, userId);
+ }
+
+ /**
+ * Hides the Input Method Switcher Menu.
+ *
+ * @param displayId the ID of the display from where the menu should be hidden.
+ * @param userId the ID of the user for which the menu should be hidden.
+ */
+ void hide(int displayId, @UserIdInt int userId) {
+ if (DEBUG) Slog.v(TAG, "Hide IME switcher menu.");
+
+ mMenuItems = null;
+ // Cannot use dialog.isShowing() here, as the cancel listener flow already resets mShowing.
+ if (mDialog != null) {
+ mDialog.dismiss();
+ mDialog = null;
+
+ InputMethodManagerInternal.get().updateShouldShowImeSwitcher(displayId, userId);
+ }
+ }
+
+ /**
+ * Returns whether the Input Method Switcher Menu is showing.
+ */
+ boolean isShowing() {
+ return mDialog != null && mDialog.isShowing();
+ }
+
+ void dump(@NonNull Printer pw, @NonNull String prefix) {
+ final boolean showing = isShowing();
+ pw.println(prefix + " isShowing: " + showing);
+
+ if (showing) {
+ pw.println(prefix + " menuItems: " + mMenuItems);
+ }
+ }
+
+ /**
+ * Item to be shown in the Input Method Switcher Menu, containing an input method and
+ * optionally an input method subtype.
+ */
+ static class MenuItem {
+
+ /** The name of the input method. */
+ @NonNull
+ private final CharSequence mImeName;
+
+ /**
+ * The name of the input method subtype, or {@code null} if this item doesn't have a
+ * subtype.
+ */
+ @Nullable
+ private final CharSequence mSubtypeName;
+
+ /** The info of the input method. */
+ @NonNull
+ private final InputMethodInfo mImi;
+
+ /**
+ * The index of the subtype in the input method's array of subtypes,
+ * or {@link InputMethodUtils#NOT_A_SUBTYPE_ID} if this item doesn't have a subtype.
+ */
+ @IntRange(from = NOT_A_SUBTYPE_ID)
+ private final int mSubtypeId;
+
+ /** Whether this item has a group header (only the first item of each input method). */
+ private final boolean mHasHeader;
+
+ /**
+ * Whether this item should has a group divider (same as {@link #mHasHeader},
+ * excluding the first IME).
+ */
+ private final boolean mHasDivider;
+
+ MenuItem(@NonNull CharSequence imeName, @Nullable CharSequence subtypeName,
+ @NonNull InputMethodInfo imi, @IntRange(from = NOT_A_SUBTYPE_ID) int subtypeId,
+ boolean hasHeader, boolean hasDivider) {
+ mImeName = imeName;
+ mSubtypeName = subtypeName;
+ mImi = imi;
+ mSubtypeId = subtypeId;
+ mHasHeader = hasHeader;
+ mHasDivider = hasDivider;
+ }
+
+ @Override
+ public String toString() {
+ return "MenuItem{"
+ + "mImeName=" + mImeName
+ + " mSubtypeName=" + mSubtypeName
+ + " mSubtypeId=" + mSubtypeId
+ + " mHasHeader=" + mHasHeader
+ + " mHasDivider=" + mHasDivider
+ + "}";
+ }
+ }
+
+ private static class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
+
+ /** The list of items to show. */
+ @NonNull
+ private final List<MenuItem> mItems;
+ /** The index of the selected item. */
+ private final int mSelectedIndex;
+ @NonNull
+ private final LayoutInflater mInflater;
+ @NonNull
+ private final DialogInterface.OnClickListener mOnClickListener;
+
+ Adapter(@NonNull List<MenuItem> items, int selectedIndex,
+ @NonNull LayoutInflater inflater,
+ @NonNull DialogInterface.OnClickListener onClickListener) {
+ mItems = items;
+ mSelectedIndex = selectedIndex;
+ mInflater = inflater;
+ mOnClickListener = onClickListener;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ final View view = mInflater.inflate(
+ com.android.internal.R.layout.input_method_switch_item_new, parent, false);
+
+ return new ViewHolder(view, mOnClickListener);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ holder.bind(mItems.get(position), position == mSelectedIndex /* isSelected */);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mItems.size();
+ }
+
+ private static class ViewHolder extends RecyclerView.ViewHolder {
+
+ /** The container of the item. */
+ @NonNull
+ private final View mContainer;
+ /** The name of the item. */
+ @NonNull
+ private final TextView mName;
+ /** Indicator for the selected status of the item. */
+ @NonNull
+ private final ImageView mCheckmark;
+ /** The group header optionally drawn above the item. */
+ @NonNull
+ private final TextView mHeader;
+ /** The group divider optionally drawn above the item. */
+ @NonNull
+ private final View mDivider;
+
+ private ViewHolder(@NonNull View itemView,
+ @NonNull DialogInterface.OnClickListener onClickListener) {
+ super(itemView);
+
+ mContainer = itemView.requireViewById(com.android.internal.R.id.list_item);
+ mName = itemView.requireViewById(com.android.internal.R.id.text);
+ mCheckmark = itemView.requireViewById(com.android.internal.R.id.image);
+ mHeader = itemView.requireViewById(com.android.internal.R.id.header_text);
+ mDivider = itemView.requireViewById(com.android.internal.R.id.divider);
+
+ mContainer.setOnClickListener((v) ->
+ onClickListener.onClick(null /* dialog */, getAdapterPosition()));
+ }
+
+ /**
+ * Binds the given item to the current view.
+ *
+ * @param item the item to bind.
+ * @param isSelected whether this is selected.
+ */
+ private void bind(@NonNull MenuItem item, boolean isSelected) {
+ // Use the IME name for subtypes with an empty subtype name.
+ final var name = TextUtils.isEmpty(item.mSubtypeName)
+ ? item.mImeName : item.mSubtypeName;
+ mContainer.setActivated(isSelected);
+ // Activated is the correct state, but we also set selected for accessibility info.
+ mContainer.setSelected(isSelected);
+ mName.setSelected(isSelected);
+ mName.setText(name);
+ mCheckmark.setVisibility(isSelected ? View.VISIBLE : View.GONE);
+ mHeader.setText(item.mImeName);
+ mHeader.setVisibility(item.mHasHeader ? View.VISIBLE : View.GONE);
+ mDivider.setVisibility(item.mHasDivider ? View.VISIBLE : View.GONE);
+ }
+ }
+ }
+}