Merge "Move EmptyShadeViewModel flows to the bg" into main
diff --git a/Android.bp b/Android.bp
index 516fc9c..811755d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -368,6 +368,7 @@
jarjar_rules: ":framework-jarjar-rules",
javac_shard_size: 150,
plugins: [
+ "cached-property-annotation-processor",
"view-inspector-annotation-processor",
"staledataclass-annotation-processor",
"error_prone_android_framework",
diff --git a/core/api/current.txt b/core/api/current.txt
index 34f1b6e..53da338 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -37434,6 +37434,7 @@
field public static final String ACTION_APPLICATION_SETTINGS = "android.settings.APPLICATION_SETTINGS";
field public static final String ACTION_APP_LOCALE_SETTINGS = "android.settings.APP_LOCALE_SETTINGS";
field public static final String ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS = "android.settings.APP_NOTIFICATION_BUBBLE_SETTINGS";
+ field @FlaggedApi("android.app.api_rich_ongoing") public static final String ACTION_APP_NOTIFICATION_PROMOTION_SETTINGS = "android.settings.APP_NOTIFICATION_PROMOTION_SETTINGS";
field public static final String ACTION_APP_NOTIFICATION_SETTINGS = "android.settings.APP_NOTIFICATION_SETTINGS";
field public static final String ACTION_APP_OPEN_BY_DEFAULT_SETTINGS = "android.settings.APP_OPEN_BY_DEFAULT_SETTINGS";
field public static final String ACTION_APP_SEARCH_SETTINGS = "android.settings.APP_SEARCH_SETTINGS";
@@ -61657,6 +61658,7 @@
method public void unregisterOnBackInvokedCallback(@NonNull android.window.OnBackInvokedCallback);
field public static final int PRIORITY_DEFAULT = 0; // 0x0
field public static final int PRIORITY_OVERLAY = 1000000; // 0xf4240
+ field @FlaggedApi("com.android.window.flags.predictive_back_priority_system_navigation_observer") public static final int PRIORITY_SYSTEM_NAVIGATION_OBSERVER = -2; // 0xfffffffe
}
public interface SplashScreen {
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 0812a13..38632bd 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -809,6 +809,10 @@
return false;
}
+ private static boolean isStandardLayout(int layoutId) {
+ return STANDARD_LAYOUTS.contains(layoutId);
+ }
+
/** @hide */
@IntDef(flag = true, prefix = {"FLAG_"}, value = {
FLAG_SHOW_LIGHTS,
@@ -5983,9 +5987,9 @@
}
}
boolean contentViewUsesHeader = mN.contentView == null
- || STANDARD_LAYOUTS.contains(mN.contentView.getLayoutId());
+ || isStandardLayout(mN.contentView.getLayoutId());
boolean bigContentViewUsesHeader = mN.bigContentView == null
- || STANDARD_LAYOUTS.contains(mN.bigContentView.getLayoutId());
+ || isStandardLayout(mN.bigContentView.getLayoutId());
return contentViewUsesHeader && bigContentViewUsesHeader;
}
@@ -6781,7 +6785,7 @@
return false;
}
if (fullyCustomViewRequiresDecoration(false)
- && STANDARD_LAYOUTS.contains(customContent.getLayoutId())) {
+ && isStandardLayout(customContent.getLayoutId())) {
// If the app's custom views are objects returned from Builder.create*ContentView()
// then the app is most likely attempting to spoof the user. Even if they are not,
// the result would be broken (b/189189308) so we will ignore it.
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index dfed1f7..41abd68 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -958,6 +958,9 @@
* Returns whether the calling app's properly formatted notifications can appear in a promoted
* format, which may result in higher ranking, appearances on additional surfaces, and richer
* presentation.
+ *
+ * Apps can request this permission by sending the user to the activity that matches the system
+ * intent action {@link android.provider.Settings#ACTION_APP_NOTIFICATION_PROMOTION_SETTINGS}.
*/
@FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
public boolean canPostPromotedNotifications() {
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index e043a5d..a458b4e 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -1980,6 +1980,7 @@
public void registerAllResourcesReference(@NonNull Resources resources) {
if (android.content.res.Flags.registerResourcePaths()) {
synchronized (mLock) {
+ cleanupReferences(mAllResourceReferences, mAllResourceReferencesQueue);
mAllResourceReferences.add(
new WeakReference<>(resources, mAllResourceReferencesQueue));
}
diff --git a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
index fe2db49..64dece9 100644
--- a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
+++ b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
@@ -16,6 +16,8 @@
package android.app.appfunctions;
+import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DEFAULT;
+import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED;
import static android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID;
import static android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_ENABLED;
import static android.app.appfunctions.AppFunctionStaticMetadataHelper.APP_FUNCTION_INDEXER_PACKAGE;
@@ -166,15 +168,18 @@
if (runtimeMetadataResults.isEmpty()) {
throw new IllegalArgumentException("App function not found.");
}
- boolean[] enabled =
+ long enabled =
runtimeMetadataResults
.getFirst()
.getGenericDocument()
- .getPropertyBooleanArray(PROPERTY_ENABLED);
- if (enabled != null && enabled.length != 0) {
- return enabled[0];
+ .getPropertyLong(PROPERTY_ENABLED);
+ // If enabled is not equal to APP_FUNCTION_STATE_DEFAULT, it means it IS overridden and
+ // we should return the overridden value.
+ if (enabled != APP_FUNCTION_STATE_DEFAULT) {
+ return enabled == APP_FUNCTION_STATE_ENABLED;
}
- // Runtime metadata not found. Using the default value in the static metadata.
+ // Runtime metadata not found or enabled is equal to APP_FUNCTION_STATE_DEFAULT.
+ // Using the default value in the static metadata.
return joinedStaticRuntimeResults
.getFirst()
.getGenericDocument()
diff --git a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java
index 8b7f326..08ecced 100644
--- a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java
+++ b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java
@@ -16,11 +16,15 @@
package android.app.appfunctions;
+import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DEFAULT;
+import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DISABLED;
+import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED;
import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.appfunctions.AppFunctionManager.EnabledState;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.GenericDocument;
@@ -162,15 +166,13 @@
* Returns if the function is set to be enabled or not. If not set, the {@link
* AppFunctionStaticMetadataHelper#STATIC_PROPERTY_ENABLED_BY_DEFAULT} value would be used.
*/
- @Nullable
- public Boolean getEnabled() {
- // We can't use getPropertyBoolean here. getPropertyBoolean returns false instead of null
- // if the value is missing.
- boolean[] enabled = getPropertyBooleanArray(PROPERTY_ENABLED);
- if (enabled == null || enabled.length == 0) {
- return null;
- }
- return enabled[0];
+ @EnabledState
+ public int getEnabled() {
+ // getPropertyLong returns the first long associated with the given path or default value 0
+ // if there is no such value or the value is of a different type.
+ // APP_FUNCTION_STATE_DEFAULT also equals 0 which means the returned value will be 0 when an
+ // app as either never changed the enabled bit at runtime or has reset it to the default.
+ return (int) getPropertyLong(PROPERTY_ENABLED);
}
/** Returns the qualified id linking to the static metadata of the app function. */
@@ -217,12 +219,14 @@
* TODO(369683073) Replace the tristate Boolean with IntDef EnabledState.
*/
@NonNull
- public Builder setEnabled(@Nullable Boolean enabled) {
- if (enabled == null) {
- setPropertyBoolean(PROPERTY_ENABLED);
- } else {
- setPropertyBoolean(PROPERTY_ENABLED, enabled);
+ public Builder setEnabled(@EnabledState int enabledState) {
+ if (enabledState != APP_FUNCTION_STATE_DEFAULT
+ && enabledState != APP_FUNCTION_STATE_ENABLED
+ && enabledState != APP_FUNCTION_STATE_DISABLED) {
+ throw new IllegalArgumentException(
+ "Value of EnabledState is unsupported.");
}
+ setPropertyLong(PROPERTY_ENABLED, enabledState);
return this;
}
diff --git a/core/java/android/app/appfunctions/OWNERS b/core/java/android/app/appfunctions/OWNERS
index c6827cc..6a69e15 100644
--- a/core/java/android/app/appfunctions/OWNERS
+++ b/core/java/android/app/appfunctions/OWNERS
@@ -4,3 +4,4 @@
tonymak@google.com
mingweiliao@google.com
anothermark@google.com
+utkarshnigam@google.com
diff --git a/core/java/android/app/appfunctions/TEST_MAPPING b/core/java/android/app/appfunctions/TEST_MAPPING
index 91e82ec..27517c8 100644
--- a/core/java/android/app/appfunctions/TEST_MAPPING
+++ b/core/java/android/app/appfunctions/TEST_MAPPING
@@ -1,10 +1,7 @@
{
- "postsubmit": [
+ "imports": [
{
- "name": "FrameworksAppFunctionsTests"
- },
- {
- "name": "CtsAppFunctionTestCases"
+ "path": "frameworks/base/services/appfunctions/TEST_MAPPING"
}
]
}
\ No newline at end of file
diff --git a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
index b9eba9c..ce8661e 100644
--- a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
@@ -1028,6 +1028,9 @@
// Camera is already closed, so nothing left to do
if (DEBUG) Log.v(TAG, mIdString +
"Camera was already closed or busy, skipping unconfigure");
+ } catch (SecurityException e) {
+ // UID state change revoked camera permission
+ Log.e(TAG, mIdString + "Exception while unconfiguring outputs: ", e);
}
}
}
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 461f1e0..3ae9511 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -69,6 +69,8 @@
import android.view.WindowManager.LayoutParams;
import com.android.internal.R;
+import com.android.internal.annotations.CachedProperty;
+import com.android.internal.annotations.CachedPropertyDefaults;
import java.io.IOException;
import java.lang.annotation.Retention;
@@ -90,6 +92,7 @@
*/
@SystemService(Context.USER_SERVICE)
@android.ravenwood.annotation.RavenwoodKeepPartialClass
+@CachedPropertyDefaults()
public class UserManager {
private static final String TAG = "UserManager";
@@ -108,6 +111,9 @@
/** Whether the device is in headless system user mode; null until cached. */
private static Boolean sIsHeadlessSystemUser = null;
+ /** Generated class containing IpcDataCaches. */
+ private final Object mIpcDataCache = new UserManagerCache();
+
/** Maximum length of username.
* @hide
*/
@@ -3766,62 +3772,18 @@
return isUserUnlocked(user.getIdentifier());
}
- private static final String CACHE_KEY_IS_USER_UNLOCKED_PROPERTY =
- PropertyInvalidatedCache.createPropertyName(
- PropertyInvalidatedCache.MODULE_SYSTEM, "is_user_unlocked");
-
- private final PropertyInvalidatedCache<Integer, Boolean> mIsUserUnlockedCache =
- new PropertyInvalidatedCache<Integer, Boolean>(
- 32, CACHE_KEY_IS_USER_UNLOCKED_PROPERTY) {
- @Override
- public Boolean recompute(Integer query) {
- try {
- return mService.isUserUnlocked(query);
- } catch (RemoteException re) {
- throw re.rethrowFromSystemServer();
- }
- }
- @Override
- public boolean bypass(Integer query) {
- return query < 0;
- }
- };
-
- // Uses IS_USER_UNLOCKED_PROPERTY for invalidation as the APIs have the same dependencies.
- private final PropertyInvalidatedCache<Integer, Boolean> mIsUserUnlockingOrUnlockedCache =
- new PropertyInvalidatedCache<Integer, Boolean>(
- 32, CACHE_KEY_IS_USER_UNLOCKED_PROPERTY) {
- @Override
- public Boolean recompute(Integer query) {
- try {
- return mService.isUserUnlockingOrUnlocked(query);
- } catch (RemoteException re) {
- throw re.rethrowFromSystemServer();
- }
- }
- @Override
- public boolean bypass(Integer query) {
- return query < 0;
- }
- };
-
/** @hide */
@UnsupportedAppUsage
@RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+ @CachedProperty(modsFlagOnOrNone = {}, api = "is_user_unlocked")
public boolean isUserUnlocked(@UserIdInt int userId) {
- return mIsUserUnlockedCache.query(userId);
- }
-
- /** @hide */
- public void disableIsUserUnlockedCache() {
- mIsUserUnlockedCache.disableLocal();
- mIsUserUnlockingOrUnlockedCache.disableLocal();
+ return ((UserManagerCache) mIpcDataCache).isUserUnlocked(mService::isUserUnlocked, userId);
}
/** @hide */
public static final void invalidateIsUserUnlockedCache() {
- PropertyInvalidatedCache.invalidateCache(CACHE_KEY_IS_USER_UNLOCKED_PROPERTY);
+ UserManagerCache.invalidateUserUnlocked();
}
/**
@@ -3852,8 +3814,10 @@
/** @hide */
@RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+ @CachedProperty(modsFlagOnOrNone = {}, api = "is_user_unlocked")
public boolean isUserUnlockingOrUnlocked(@UserIdInt int userId) {
- return mIsUserUnlockingOrUnlockedCache.query(userId);
+ return ((UserManagerCache) mIpcDataCache)
+ .isUserUnlockingOrUnlocked(mService::isUserUnlockingOrUnlocked, userId);
}
/**
@@ -5686,31 +5650,9 @@
}
}
- private static final String CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY =
- PropertyInvalidatedCache.createPropertyName(
- PropertyInvalidatedCache.MODULE_SYSTEM, "quiet_mode_enabled");
-
- private final PropertyInvalidatedCache<Integer, Boolean> mQuietModeEnabledCache =
- new PropertyInvalidatedCache<Integer, Boolean>(
- 32, CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY) {
- @Override
- public Boolean recompute(Integer query) {
- try {
- return mService.isQuietModeEnabled(query);
- } catch (RemoteException re) {
- throw re.rethrowFromSystemServer();
- }
- }
- @Override
- public boolean bypass(Integer query) {
- return query < 0;
- }
- };
-
-
/** @hide */
public static final void invalidateQuietModeEnabledCache() {
- PropertyInvalidatedCache.invalidateCache(CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY);
+ UserManagerCache.invalidateQuietModeEnabled();
}
/**
@@ -5719,13 +5661,15 @@
* @param userHandle The user handle of the profile to be queried.
* @return true if the profile is in quiet mode, false otherwise.
*/
+ @CachedProperty(modsFlagOnOrNone = {})
public boolean isQuietModeEnabled(UserHandle userHandle) {
- if (android.multiuser.Flags.cacheQuietModeState()){
+ if (android.multiuser.Flags.cacheQuietModeState()) {
final int userId = userHandle.getIdentifier();
if (userId < 0) {
return false;
}
- return mQuietModeEnabledCache.query(userId);
+ return ((UserManagerCache) mIpcDataCache).isQuietModeEnabled(
+ (UserHandle uh) -> mService.isQuietModeEnabled(uh.getIdentifier()), userHandle);
}
try {
return mService.isQuietModeEnabled(userHandle.getIdentifier());
@@ -6424,41 +6368,21 @@
Settings.Global.DEVICE_DEMO_MODE, 0) > 0;
}
- private static final String CACHE_KEY_USER_SERIAL_NUMBER_PROPERTY =
- PropertyInvalidatedCache.createPropertyName(
- PropertyInvalidatedCache.MODULE_SYSTEM, "user_serial_number");
-
- private final PropertyInvalidatedCache<Integer, Integer> mUserSerialNumberCache =
- new PropertyInvalidatedCache<Integer, Integer>(
- 32, CACHE_KEY_USER_SERIAL_NUMBER_PROPERTY) {
- @Override
- public Integer recompute(Integer query) {
- try {
- return mService.getUserSerialNumber(query);
- } catch (RemoteException re) {
- throw re.rethrowFromSystemServer();
- }
- }
- @Override
- public boolean bypass(Integer query) {
- return query <= 0;
- }
- };
-
-
/** @hide */
public static final void invalidateUserSerialNumberCache() {
- PropertyInvalidatedCache.invalidateCache(CACHE_KEY_USER_SERIAL_NUMBER_PROPERTY);
+ UserManagerCache.invalidateUserSerialNumber();
}
/**
* Returns a serial number on this device for a given userId. User handles can be recycled
- * when deleting and creating users, but serial numbers are not reused until the device is wiped.
+ * when deleting and creating users, but serial numbers are not reused until the device is
+ * wiped.
* @param userId
* @return a serial number associated with that user, or -1 if the userId is not valid.
* @hide
*/
@UnsupportedAppUsage
+ @CachedProperty(modsFlagOnOrNone = {})
public int getUserSerialNumber(@UserIdInt int userId) {
// Read only flag should is to fix early access to this API
// cacheUserSerialNumber to be removed after the
@@ -6470,7 +6394,8 @@
if (userId == UserHandle.USER_SYSTEM) {
return UserHandle.USER_SERIAL_SYSTEM;
}
- return mUserSerialNumberCache.query(userId);
+ return ((UserManagerCache) mIpcDataCache).getUserSerialNumber(
+ mService::getUserSerialNumber, userId);
}
try {
return mService.getUserSerialNumber(userId);
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index a2c41c1..1a15d09 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2350,6 +2350,21 @@
"android.settings.ALL_APPS_NOTIFICATION_SETTINGS_FOR_REVIEW";
/**
+ * Activity Action: Show the permission screen for allowing apps to post promoted notifications.
+ * <p>
+ * Input: {@link #EXTRA_APP_PACKAGE}, the package to display.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Output: Nothing.
+ */
+ @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_APP_NOTIFICATION_PROMOTION_SETTINGS
+ = "android.settings.APP_NOTIFICATION_PROMOTION_SETTINGS";
+
+ /**
* Activity Action: Show notification settings for a single app.
* <p>
* Input: {@link #EXTRA_APP_PACKAGE}, the package to display.
@@ -17798,12 +17813,6 @@
public static final String FORCE_NON_DEBUGGABLE_FINAL_BUILD_FOR_COMPAT =
"force_non_debuggable_final_build_for_compat";
- /**
- * Flag to enable the use of ApplicationInfo for getting not-launched status.
- *
- * @hide
- */
- public static final String ENABLE_USE_APP_INFO_NOT_LAUNCHED = "use_app_info_not_launched";
/**
* Current version of signed configuration applied.
@@ -20149,6 +20158,12 @@
public static final int PHONE_SWITCHING_STATUS_IN_PROGRESS_MIGRATION_SUCCESS = 11;
/**
+ * Phone switching has finished account match step.
+ * @hide
+ */
+ public static final int PHONE_SWITCHING_STATUS_ACCOUNTS_MATCHED = 12;
+
+ /**
* Phone switching request source
* @hide
*/
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index a86c961..aedf8e0 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -106,3 +106,10 @@
description: "Clear StrongAuth on add credential"
bug: "320817991"
}
+
+flag {
+ name: "afl_api"
+ namespace: "platform_security"
+ description: "AFL feature"
+ bug: "365994454"
+}
diff --git a/core/java/android/service/carrier/CarrierMessagingServiceWrapper.java b/core/java/android/service/carrier/CarrierMessagingServiceWrapper.java
index de2c6f77..afff8fe 100644
--- a/core/java/android/service/carrier/CarrierMessagingServiceWrapper.java
+++ b/core/java/android/service/carrier/CarrierMessagingServiceWrapper.java
@@ -20,6 +20,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -28,8 +29,10 @@
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.telephony.SmsMessage;
+import com.android.internal.telephony.flags.Flags;
import com.android.internal.util.Preconditions;
import java.util.List;
@@ -91,8 +94,12 @@
mOnServiceReadyCallback = onServiceReadyCallback;
mServiceReadyCallbackExecutor = executor;
mContext = context;
- return context.bindService(intent, mCarrierMessagingServiceConnection,
- Context.BIND_AUTO_CREATE);
+ return Flags.supportCarrierServicesForHsum()
+ ? context.bindServiceAsUser(intent, mCarrierMessagingServiceConnection,
+ Context.BIND_AUTO_CREATE,
+ UserHandle.of(ActivityManager.getCurrentUser()))
+ : context.bindService(intent, mCarrierMessagingServiceConnection,
+ Context.BIND_AUTO_CREATE);
}
/**
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 90ceb44..2748e32 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -152,6 +152,8 @@
long nativeObject, int priority);
private static native void nativeSetWindowCrop(long transactionObj, long nativeObject,
int l, int t, int r, int b);
+ private static native void nativeSetCrop(long transactionObj, long nativeObject,
+ float l, float t, float r, float b);
private static native void nativeSetCornerRadius(long transactionObj, long nativeObject,
float cornerRadius);
private static native void nativeSetBackgroundBlurRadius(long transactionObj, long nativeObject,
@@ -3452,6 +3454,29 @@
}
/**
+ * Bounds the surface and its children to the bounds specified. Size of the surface will be
+ * ignored and only the crop and buffer size will be used to determine the bounds of the
+ * surface. If no crop is specified and the surface has no buffer, the surface bounds is
+ * only constrained by the size of its parent bounds.
+ *
+ * @param sc SurfaceControl to set crop of.
+ * @param crop Bounds of the crop to apply.
+ * @return this This transaction for chaining
+ * @hide
+ */
+ public @NonNull Transaction setCrop(@NonNull SurfaceControl sc, float top, float left,
+ float bottom, float right) {
+ checkPreconditions(sc);
+ if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
+ SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
+ "setCrop", this, sc, "crop={" + top + ", " + left + ", " +
+ bottom + ", " + right + "}");
+ }
+ nativeSetCrop(mNativeObject, sc.mNativeObject, top, left, bottom, right);
+ return this;
+ }
+
+ /**
* Sets the corner radius of a {@link SurfaceControl}.
* @param sc SurfaceControl
* @param cornerRadius Corner radius in pixels.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 8fb17c7..0ca442d 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -7531,6 +7531,7 @@
if (keyEvent.isCanceled()) {
animationCallback.onBackCancelled();
} else {
+ dispatcher.tryInvokeSystemNavigationObserverCallback();
topCallback.onBackInvoked();
}
break;
@@ -7538,6 +7539,7 @@
} else if (topCallback != null) {
if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
if (!keyEvent.isCanceled()) {
+ dispatcher.tryInvokeSystemNavigationObserverCallback();
topCallback.onBackInvoked();
} else {
Log.d(mTag, "Skip onBackInvoked(), reason: keyEvent.isCanceled=true");
diff --git a/core/java/android/window/OnBackInvokedDispatcher.java b/core/java/android/window/OnBackInvokedDispatcher.java
index 0632a37..02ed57d 100644
--- a/core/java/android/window/OnBackInvokedDispatcher.java
+++ b/core/java/android/window/OnBackInvokedDispatcher.java
@@ -16,11 +16,14 @@
package android.window;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
+import com.android.window.flags.Flags;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -43,6 +46,7 @@
@IntDef({
PRIORITY_DEFAULT,
PRIORITY_OVERLAY,
+ PRIORITY_SYSTEM_NAVIGATION_OBSERVER,
})
@Retention(RetentionPolicy.SOURCE)
@interface Priority{}
@@ -67,6 +71,20 @@
int PRIORITY_SYSTEM = -1;
/**
+ * Priority level of {@link OnBackInvokedCallback}s designed to observe system-level back
+ * handling.
+ *
+ * <p>Callbacks registered with this priority do not consume back events. They receive back
+ * events whenever the system handles a back navigation and have no impact on the normal back
+ * navigation flow. Useful for logging or analytics.
+ *
+ * <p>Only one callback with {@link #PRIORITY_SYSTEM_NAVIGATION_OBSERVER} can be registered at a
+ * time.
+ */
+ @FlaggedApi(Flags.FLAG_PREDICTIVE_BACK_PRIORITY_SYSTEM_NAVIGATION_OBSERVER)
+ int PRIORITY_SYSTEM_NAVIGATION_OBSERVER = -2;
+
+ /**
* Registers a {@link OnBackInvokedCallback}.
*
* Within the same priority level, callbacks are invoked in the reverse order in which
diff --git a/core/java/android/window/ProxyOnBackInvokedDispatcher.java b/core/java/android/window/ProxyOnBackInvokedDispatcher.java
index 56c05b2..dfc4a58 100644
--- a/core/java/android/window/ProxyOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ProxyOnBackInvokedDispatcher.java
@@ -16,6 +16,8 @@
package android.window;
+import static com.android.window.flags.Flags.predictiveBackPrioritySystemNavigationObserver;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -95,7 +97,7 @@
synchronized (mLock) {
mCallbacks.add(Pair.create(callback, priority));
if (mActualDispatcher != null) {
- if (priority <= PRIORITY_SYSTEM) {
+ if (priority == PRIORITY_SYSTEM) {
mActualDispatcher.registerSystemOnBackInvokedCallback(callback);
} else {
mActualDispatcher.registerOnBackInvokedCallback(priority, callback);
@@ -123,10 +125,19 @@
}
for (Pair<OnBackInvokedCallback, Integer> callbackPair : mCallbacks) {
int priority = callbackPair.second;
- if (priority >= PRIORITY_DEFAULT) {
- mActualDispatcher.registerOnBackInvokedCallback(priority, callbackPair.first);
+ if (predictiveBackPrioritySystemNavigationObserver()) {
+ if (priority >= PRIORITY_DEFAULT
+ || priority == PRIORITY_SYSTEM_NAVIGATION_OBSERVER) {
+ mActualDispatcher.registerOnBackInvokedCallback(priority, callbackPair.first);
+ } else {
+ mActualDispatcher.registerSystemOnBackInvokedCallback(callbackPair.first);
+ }
} else {
- mActualDispatcher.registerSystemOnBackInvokedCallback(callbackPair.first);
+ if (priority >= PRIORITY_DEFAULT) {
+ mActualDispatcher.registerOnBackInvokedCallback(priority, callbackPair.first);
+ } else {
+ mActualDispatcher.registerSystemOnBackInvokedCallback(callbackPair.first);
+ }
}
}
mCallbacks.clear();
diff --git a/core/java/android/window/TaskConstants.java b/core/java/android/window/TaskConstants.java
index 44bb33db..46bc30e 100644
--- a/core/java/android/window/TaskConstants.java
+++ b/core/java/android/window/TaskConstants.java
@@ -53,25 +53,30 @@
*/
public static final int TASK_CHILD_LAYER_COMPAT_UI = TASK_CHILD_LAYER_REGION_SIZE;
+ /**
+ * Settings dialogs belonging to the task (e.g. Open by default settings dialog)
+ * @hide
+ */
+ public static final int TASK_CHILD_LAYER_SETTINGS_DIALOG = 2 * TASK_CHILD_LAYER_REGION_SIZE;
/**
* Captions, window frames and resize handlers around task windows.
* @hide
*/
- public static final int TASK_CHILD_LAYER_WINDOW_DECORATIONS = 2 * TASK_CHILD_LAYER_REGION_SIZE;
+ public static final int TASK_CHILD_LAYER_WINDOW_DECORATIONS = 3 * TASK_CHILD_LAYER_REGION_SIZE;
/**
* Overlays the task when going into PIP w/ gesture navigation.
* @hide
*/
public static final int TASK_CHILD_LAYER_RECENTS_ANIMATION_PIP_OVERLAY =
- 3 * TASK_CHILD_LAYER_REGION_SIZE;
+ 4 * TASK_CHILD_LAYER_REGION_SIZE;
/**
* Allows other apps to add overlays on the task (i.e. game dashboard)
* @hide
*/
- public static final int TASK_CHILD_LAYER_TASK_OVERLAY = 4 * TASK_CHILD_LAYER_REGION_SIZE;
+ public static final int TASK_CHILD_LAYER_TASK_OVERLAY = 5 * TASK_CHILD_LAYER_REGION_SIZE;
/**
@@ -95,6 +100,7 @@
TASK_CHILD_LAYER_TASK_BACKGROUND,
TASK_CHILD_LAYER_LETTERBOX_BACKGROUND,
TASK_CHILD_LAYER_COMPAT_UI,
+ TASK_CHILD_LAYER_SETTINGS_DIALOG,
TASK_CHILD_LAYER_WINDOW_DECORATIONS,
TASK_CHILD_LAYER_RECENTS_ANIMATION_PIP_OVERLAY,
TASK_CHILD_LAYER_TASK_OVERLAY,
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 314bf89..0dc9263 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -28,6 +28,7 @@
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.app.PendingIntent;
import android.app.WindowConfiguration;
@@ -44,6 +45,7 @@
import android.view.InsetsFrameProvider;
import android.view.InsetsSource;
import android.view.SurfaceControl;
+import android.view.WindowInsets;
import android.view.WindowInsets.Type.InsetsType;
import com.android.window.flags.Flags;
@@ -267,6 +269,23 @@
}
/**
+ * Sets whether the IME insets should be excluded by {@link com.android.server.wm.InsetsPolicy}.
+ * @hide
+ */
+ @SuppressLint("UnflaggedApi")
+ @NonNull
+ public WindowContainerTransaction setExcludeImeInsets(
+ @NonNull WindowContainerToken container, boolean exclude) {
+ final HierarchyOp hierarchyOp =
+ new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES)
+ .setContainer(container.asBinder())
+ .setExcludeInsetsTypes(exclude ? WindowInsets.Type.ime() : 0)
+ .build();
+ mHierarchyOps.add(hierarchyOp);
+ return this;
+ }
+
+ /**
* Sets whether a container or its children should be hidden. When {@code false}, the existing
* visibility of the container applies, but when {@code true} the container will be forced
* to be hidden.
@@ -1449,6 +1468,7 @@
public static final int HIERARCHY_OP_TYPE_MOVE_PIP_ACTIVITY_TO_PINNED_TASK = 18;
public static final int HIERARCHY_OP_TYPE_SET_IS_TRIMMABLE = 19;
public static final int HIERARCHY_OP_TYPE_RESTORE_BACK_NAVIGATION = 20;
+ public static final int HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES = 21;
// The following key(s) are for use with mLaunchOptions:
// When launching a task (eg. from recents), this is the taskId to be launched.
@@ -1512,6 +1532,8 @@
private boolean mIsTrimmableFromRecents;
+ private @InsetsType int mExcludeInsetsTypes;
+
public static HierarchyOp createForReparent(
@NonNull IBinder container, @Nullable IBinder reparent, boolean toTop) {
return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_REPARENT)
@@ -1649,6 +1671,7 @@
mAlwaysOnTop = copy.mAlwaysOnTop;
mReparentLeafTaskIfRelaunch = copy.mReparentLeafTaskIfRelaunch;
mIsTrimmableFromRecents = copy.mIsTrimmableFromRecents;
+ mExcludeInsetsTypes = copy.mExcludeInsetsTypes;
}
protected HierarchyOp(Parcel in) {
@@ -1671,6 +1694,7 @@
mAlwaysOnTop = in.readBoolean();
mReparentLeafTaskIfRelaunch = in.readBoolean();
mIsTrimmableFromRecents = in.readBoolean();
+ mExcludeInsetsTypes = in.readInt();
}
public int getType() {
@@ -1772,6 +1796,10 @@
return mIsTrimmableFromRecents;
}
+ public @InsetsType int getExcludeInsetsTypes() {
+ return mExcludeInsetsTypes;
+ }
+
/** Gets a string representation of a hierarchy-op type. */
public static String hopToString(int type) {
switch (type) {
@@ -1795,6 +1823,7 @@
return "setReparentLeafTaskIfRelaunch";
case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION:
return "addTaskFragmentOperation";
+ case HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES: return "setExcludeInsetsTypes";
default: return "HOP(" + type + ")";
}
}
@@ -1868,6 +1897,11 @@
sb.append("fragmentToken= ").append(mContainer)
.append(" operation= ").append(mTaskFragmentOperation);
break;
+ case HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES:
+ sb.append("container= ").append(mContainer)
+ .append(" mExcludeInsetsTypes= ")
+ .append(WindowInsets.Type.toString(mExcludeInsetsTypes));
+ break;
case HIERARCHY_OP_TYPE_SET_IS_TRIMMABLE:
sb.append("container= ").append(mContainer)
.append(" isTrimmable= ")
@@ -1903,6 +1937,7 @@
dest.writeBoolean(mAlwaysOnTop);
dest.writeBoolean(mReparentLeafTaskIfRelaunch);
dest.writeBoolean(mIsTrimmableFromRecents);
+ dest.writeInt(mExcludeInsetsTypes);
}
@Override
@@ -1974,6 +2009,8 @@
private boolean mIsTrimmableFromRecents;
+ private @InsetsType int mExcludeInsetsTypes;
+
Builder(int type) {
mType = type;
}
@@ -2069,6 +2106,11 @@
return this;
}
+ Builder setExcludeInsetsTypes(@InsetsType int excludeInsetsTypes) {
+ mExcludeInsetsTypes = excludeInsetsTypes;
+ return this;
+ }
+
HierarchyOp build() {
final HierarchyOp hierarchyOp = new HierarchyOp(mType);
hierarchyOp.mContainer = mContainer;
@@ -2093,6 +2135,7 @@
hierarchyOp.mIncludingParents = mIncludingParents;
hierarchyOp.mReparentLeafTaskIfRelaunch = mReparentLeafTaskIfRelaunch;
hierarchyOp.mIsTrimmableFromRecents = mIsTrimmableFromRecents;
+ hierarchyOp.mExcludeInsetsTypes = mExcludeInsetsTypes;
return hierarchyOp;
}
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index bb89a24..51bc7d5 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -16,6 +16,8 @@
package android.window;
+import static com.android.window.flags.Flags.predictiveBackPrioritySystemNavigationObserver;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
@@ -103,6 +105,9 @@
public final TreeMap<Integer, ArrayList<OnBackInvokedCallback>>
mOnBackInvokedCallbacks = new TreeMap<>();
+ @VisibleForTesting
+ public OnBackInvokedCallback mSystemNavigationObserverCallback = null;
+
private Checker mChecker;
private final Object mLock = new Object();
// The threshold for back swipe full progress.
@@ -170,6 +175,20 @@
}
}
+ private void registerSystemNavigationObserverCallback(@NonNull OnBackInvokedCallback callback) {
+ synchronized (mLock) {
+ // If callback has already been added as regular callback, remove it.
+ if (mAllCallbacks.containsKey(callback)) {
+ if (DEBUG) {
+ Log.i(TAG, "Callback already added. Removing and re-adding it as "
+ + "system-navigation-observer-callback.");
+ }
+ removeCallbackInternal(callback);
+ }
+ mSystemNavigationObserverCallback = callback;
+ }
+ }
+
/**
* Register a callback bypassing platform checks. This is used to register compatibility
* callbacks.
@@ -181,6 +200,12 @@
mImeDispatcher.registerOnBackInvokedCallback(priority, callback);
return;
}
+ if (predictiveBackPrioritySystemNavigationObserver()) {
+ if (priority == PRIORITY_SYSTEM_NAVIGATION_OBSERVER) {
+ registerSystemNavigationObserverCallback(callback);
+ return;
+ }
+ }
if (callback instanceof ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback) {
if (callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback
&& mImeBackAnimationController != null) {
@@ -202,6 +227,13 @@
Integer prevPriority = mAllCallbacks.get(callback);
mOnBackInvokedCallbacks.get(prevPriority).remove(callback);
}
+ if (mSystemNavigationObserverCallback == callback) {
+ mSystemNavigationObserverCallback = null;
+ if (DEBUG) {
+ Log.i(TAG, "Callback already registered (as system-navigation-observer "
+ + "callback). Removing and re-adding it.");
+ }
+ }
OnBackInvokedCallback previousTopCallback = getTopCallback();
callbacks.add(callback);
@@ -221,6 +253,10 @@
mImeDispatcher.unregisterOnBackInvokedCallback(callback);
return;
}
+ if (mSystemNavigationObserverCallback == callback) {
+ mSystemNavigationObserverCallback = null;
+ return;
+ }
if (callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback) {
callback = mImeBackAnimationController;
}
@@ -230,25 +266,29 @@
}
return;
}
- OnBackInvokedCallback previousTopCallback = getTopCallback();
- Integer priority = mAllCallbacks.get(callback);
- ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority);
- callbacks.remove(callback);
- if (callbacks.isEmpty()) {
- mOnBackInvokedCallbacks.remove(priority);
- }
- mAllCallbacks.remove(callback);
- // Re-populate the top callback to WM if the removed callback was previously the top
- // one.
- if (previousTopCallback == callback) {
- // We should call onBackCancelled() when an active callback is removed from
- // dispatcher.
- mProgressAnimator.removeOnBackCancelledFinishCallback();
- mProgressAnimator.removeOnBackInvokedFinishCallback();
- sendCancelledIfInProgress(callback);
- mHandler.post(mProgressAnimator::reset);
- setTopOnBackInvokedCallback(getTopCallback());
- }
+ removeCallbackInternal(callback);
+ }
+ }
+
+ private void removeCallbackInternal(@NonNull OnBackInvokedCallback callback) {
+ OnBackInvokedCallback previousTopCallback = getTopCallback();
+ Integer priority = mAllCallbacks.get(callback);
+ ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority);
+ callbacks.remove(callback);
+ if (callbacks.isEmpty()) {
+ mOnBackInvokedCallbacks.remove(priority);
+ }
+ mAllCallbacks.remove(callback);
+ // Re-populate the top callback to WM if the removed callback was previously the top
+ // one.
+ if (previousTopCallback == callback) {
+ // We should call onBackCancelled() when an active callback is removed from
+ // dispatcher.
+ mProgressAnimator.removeOnBackCancelledFinishCallback();
+ mProgressAnimator.removeOnBackInvokedFinishCallback();
+ sendCancelledIfInProgress(callback);
+ mHandler.post(mProgressAnimator::reset);
+ setTopOnBackInvokedCallback(getTopCallback());
}
}
@@ -304,6 +344,7 @@
mHandler.post(mProgressAnimator::reset);
mAllCallbacks.clear();
mOnBackInvokedCallbacks.clear();
+ mSystemNavigationObserverCallback = null;
}
}
@@ -315,6 +356,25 @@
}
}
+ /**
+ * Tries to call {@link OnBackInvokedCallback#onBackInvoked} on the system navigation observer
+ * callback (if one is set and if the top-most regular callback has
+ * {@link OnBackInvokedDispatcher#PRIORITY_SYSTEM})
+ */
+ public void tryInvokeSystemNavigationObserverCallback() {
+ OnBackInvokedCallback topCallback = getTopCallback();
+ Integer callbackPriority = mAllCallbacks.getOrDefault(topCallback, null);
+ if (callbackPriority != null && callbackPriority == PRIORITY_SYSTEM) {
+ invokeSystemNavigationObserverCallback();
+ }
+ }
+
+ private void invokeSystemNavigationObserverCallback() {
+ if (mSystemNavigationObserverCallback != null) {
+ mSystemNavigationObserverCallback.onBackInvoked();
+ }
+ }
+
private void setTopOnBackInvokedCallback(@Nullable OnBackInvokedCallback callback) {
if (mWindowSession == null || mWindow == null) {
return;
@@ -324,7 +384,9 @@
if (callback != null) {
int priority = mAllCallbacks.get(callback);
final IOnBackInvokedCallback iCallback = new OnBackInvokedCallbackWrapper(callback,
- mTouchTracker, mProgressAnimator, mHandler, this::callOnKeyPreIme);
+ mTouchTracker, mProgressAnimator, mHandler, this::callOnKeyPreIme,
+ this::invokeSystemNavigationObserverCallback,
+ /*isSystemCallback*/ priority == PRIORITY_SYSTEM);
callbackInfo = new OnBackInvokedCallbackInfo(
iCallback,
priority,
@@ -416,18 +478,26 @@
private final Handler mHandler;
@NonNull
private final BooleanSupplier mOnKeyPreIme;
+ @NonNull
+ private final Runnable mSystemNavigationObserverCallbackRunnable;
+ private final boolean mIsSystemCallback;
OnBackInvokedCallbackWrapper(
@NonNull OnBackInvokedCallback callback,
@NonNull BackTouchTracker touchTracker,
@NonNull BackProgressAnimator progressAnimator,
@NonNull Handler handler,
- @NonNull BooleanSupplier onKeyPreIme) {
+ @NonNull BooleanSupplier onKeyPreIme,
+ @NonNull Runnable systemNavigationObserverCallbackRunnable,
+ boolean isSystemCallback
+ ) {
mCallback = new WeakReference<>(callback);
mTouchTracker = touchTracker;
mProgressAnimator = progressAnimator;
mHandler = handler;
mOnKeyPreIme = onKeyPreIme;
+ mSystemNavigationObserverCallbackRunnable = systemNavigationObserverCallbackRunnable;
+ mIsSystemCallback = isSystemCallback;
}
@Override
@@ -494,9 +564,17 @@
OnBackAnimationCallback animationCallback = getBackAnimationCallback();
if (animationCallback != null
&& !(callback instanceof ImeBackAnimationController)) {
- mProgressAnimator.onBackInvoked(callback::onBackInvoked);
+ mProgressAnimator.onBackInvoked(() -> {
+ if (mIsSystemCallback) {
+ mSystemNavigationObserverCallbackRunnable.run();
+ }
+ callback.onBackInvoked();
+ });
} else {
mProgressAnimator.reset();
+ if (mIsSystemCallback) {
+ mSystemNavigationObserverCallbackRunnable.run();
+ }
callback.onBackInvoked();
}
});
@@ -597,9 +675,18 @@
+ " application manifest.");
return false;
}
- if (priority < 0) {
- throw new IllegalArgumentException("Application registered OnBackInvokedCallback "
- + "cannot have negative priority. Priority: " + priority);
+ if (predictiveBackPrioritySystemNavigationObserver()) {
+ if (priority < 0 && priority != PRIORITY_SYSTEM_NAVIGATION_OBSERVER) {
+ throw new IllegalArgumentException("Application registered "
+ + "OnBackInvokedCallback cannot have negative priority. Priority: "
+ + priority);
+ }
+ } else {
+ if (priority < 0) {
+ throw new IllegalArgumentException("Application registered "
+ + "OnBackInvokedCallback cannot have negative priority. Priority: "
+ + priority);
+ }
}
Objects.requireNonNull(callback);
return true;
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 086063f..c9b93c9 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -307,3 +307,11 @@
bug: "364930619"
is_fixed_read_only: true
}
+
+flag {
+ name: "predictive_back_priority_system_navigation_observer"
+ namespace: "systemui"
+ description: "PRIORITY_SYSTEM_NAVIGATION_OBSERVER predictive back API extension"
+ is_fixed_read_only: true
+ bug: "362938401"
+}
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 9797d96..4d2195d 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -73,6 +73,12 @@
srcs: [
"android_animation_PropertyValuesHolder.cpp",
+ "android_database_CursorWindow.cpp",
+ "android_database_SQLiteCommon.cpp",
+ "android_database_SQLiteConnection.cpp",
+ "android_database_SQLiteGlobal.cpp",
+ "android_database_SQLiteDebug.cpp",
+ "android_database_SQLiteRawStatement.cpp",
"android_os_SystemClock.cpp",
"android_os_SystemProperties.cpp",
"android_os_Trace.cpp",
@@ -157,12 +163,6 @@
"android_opengl_GLES31.cpp",
"android_opengl_GLES31Ext.cpp",
"android_opengl_GLES32.cpp",
- "android_database_CursorWindow.cpp",
- "android_database_SQLiteCommon.cpp",
- "android_database_SQLiteConnection.cpp",
- "android_database_SQLiteGlobal.cpp",
- "android_database_SQLiteDebug.cpp",
- "android_database_SQLiteRawStatement.cpp",
"android_graphics_GraphicBuffer.cpp",
"android_graphics_SurfaceTexture.cpp",
"android_view_CompositionSamplingListener.cpp",
@@ -427,6 +427,7 @@
"libnativehelper_jvm",
"libpiex",
"libpng",
+ "libsqlite",
"libtiff_directory",
"libui-types",
"libutils",
@@ -442,12 +443,6 @@
host_linux: {
srcs: [
"android_content_res_ApkAssets.cpp",
- "android_database_CursorWindow.cpp",
- "android_database_SQLiteCommon.cpp",
- "android_database_SQLiteConnection.cpp",
- "android_database_SQLiteGlobal.cpp",
- "android_database_SQLiteDebug.cpp",
- "android_database_SQLiteRawStatement.cpp",
"android_hardware_input_InputApplicationHandle.cpp",
"android_os_MessageQueue.cpp",
"android_os_Parcel.cpp",
@@ -463,7 +458,6 @@
],
static_libs: [
"libbinderthreadstateutils",
- "libsqlite",
"libgui_window_info_static",
],
shared_libs: [
diff --git a/core/jni/android_database_CursorWindow.cpp b/core/jni/android_database_CursorWindow.cpp
index c0e9215..18c3146 100644
--- a/core/jni/android_database_CursorWindow.cpp
+++ b/core/jni/android_database_CursorWindow.cpp
@@ -38,7 +38,9 @@
#define LOG_NDEBUG 1
#include <androidfw/CursorWindow.h>
+#ifdef __linux__
#include "android_os_Parcel.h"
+#endif
#include "android_util_Binder.h"
#include "android_database_SQLiteCommon.h"
@@ -111,6 +113,7 @@
return 0;
}
+#ifdef __linux__
static jlong nativeCreateFromParcel(JNIEnv* env, jclass clazz, jobject parcelObj) {
Parcel* parcel = parcelForJavaObject(env, parcelObj);
@@ -128,6 +131,7 @@
window->getNumRows(), window->getNumColumns(), window);
return reinterpret_cast<jlong>(window);
}
+#endif
static void nativeDispose(JNIEnv* env, jclass clazz, jlong windowPtr) {
CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
@@ -142,6 +146,7 @@
return env->NewStringUTF(window->name().c_str());
}
+#ifdef __linux__
static void nativeWriteToParcel(JNIEnv * env, jclass clazz, jlong windowPtr,
jobject parcelObj) {
CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
@@ -154,6 +159,7 @@
jniThrowRuntimeException(env, msg.c_str());
}
}
+#endif
static void nativeClear(JNIEnv * env, jclass clazz, jlong windowPtr) {
CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
@@ -520,55 +526,35 @@
return true;
}
-static const JNINativeMethod sMethods[] =
-{
- /* name, signature, funcPtr */
- { "nativeCreate", "(Ljava/lang/String;I)J",
- (void*)nativeCreate },
- { "nativeCreateFromParcel", "(Landroid/os/Parcel;)J",
- (void*)nativeCreateFromParcel },
- { "nativeDispose", "(J)V",
- (void*)nativeDispose },
- { "nativeWriteToParcel", "(JLandroid/os/Parcel;)V",
- (void*)nativeWriteToParcel },
+static const JNINativeMethod sMethods[] = {
+ /* name, signature, funcPtr */
+ {"nativeCreate", "(Ljava/lang/String;I)J", (void*)nativeCreate},
+ {"nativeDispose", "(J)V", (void*)nativeDispose},
+#ifdef __linux__
+ {"nativeCreateFromParcel", "(Landroid/os/Parcel;)J", (void*)nativeCreateFromParcel},
+ {"nativeWriteToParcel", "(JLandroid/os/Parcel;)V", (void*)nativeWriteToParcel},
+#endif
+ {"nativeGetName", "(J)Ljava/lang/String;", (void*)nativeGetName},
+ {"nativeGetBlob", "(JII)[B", (void*)nativeGetBlob},
+ {"nativeGetString", "(JII)Ljava/lang/String;", (void*)nativeGetString},
+ {"nativeCopyStringToBuffer", "(JIILandroid/database/CharArrayBuffer;)V",
+ (void*)nativeCopyStringToBuffer},
+ {"nativePutBlob", "(J[BII)Z", (void*)nativePutBlob},
+ {"nativePutString", "(JLjava/lang/String;II)Z", (void*)nativePutString},
- { "nativeGetName", "(J)Ljava/lang/String;",
- (void*)nativeGetName },
- { "nativeGetBlob", "(JII)[B",
- (void*)nativeGetBlob },
- { "nativeGetString", "(JII)Ljava/lang/String;",
- (void*)nativeGetString },
- { "nativeCopyStringToBuffer", "(JIILandroid/database/CharArrayBuffer;)V",
- (void*)nativeCopyStringToBuffer },
- { "nativePutBlob", "(J[BII)Z",
- (void*)nativePutBlob },
- { "nativePutString", "(JLjava/lang/String;II)Z",
- (void*)nativePutString },
+ // ------- @FastNative below here ----------------------
+ {"nativeClear", "(J)V", (void*)nativeClear},
+ {"nativeGetNumRows", "(J)I", (void*)nativeGetNumRows},
+ {"nativeSetNumColumns", "(JI)Z", (void*)nativeSetNumColumns},
+ {"nativeAllocRow", "(J)Z", (void*)nativeAllocRow},
+ {"nativeFreeLastRow", "(J)V", (void*)nativeFreeLastRow},
+ {"nativeGetType", "(JII)I", (void*)nativeGetType},
+ {"nativeGetLong", "(JII)J", (void*)nativeGetLong},
+ {"nativeGetDouble", "(JII)D", (void*)nativeGetDouble},
- // ------- @FastNative below here ----------------------
- { "nativeClear", "(J)V",
- (void*)nativeClear },
- { "nativeGetNumRows", "(J)I",
- (void*)nativeGetNumRows },
- { "nativeSetNumColumns", "(JI)Z",
- (void*)nativeSetNumColumns },
- { "nativeAllocRow", "(J)Z",
- (void*)nativeAllocRow },
- { "nativeFreeLastRow", "(J)V",
- (void*)nativeFreeLastRow },
- { "nativeGetType", "(JII)I",
- (void*)nativeGetType },
- { "nativeGetLong", "(JII)J",
- (void*)nativeGetLong },
- { "nativeGetDouble", "(JII)D",
- (void*)nativeGetDouble },
-
- { "nativePutLong", "(JJII)Z",
- (void*)nativePutLong },
- { "nativePutDouble", "(JDII)Z",
- (void*)nativePutDouble },
- { "nativePutNull", "(JII)Z",
- (void*)nativePutNull },
+ {"nativePutLong", "(JJII)Z", (void*)nativePutLong},
+ {"nativePutDouble", "(JDII)Z", (void*)nativePutDouble},
+ {"nativePutNull", "(JII)Z", (void*)nativePutNull},
};
int register_android_database_CursorWindow(JNIEnv* env)
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 17c89f8..71ba214 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -48,6 +48,7 @@
#include <ui/DisplayMode.h>
#include <ui/DisplayedFrameStats.h>
#include <ui/DynamicDisplayInfo.h>
+#include <ui/FloatRect.h>
#include <ui/FrameStats.h>
#include <ui/GraphicTypes.h>
#include <ui/HdrCapabilities.h>
@@ -992,6 +993,15 @@
transaction->setCrop(ctrl, crop);
}
+static void nativeSetCrop(JNIEnv* env, jclass clazz, jlong transactionObj, jlong nativeObject,
+ jfloat l, jfloat t, jfloat r, jfloat b) {
+ auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+
+ SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject);
+ FloatRect crop(l, t, r, b);
+ transaction->setCrop(ctrl, crop);
+}
+
static void nativeSetCornerRadius(JNIEnv* env, jclass clazz, jlong transactionObj,
jlong nativeObject, jfloat cornerRadius) {
auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
@@ -2347,6 +2357,8 @@
(void*)nativeSetFrameRateSelectionPriority },
{"nativeSetWindowCrop", "(JJIIII)V",
(void*)nativeSetWindowCrop },
+ {"nativeSetCrop", "(JJFFFF)V",
+ (void*)nativeSetCrop },
{"nativeSetCornerRadius", "(JJF)V",
(void*)nativeSetCornerRadius },
{"nativeSetBackgroundBlurRadius", "(JJI)V",
diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp
index 19f8299..88b3e1c 100644
--- a/core/jni/platform/host/HostRuntime.cpp
+++ b/core/jni/platform/host/HostRuntime.cpp
@@ -115,6 +115,9 @@
#ifdef __linux__
{"android.content.res.ApkAssets", REG_JNI(register_android_content_res_ApkAssets)},
{"android.content.res.AssetManager", REG_JNI(register_android_content_AssetManager)},
+ {"android.content.res.StringBlock", REG_JNI(register_android_content_StringBlock)},
+ {"android.content.res.XmlBlock", REG_JNI(register_android_content_XmlBlock)},
+#endif
{"android.database.CursorWindow", REG_JNI(register_android_database_CursorWindow)},
{"android.database.sqlite.SQLiteConnection",
REG_JNI(register_android_database_SQLiteConnection)},
@@ -122,9 +125,6 @@
{"android.database.sqlite.SQLiteDebug", REG_JNI(register_android_database_SQLiteDebug)},
{"android.database.sqlite.SQLiteRawStatement",
REG_JNI(register_android_database_SQLiteRawStatement)},
-#endif
- {"android.content.res.StringBlock", REG_JNI(register_android_content_StringBlock)},
- {"android.content.res.XmlBlock", REG_JNI(register_android_content_XmlBlock)},
#ifdef __linux__
{"android.os.Binder", REG_JNI(register_android_os_Binder)},
{"android.os.FileObserver", REG_JNI(register_android_os_FileObserver)},
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index ed33ede..f067b51 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -792,6 +792,7 @@
<protected-broadcast android:name="com.android.cellbroadcastreceiver.GET_LATEST_CB_AREA_INFO" />
<protected-broadcast android:name="com.android.internal.telephony.ACTION_CARRIER_CERTIFICATE_DOWNLOAD" />
<protected-broadcast android:name="com.android.internal.telephony.action.COUNTRY_OVERRIDE" />
+ <protected-broadcast android:name="com.android.internal.telephony.action.SILENCE_WIFI_CALLING_NOTIFICATION"/>
<protected-broadcast android:name="com.android.internal.telephony.OPEN_DEFAULT_SMS_APP" />
<protected-broadcast android:name="com.android.internal.telephony.ACTION_TEST_OVERRIDE_CARRIER_ID" />
<protected-broadcast android:name="android.telephony.action.SIM_CARD_STATE_CHANGED" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 1a3a30d..b90ee2b 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1529,6 +1529,11 @@
factory reset. -->
<bool name="config_enableCredentialFactoryResetProtection">true</bool>
+ <!-- If true, then work around broken Weaver HALs that don't work reliably before the device has
+ fully booted. Setting this to true weakens a security feature; it should be done only when
+ necessary, though it is still better than not using Weaver at all. -->
+ <bool name="config_disableWeaverOnUnsecuredUsers">false</bool>
+
<!-- Control the behavior when the user long presses the home button.
0 - Nothing
1 - Launch all apps intent
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d5298ac..c50c336 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3988,6 +3988,7 @@
<java-symbol type="string" name="foreground_service_multiple_separator" />
<java-symbol type="bool" name="config_enableCredentialFactoryResetProtection" />
+ <java-symbol type="bool" name="config_disableWeaverOnUnsecuredUsers" />
<!-- ETWS primary messages -->
<java-symbol type="string" name="etws_primary_default_message_earthquake" />
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index 0bda0ff..0a4c5e6 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -18,6 +18,9 @@
import static android.window.OnBackInvokedDispatcher.PRIORITY_DEFAULT;
import static android.window.OnBackInvokedDispatcher.PRIORITY_OVERLAY;
+import static android.window.OnBackInvokedDispatcher.PRIORITY_SYSTEM_NAVIGATION_OBSERVER;
+
+import static com.android.window.flags.Flags.FLAG_PREDICTIVE_BACK_PRIORITY_SYSTEM_NAVIGATION_OBSERVER;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -39,6 +42,10 @@
import android.os.Looper;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.view.IWindow;
import android.view.IWindowSession;
import android.view.ImeBackAnimationController;
@@ -80,6 +87,8 @@
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
@Mock
private IWindowSession mWindowSession;
@@ -145,7 +154,8 @@
assertEquals("No setOnBackInvokedCallbackInfo", mCallbackInfoCalls, actual);
}
- private void assertCallbacksSize(int expectedDefault, int expectedOverlay) {
+ private void assertCallbacksSize(int expectedDefault, int expectedOverlay,
+ int expectedObserver) {
ArrayList<OnBackInvokedCallback> callbacksDefault = mDispatcher
.mOnBackInvokedCallbacks.get(PRIORITY_DEFAULT);
int actualSizeDefault = callbacksDefault != null ? callbacksDefault.size() : 0;
@@ -155,6 +165,10 @@
.mOnBackInvokedCallbacks.get(PRIORITY_OVERLAY);
int actualSizeOverlay = callbacksOverlay != null ? callbacksOverlay.size() : 0;
assertEquals("mOnBackInvokedCallbacks OVERLAY size", expectedOverlay, actualSizeOverlay);
+
+ int actualSizeObserver = mDispatcher.mSystemNavigationObserverCallback == null ? 0 : 1;
+ assertEquals("mOnBackInvokedCallbacks SYSTEM_NAVIGATION_OBSERVER size", expectedObserver,
+ actualSizeObserver);
}
private void assertTopCallback(OnBackInvokedCallback expectedCallback) {
@@ -164,13 +178,13 @@
@Test
public void registerCallback_samePriority_sameCallback() throws RemoteException {
mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback1);
- assertCallbacksSize(/* default */ 1, /* overlay */ 0);
+ assertCallbacksSize(/* default */ 1, /* overlay */ 0, /* observer */ 0);
assertSetCallbackInfo();
assertTopCallback(mCallback1);
// The callback is removed and added again
mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback1);
- assertCallbacksSize(/* default */ 1, /* overlay */ 0);
+ assertCallbacksSize(/* default */ 1, /* overlay */ 0, /* observer */ 0);
assertSetCallbackInfo();
assertTopCallback(mCallback1);
@@ -182,13 +196,13 @@
@Test
public void registerCallback_samePriority_differentCallback() throws RemoteException {
mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback1);
- assertCallbacksSize(/* default */ 1, /* overlay */ 0);
+ assertCallbacksSize(/* default */ 1, /* overlay */ 0, /* observer */ 0);
assertSetCallbackInfo();
assertTopCallback(mCallback1);
// The new callback becomes the TopCallback
mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback2);
- assertCallbacksSize(/* default */ 2, /* overlay */ 0);
+ assertCallbacksSize(/* default */ 2, /* overlay */ 0, /* observer */ 0);
assertSetCallbackInfo();
assertTopCallback(mCallback2);
@@ -201,13 +215,13 @@
@Test
public void registerCallback_differentPriority_sameCallback() throws RemoteException {
mDispatcher.registerOnBackInvokedCallback(PRIORITY_OVERLAY, mCallback1);
- assertCallbacksSize(/* default */ 0, /* overlay */ 1);
+ assertCallbacksSize(/* default */ 0, /* overlay */ 1, /* observer */ 0);
assertSetCallbackInfo();
assertTopCallback(mCallback1);
// The callback is moved to the new priority list
mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback1);
- assertCallbacksSize(/* default */ 1, /* overlay */ 0);
+ assertCallbacksSize(/* default */ 1, /* overlay */ 0, /* observer */ 0);
assertSetCallbackInfo();
assertTopCallback(mCallback1);
@@ -220,13 +234,13 @@
public void registerCallback_differentPriority_differentCallback() throws RemoteException {
mDispatcher.registerOnBackInvokedCallback(PRIORITY_OVERLAY, mCallback1);
assertSetCallbackInfo();
- assertCallbacksSize(/* default */ 0, /* overlay */ 1);
+ assertCallbacksSize(/* default */ 0, /* overlay */ 1, /* observer */ 0);
assertTopCallback(mCallback1);
// The callback with higher priority is still the TopCallback
mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback2);
assertNoSetCallbackInfo();
- assertCallbacksSize(/* default */ 1, /* overlay */ 1);
+ assertCallbacksSize(/* default */ 1, /* overlay */ 1, /* observer */ 0);
assertTopCallback(mCallback1);
waitForIdle();
@@ -238,22 +252,22 @@
@Test
public void registerCallback_sameInstanceAddedTwice() throws RemoteException {
mDispatcher.registerOnBackInvokedCallback(PRIORITY_OVERLAY, mCallback1);
- assertCallbacksSize(/* default */ 0, /* overlay */ 1);
+ assertCallbacksSize(/* default */ 0, /* overlay */ 1, /* observer */ 0);
assertSetCallbackInfo();
assertTopCallback(mCallback1);
mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback2);
- assertCallbacksSize(/* default */ 1, /* overlay */ 1);
+ assertCallbacksSize(/* default */ 1, /* overlay */ 1, /* observer */ 0);
assertNoSetCallbackInfo();
assertTopCallback(mCallback1);
mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback1);
- assertCallbacksSize(/* default */ 2, /* overlay */ 0);
+ assertCallbacksSize(/* default */ 2, /* overlay */ 0, /* observer */ 0);
assertSetCallbackInfo();
assertTopCallback(mCallback1);
mDispatcher.registerOnBackInvokedCallback(PRIORITY_OVERLAY, mCallback2);
- assertCallbacksSize(/* default */ 1, /* overlay */ 1);
+ assertCallbacksSize(/* default */ 1, /* overlay */ 1, /* observer */ 0);
assertSetCallbackInfo();
assertTopCallback(mCallback2);
@@ -570,6 +584,102 @@
assertFalse(mDispatcher.mProgressAnimator.isBackAnimationInProgress());
}
+ @Test(expected = IllegalArgumentException.class)
+ @RequiresFlagsDisabled(FLAG_PREDICTIVE_BACK_PRIORITY_SYSTEM_NAVIGATION_OBSERVER)
+ public void testNoUiCallback_registrationFailsWithoutFlaggedApiEnabled() {
+ mDispatcher.registerOnBackInvokedCallback(PRIORITY_SYSTEM_NAVIGATION_OBSERVER, mCallback2);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_PRIORITY_SYSTEM_NAVIGATION_OBSERVER)
+ public void testNoUiCallback_invokedWithSystemCallback() throws RemoteException {
+ mDispatcher.registerSystemOnBackInvokedCallback(mCallback1);
+ mDispatcher.registerOnBackInvokedCallback(PRIORITY_SYSTEM_NAVIGATION_OBSERVER, mCallback2);
+
+ assertCallbacksSize(/* default */ 0, /* overlay */ 0, /* observer */ 1);
+ OnBackInvokedCallbackInfo callbackInfo = assertSetCallbackInfo();
+ assertTopCallback(mCallback1);
+
+ callbackInfo.getCallback().onBackStarted(mBackEvent);
+ waitForIdle();
+ verify(mCallback1).onBackStarted(any());
+ verify(mCallback2, never()).onBackStarted(any());
+
+ callbackInfo.getCallback().onBackProgressed(mBackEvent);
+ waitForIdle();
+ verify(mCallback1).onBackProgressed(any());
+ verify(mCallback2, never()).onBackProgressed(any());
+
+ callbackInfo.getCallback().onBackCancelled();
+ waitForIdle();
+ verify(mCallback1, timeout(1000)).onBackCancelled();
+ verify(mCallback2, never()).onBackCancelled();
+
+ // start new gesture to test onBackInvoked case
+ callbackInfo.getCallback().onBackStarted(mBackEvent);
+ callbackInfo.getCallback().onBackInvoked();
+ waitForIdle();
+ verify(mCallback1, timeout(1000)).onBackInvoked();
+ verify(mCallback2).onBackInvoked();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_PRIORITY_SYSTEM_NAVIGATION_OBSERVER)
+ public void testNoUiCallback_notInvokedWithNonSystemCallback() throws RemoteException {
+ mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback1);
+ mDispatcher.registerOnBackInvokedCallback(PRIORITY_SYSTEM_NAVIGATION_OBSERVER, mCallback2);
+
+ assertCallbacksSize(/* default */ 1, /* overlay */ 0, /* observer */ 1);
+ OnBackInvokedCallbackInfo callbackInfo = assertSetCallbackInfo();
+ assertTopCallback(mCallback1);
+
+ callbackInfo.getCallback().onBackStarted(mBackEvent);
+ waitForIdle();
+ verify(mCallback1).onBackStarted(any());
+ verify(mCallback2, never()).onBackStarted(any());
+
+ callbackInfo.getCallback().onBackProgressed(mBackEvent);
+ waitForIdle();
+ verify(mCallback1).onBackProgressed(any());
+ verify(mCallback2, never()).onBackProgressed(any());
+
+ callbackInfo.getCallback().onBackCancelled();
+ waitForIdle();
+ verify(mCallback1, timeout(1000)).onBackCancelled();
+ verify(mCallback2, never()).onBackCancelled();
+
+ // start new gesture to test onBackInvoked case
+ callbackInfo.getCallback().onBackStarted(mBackEvent);
+ callbackInfo.getCallback().onBackInvoked();
+ waitForIdle();
+ verify(mCallback1, timeout(1000)).onBackInvoked();
+ verify(mCallback2, never()).onBackInvoked();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_PRIORITY_SYSTEM_NAVIGATION_OBSERVER)
+ public void testNoUiCallback_reregistrations() {
+ mDispatcher.registerOnBackInvokedCallback(PRIORITY_SYSTEM_NAVIGATION_OBSERVER, mCallback1);
+ assertCallbacksSize(/* default */ 0, /* overlay */ 0, /* observer */ 1);
+ assertEquals(mCallback1, mDispatcher.mSystemNavigationObserverCallback);
+
+ // test reregistration of observer-callback as observer-callback
+ mDispatcher.registerOnBackInvokedCallback(PRIORITY_SYSTEM_NAVIGATION_OBSERVER, mCallback2);
+ assertCallbacksSize(/* default */ 0, /* overlay */ 0, /* observer */ 1);
+ assertEquals(mCallback2, mDispatcher.mSystemNavigationObserverCallback);
+
+ // test reregistration of observer-callback as regular callback
+ mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback2);
+ assertCallbacksSize(/* default */ 1, /* overlay */ 0, /* observer */ 0);
+
+ // test reregistration of regular callback as observer-callback
+ mDispatcher.registerOnBackInvokedCallback(PRIORITY_SYSTEM_NAVIGATION_OBSERVER, mCallback2);
+ assertCallbacksSize(/* default */ 0, /* overlay */ 0, /* observer */ 1);
+
+ mDispatcher.unregisterOnBackInvokedCallback(mCallback2);
+ assertCallbacksSize(/* default */ 0, /* overlay */ 0, /* observer */ 0);
+ }
+
private BackMotionEvent backMotionEventFrom(float progress) {
return new BackMotionEvent(
/* touchX = */ 0,
@@ -585,13 +695,13 @@
private void verifyImeCallackRegistrations() throws RemoteException {
// verify default callback is replaced with ImeBackAnimationController
mDispatcher.registerOnBackInvokedCallbackUnchecked(mDefaultImeCallback, PRIORITY_DEFAULT);
- assertCallbacksSize(/* default */ 1, /* overlay */ 0);
+ assertCallbacksSize(/* default */ 1, /* overlay */ 0, /* observer */ 0);
assertSetCallbackInfo();
assertTopCallback(mImeBackAnimationController);
// verify regular ime callback is successfully registered
mDispatcher.registerOnBackInvokedCallbackUnchecked(mImeCallback, PRIORITY_DEFAULT);
- assertCallbacksSize(/* default */ 2, /* overlay */ 0);
+ assertCallbacksSize(/* default */ 2, /* overlay */ 0, /* observer */ 0);
assertSetCallbackInfo();
assertTopCallback(mImeCallback);
}
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 880f30c..2e72f0e 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -585,6 +585,8 @@
<permission name="android.permission.EXECUTE_APP_FUNCTIONS" />
<!-- Permission required for CTS test - CtsNfcTestCases -->
<permission name="android.permission.NFC_SET_CONTROLLER_ALWAYS_ON" />
+ <!-- Permission required for CTS test - CtsAppTestCases -->
+ <permission name="android.permission.KILL_UID" />
</privapp-permissions>
<privapp-permissions package="com.android.statementservice">
diff --git a/libs/WindowManager/Shell/res/color/open_by_default_settings_dialog_radio_button_color.xml b/libs/WindowManager/Shell/res/color/open_by_default_settings_dialog_radio_button_color.xml
new file mode 100644
index 0000000..0f9b28a
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/open_by_default_settings_dialog_radio_button_color.xml
@@ -0,0 +1,22 @@
+<?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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:state_checked="true"
+ android:color="?androidprv:attr/materialColorPrimaryContainer"/>
+ <item android:color="?androidprv:attr/materialColorSurfaceContainer"/>
+</selector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_open_by_default_settings.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_open_by_default_settings.xml
new file mode 100644
index 0000000..4070c3d
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_open_by_default_settings.xml
@@ -0,0 +1,25 @@
+<?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="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path android:fillColor="@android:color/black"
+ android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_background.xml b/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_background.xml
new file mode 100644
index 0000000..4eb2271
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_background.xml
@@ -0,0 +1,22 @@
+<?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.
+ -->
+
+<shape android:shape="rectangle"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <solid android:color="?androidprv:attr/materialColorSurfaceContainer"/>
+ <corners android:radius="28dp"/>
+</shape>
diff --git a/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_dismiss_button_background.xml b/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_dismiss_button_background.xml
new file mode 100644
index 0000000..2b2e9df
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_dismiss_button_background.xml
@@ -0,0 +1,22 @@
+<?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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <solid android:color="?androidprv:attr/materialColorPrimary"/>
+ <corners android:radius="50dp"/>
+</shape>
diff --git a/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_radio_buttons_background.xml b/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_radio_buttons_background.xml
new file mode 100644
index 0000000..1ac952b
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_radio_buttons_background.xml
@@ -0,0 +1,21 @@
+<?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.
+ -->
+
+<shape android:shape="rectangle"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/open_by_default_settings_dialog_radio_button_color"/>
+ <corners android:radius="16dp"/>
+</shape>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index 6913e54..aeb734e 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -165,17 +165,28 @@
android:layout_height="@dimen/desktop_mode_handle_menu_open_in_browser_pill_height"
android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin"
android:layout_marginStart="1dp"
- android:orientation="vertical"
+ android:orientation="horizontal"
android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation"
android:background="@drawable/desktop_mode_decor_handle_menu_background">
<Button
android:id="@+id/open_in_browser_button"
+ android:layout_weight="1"
android:contentDescription="@string/open_in_browser_text"
android:text="@string/open_in_browser_text"
android:drawableStart="@drawable/desktop_mode_ic_handle_menu_open_in_browser"
android:drawableTint="?androidprv:attr/materialColorOnSurface"
style="@style/DesktopModeHandleMenuActionButton"/>
+
+ <ImageButton
+ android:id="@+id/open_by_default_button"
+ android:layout_width="20dp"
+ android:layout_height="20dp"
+ android:layout_gravity="end|center_vertical"
+ android:layout_marginEnd="16dp"
+ android:contentDescription="@string/open_by_default_settings_text"
+ android:src="@drawable/desktop_mode_ic_handle_menu_open_by_default_settings"
+ android:tint="?androidprv:attr/materialColorOnSurface"/>
</LinearLayout>
</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml b/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml
new file mode 100644
index 0000000..8ff382b
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml
@@ -0,0 +1,130 @@
+<?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.
+ -->
+
+<com.android.wm.shell.apptoweb.OpenByDefaultDialogView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/LetterboxDialog">
+
+ <!-- The background of the top-level layout acts as the background dim. -->
+ <FrameLayout
+ android:id="@+id/open_by_default_dialog_container"
+ android:layout_width="@dimen/open_by_default_settings_dialog_width"
+ android:layout_height="wrap_content"
+ android:background="@drawable/open_by_default_settings_dialog_background"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent">
+
+ <!-- The ScrollView should only wrap the content of the dialog, otherwise the background
+ corner radius will be cut off when scrolling to the top/bottom. -->
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:padding="24dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/application_icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_gravity="center_horizontal"
+ android:importantForAccessibility="no"
+ android:layout_marginTop="24dp"
+ android:layout_marginBottom="16dp"
+ android:scaleType="centerCrop"/>
+
+ <TextView
+ android:id="@+id/application_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginBottom="8dp"
+ android:lineHeight="32dp"
+ android:textFontWeight="400"
+ android:textSize="24sp"
+ android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
+ tools:text="Gmail" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:textFontWeight="400"
+ android:lineHeight="16dp"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginBottom="16dp"
+ android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
+ android:text="@string/open_by_default_dialog_subheader_text"/>
+
+ <RadioGroup
+ android:id="@+id/open_by_default_radio_group"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal">
+ <RadioButton
+ android:id="@+id/open_in_app_button"
+ android:layout_width="@dimen/open_by_default_settings_dialog_radio_button_width"
+ android:layout_height="@dimen/open_by_default_settings_dialog_radio_button_height"
+ android:paddingStart="20dp"
+ android:paddingEnd="0dp"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginBottom="4dp"
+ android:text="@string/open_by_default_dialog_in_app_text"
+ android:textFontWeight="500"
+ android:textSize="16sp"
+ android:lineHeight="24dp"
+ android:background="@drawable/open_by_default_settings_dialog_radio_buttons_background"/>
+ <RadioButton
+ android:id="@+id/open_in_browser_button"
+ android:layout_width="@dimen/open_by_default_settings_dialog_radio_button_width"
+ android:layout_height="@dimen/open_by_default_settings_dialog_radio_button_height"
+ android:paddingStart="20dp"
+ android:paddingEnd="0dp"
+ android:layout_marginStart="16dp"
+ android:text="@string/open_by_default_dialog_in_browser_text"
+ android:textFontWeight="500"
+ android:textSize="16sp"
+ android:lineHeight="24dp"
+ android:background="@drawable/open_by_default_settings_dialog_radio_buttons_background"/>
+ </RadioGroup>
+
+ <Button
+ android:id="@+id/open_by_default_settings_dialog_dismiss_button"
+ android:layout_width="wrap_content"
+ android:layout_height="36dp"
+ android:text="@string/open_by_default_dialog_dismiss_button_text"
+ android:layout_gravity="end"
+ android:layout_marginHorizontal="24dp"
+ android:layout_marginTop="32dp"
+ android:layout_marginBottom="24dp"
+ android:textSize="14sp"
+ android:textFontWeight="500"
+ android:textColor="?androidprv:attr/materialColorOnPrimary"
+ android:background="@drawable/open_by_default_settings_dialog_dismiss_button_background"/>
+ </LinearLayout>
+ </ScrollView>
+ </FrameLayout>
+</com.android.wm.shell.apptoweb.OpenByDefaultDialogView>
+
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index c7109f5..1f15651 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -624,4 +624,12 @@
<!-- The offset from the left edge of the entering page for the cross-activity animation -->
<dimen name="cross_activity_back_entering_start_offset">96dp</dimen>
+ <!-- The open by default settings dialog menu width. -->
+ <dimen name="open_by_default_settings_dialog_width">348dp</dimen>
+ <!-- The open by default settings dialog menu height. -->
+ <dimen name="open_by_default_settings_dialog_height">380dp</dimen>
+ <!-- The height of radio buttons in the open by default settings dialog. -->
+ <dimen name="open_by_default_settings_dialog_radio_button_height">64dp</dimen>
+ <!-- The width of radio buttons in the open by default settings dialog. -->
+ <dimen name="open_by_default_settings_dialog_radio_button_width">316dp</dimen>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 56f25da..5ef8432 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -323,4 +323,15 @@
<string name="desktop_mode_maximize_menu_snap_left_button_text">Snap left</string>
<!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] -->
<string name="desktop_mode_maximize_menu_snap_right_button_text">Snap right</string>
+
+ <!-- Accessibility text for open by default settings button [CHAR LIMIT=NONE] -->
+ <string name="open_by_default_settings_text">Open by default settings</string>
+ <!-- Subheader for open by default menu string. -->
+ <string name="open_by_default_dialog_subheader_text">Choose how to open web links for this app</string>
+ <!-- Text for open by default settings dialog option. -->
+ <string name="open_by_default_dialog_in_app_text">In the app</string>
+ <!-- Text for open by default settings dialog option. -->
+ <string name="open_by_default_dialog_in_browser_text">In your browser</string>
+ <!-- Text for open by default settings dialog dismiss button. -->
+ <string name="open_by_default_dialog_dismiss_button_text">OK</string>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
new file mode 100644
index 0000000..4926cbd
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.wm.shell.apptoweb
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.TaskInfo
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.PixelFormat
+import android.view.LayoutInflater
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
+import android.view.WindowlessWindowManager
+import android.widget.ImageView
+import android.widget.TextView
+import android.window.TaskConstants
+import com.android.wm.shell.R
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer
+import java.util.function.Supplier
+
+
+/**
+ * Window manager for the open by default settings dialog
+ */
+internal class OpenByDefaultDialog(
+ private val context: Context,
+ private val taskInfo: TaskInfo,
+ private val taskSurface: SurfaceControl,
+ private val displayController: DisplayController,
+ private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>,
+ private val listener: DialogLifecycleListener,
+ appIconBitmap: Bitmap?,
+ appName: CharSequence?
+) {
+ private lateinit var dialog: OpenByDefaultDialogView
+ private lateinit var viewHost: SurfaceControlViewHost
+ private lateinit var dialogSurfaceControl: SurfaceControl
+ private var dialogContainer: AdditionalViewHostViewContainer? = null
+ private lateinit var appIconView: ImageView
+ private lateinit var appNameView: TextView
+
+ init {
+ createDialog()
+ bindAppInfo(appIconBitmap, appName)
+ }
+
+ /** Creates an open by default settings dialog. */
+ fun createDialog() {
+ val t = SurfaceControl.Transaction()
+ val taskBounds = taskInfo.configuration.windowConfiguration.bounds
+
+ dialog = LayoutInflater.from(context)
+ .inflate(
+ R.layout.open_by_default_settings_dialog,
+ null /* root */
+ ) as OpenByDefaultDialogView
+ appIconView = dialog.requireViewById(R.id.application_icon)
+ appNameView = dialog.requireViewById(R.id.application_name)
+
+ val display = displayController.getDisplay(taskInfo.displayId)
+ val builder: SurfaceControl.Builder = SurfaceControl.Builder()
+ dialogSurfaceControl = builder
+ .setName("Open by Default Dialog of Task=" + taskInfo.taskId)
+ .setContainerLayer()
+ .setParent(taskSurface)
+ .setCallsite("OpenByDefaultDialog#createDialog")
+ .build()
+ t.setPosition(dialogSurfaceControl, 0f, 0f)
+ .setWindowCrop(dialogSurfaceControl, taskBounds.width(), taskBounds.height())
+ .setLayer(dialogSurfaceControl, TaskConstants.TASK_CHILD_LAYER_SETTINGS_DIALOG)
+ .show(dialogSurfaceControl)
+ val lp = WindowManager.LayoutParams(
+ taskBounds.width(),
+ taskBounds.height(),
+ TYPE_APPLICATION_PANEL,
+ FLAG_NOT_FOCUSABLE or FLAG_NOT_TOUCH_MODAL,
+ PixelFormat.TRANSLUCENT)
+ lp.title = "Open by default settings dialog of task=" + taskInfo.taskId
+ lp.setTrustedOverlay()
+ val windowManager = WindowlessWindowManager(
+ taskInfo.configuration,
+ dialogSurfaceControl, null /* hostInputToken */
+ )
+ viewHost = SurfaceControlViewHost(context, display, windowManager, "Dialog").apply {
+ setView(dialog, lp)
+ rootSurfaceControl.applyTransactionOnDraw(t)
+ }
+ dialogContainer = AdditionalViewHostViewContainer(
+ dialogSurfaceControl, viewHost, surfaceControlTransactionSupplier)
+
+ dialog.setDismissOnClickListener{
+ closeMenu()
+ }
+
+ listener.onDialogCreated()
+ }
+
+ private fun closeMenu() {
+ dialogContainer?.releaseView()
+ dialogContainer = null
+ listener.onDialogDismissed()
+ }
+
+ private fun bindAppInfo(
+ appIconBitmap: Bitmap?,
+ appName: CharSequence?
+ ) {
+ appIconView.setImageBitmap(appIconBitmap)
+ appNameView.text = appName
+ }
+
+ /**
+ * Relayout the dialog to the new task bounds.
+ */
+ fun relayout(
+ taskInfo: RunningTaskInfo,
+ ) {
+ val t = surfaceControlTransactionSupplier.get()
+ val taskBounds = taskInfo.configuration.windowConfiguration.bounds
+ t.setWindowCrop(dialogSurfaceControl, taskBounds.width(), taskBounds.height())
+ viewHost.rootSurfaceControl.applyTransactionOnDraw(t)
+ viewHost.relayout(taskBounds.width(), taskBounds.height())
+ }
+
+ /**
+ * Defines interface for classes that can listen to lifecycle events of open by default settings
+ * dialog.
+ */
+ interface DialogLifecycleListener {
+ /** Called when open by default dialog view has been created. */
+ fun onDialogCreated()
+
+ /** Called when open by default dialog view has been released. */
+ fun onDialogDismissed()
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt
new file mode 100644
index 0000000..d03a38e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.wm.shell.apptoweb
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.View
+import android.widget.Button
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.android.wm.shell.R
+
+/** View for open by default settings dialog for an application which allows the user to change
+ * where links will open by default, in the default browser or in the application. */
+class OpenByDefaultDialogView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
+
+ private lateinit var dialogContainer: View
+ private lateinit var backgroundDim: Drawable
+
+ fun setDismissOnClickListener(callback: (View) -> Unit) {
+ val dismissButton = dialogContainer.requireViewById<Button>(
+ R.id.open_by_default_settings_dialog_dismiss_button)
+ dismissButton.setOnClickListener(callback)
+ // Clicks on the background dim should also dismiss the dialog.
+ setOnClickListener(callback)
+ // We add a no-op on-click listener to the dialog container so that clicks on it won't
+ // propagate to the listener of the layout (which represents the background dim).
+ dialogContainer.setOnClickListener { }
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ dialogContainer = requireViewById(R.id.open_by_default_dialog_container)
+ backgroundDim = background.mutate()
+ backgroundDim.alpha = 128
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 03b7c8b..a8a8c2a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -274,7 +274,8 @@
private final DragAndDropController mDragAndDropController;
/** Used to send bubble events to launcher. */
private Bubbles.BubbleStateListener mBubbleStateListener;
-
+ /** Used to track previous navigation mode to detect switch to buttons navigation. */
+ private boolean mIsPrevNavModeGestures;
/** Used to send updates to the views from {@link #mBubbleDataListener}. */
private BubbleViewCallback mBubbleViewCallback;
@@ -356,6 +357,7 @@
}
};
mExpandedViewManager = BubbleExpandedViewManager.fromBubbleController(this);
+ mIsPrevNavModeGestures = ContextUtils.isGestureNavigationMode(mContext);
}
private void registerOneHandedState(OneHandedController oneHanded) {
@@ -589,6 +591,13 @@
*/
private void sendInitialListenerUpdate() {
if (mBubbleStateListener != null) {
+ boolean isCurrentNavModeGestures = ContextUtils.isGestureNavigationMode(mContext);
+ if (mIsPrevNavModeGestures && !isCurrentNavModeGestures) {
+ BubbleBarLocation navButtonsLocation = ContextUtils.isRtl(mContext)
+ ? BubbleBarLocation.RIGHT : BubbleBarLocation.LEFT;
+ mBubblePositioner.setBubbleBarLocation(navButtonsLocation);
+ }
+ mIsPrevNavModeGestures = isCurrentNavModeGestures;
BubbleBarUpdate update = mBubbleData.getInitialStateForBubbleBar();
mBubbleStateListener.onBubbleStateChange(update);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 2795881..35a0d07 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -61,7 +61,6 @@
import android.view.ViewPropertyAnimator;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
-import android.view.WindowManagerPolicyConstants;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.FrameLayout;
@@ -91,10 +90,10 @@
import com.android.wm.shell.bubbles.animation.StackAnimationController;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.shared.bubbles.DismissView;
-import com.android.wm.shell.shared.bubbles.RelativeTouchListener;
import com.android.wm.shell.shared.animation.Interpolators;
import com.android.wm.shell.shared.animation.PhysicsAnimator;
+import com.android.wm.shell.shared.bubbles.DismissView;
+import com.android.wm.shell.shared.bubbles.RelativeTouchListener;
import com.android.wm.shell.shared.magnetictarget.MagnetizedObject;
import java.io.PrintWriter;
@@ -2276,7 +2275,7 @@
void startMonitoringSwipeUpGesture() {
stopMonitoringSwipeUpGestureInternal();
- if (isGestureNavEnabled()) {
+ if (ContextUtils.isGestureNavigationMode(mContext)) {
mBubblesNavBarGestureTracker = new BubblesNavBarGestureTracker(mContext, mPositioner);
mBubblesNavBarGestureTracker.start(mSwipeUpListener);
setOnTouchListener(mContainerSwipeListener);
@@ -2311,12 +2310,6 @@
}
}
- private boolean isGestureNavEnabled() {
- return mContext.getResources().getInteger(
- com.android.internal.R.integer.config_navBarInteractionMode)
- == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
- }
-
/**
* Stop monitoring for swipe up gesture
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ContextUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ContextUtils.kt
new file mode 100644
index 0000000..0b36f45
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ContextUtils.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.wm.shell.bubbles
+
+import android.content.Context
+import android.view.View
+import android.view.WindowManagerPolicyConstants
+import com.android.internal.R
+
+/** Simplifies accessing context fields. */
+object ContextUtils {
+
+ /** Gets navigation mode. */
+ @JvmStatic
+ val Context.navigationMode: Int
+ get() = resources.getInteger(R.integer.config_navBarInteractionMode)
+
+ /** Returns whether the navigation mode is gestures. */
+ @JvmStatic
+ val Context.isGestureNavigationMode: Boolean
+ get() = navigationMode == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL
+
+ /** Returns whether layout direction is rtl. */
+ @JvmStatic
+ val Context.isRtl: Boolean
+ get() = resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 0047ec5..38087c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -157,6 +157,14 @@
}
}
+ private void dispatchImeRequested(int displayId, boolean isRequested) {
+ synchronized (mPositionProcessors) {
+ for (ImePositionProcessor pp : mPositionProcessors) {
+ pp.onImeRequested(displayId, isRequested);
+ }
+ }
+ }
+
@ImePositionProcessor.ImeAnimationFlags
private int dispatchStartPositioning(int displayId, int hiddenTop, int shownTop,
boolean show, boolean isFloating, SurfaceControl.Transaction t) {
@@ -398,6 +406,8 @@
public void setImeInputTargetRequestedVisibility(boolean visible) {
if (android.view.inputmethod.Flags.refactorInsetsController()) {
mImeRequestedVisible = visible;
+ dispatchImeRequested(mDisplayId, mImeRequestedVisible);
+
// In the case that the IME becomes visible, but we have the control with leash
// already (e.g., when focussing an editText in activity B, while and editText in
// activity A is focussed), we will not get a call of #insetsControlChanged, and
@@ -446,6 +456,8 @@
if (imeSource == null || mImeSourceControl == null) {
return;
}
+ // TODO(b/353463205): For hide: this still has the statsToken from the previous show
+ // request
final var statsToken = mImeSourceControl.getImeStatsToken();
startAnimation(show, forceRestart, statsToken);
@@ -706,6 +718,14 @@
}
/**
+ * Called when the IME was requested by an app
+ *
+ * @param isRequested {@code true} if the IME was requested to be visible
+ */
+ default void onImeRequested(int displayId, boolean isRequested) {
+ }
+
+ /**
* Called when the IME position is starting to animate.
*
* @param hiddenTop The y position of the top of the IME surface when it is hidden.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 4b55fd0..83ffaf4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -1106,6 +1106,11 @@
default void onDoubleTappedDivider() {
}
+ /**
+ * Sets the excludedInsetsTypes for the IME in the root WindowContainer.
+ */
+ void setExcludeImeInsets(boolean exclude);
+
/** Returns split position of the token. */
@SplitPosition
int getSplitItemPosition(WindowContainerToken token);
@@ -1305,6 +1310,14 @@
}
@Override
+ public void onImeRequested(int displayId, boolean isRequested) {
+ if (displayId != mDisplayId) return;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "IME was set to requested=%s",
+ isRequested);
+ mSplitLayoutHandler.setExcludeImeInsets(true);
+ }
+
+ @Override
public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
boolean showing, boolean isFloating, SurfaceControl.Transaction t) {
if (displayId != mDisplayId || !mInitialized) {
@@ -1356,6 +1369,12 @@
setDividerInteractive(!mImeShown || !mHasImeFocus || isFloating, true,
"onImeStartPositioning");
+ if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mImeShown) {
+ mSplitLayoutHandler.setExcludeImeInsets(false);
+ }
+ }
+
return mTargetYOffset != mLastYOffset ? IME_ANIMATION_NO_ALPHA : 0;
}
@@ -1374,6 +1393,16 @@
"Split IME animation ending, canceled=%b", cancel);
onProgress(1.0f);
mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
+ if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (!mImeShown) {
+ // The IME hide animation is started immediately and at that point, the IME
+ // insets are not yet set to hidden. Therefore only resetting the
+ // excludedTypes at the end of the animation. Note: InsetsPolicy will only
+ // set the IME height to zero, when it is visible. When it becomes invisible,
+ // we dispatch the insets (the height there is zero as well)
+ mSplitLayoutHandler.setExcludeImeInsets(false);
+ }
+ }
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 4227a6e..2a5a519 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -87,7 +87,7 @@
import com.android.wm.shell.compatui.impl.DefaultCompatUIRepository;
import com.android.wm.shell.compatui.impl.DefaultComponentIdGenerator;
import com.android.wm.shell.desktopmode.DesktopMode;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
@@ -267,7 +267,7 @@
Lazy<CompatUIShellCommandHandler> compatUIShellCommandHandler,
Lazy<AccessibilityManager> accessibilityManager,
CompatUIRepository compatUIRepository,
- Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ Optional<DesktopRepository> desktopRepository,
@NonNull CompatUIState compatUIState,
@NonNull CompatUIComponentIdGenerator componentIdGenerator,
@NonNull CompatUIComponentFactory compatUIComponentFactory,
@@ -281,7 +281,7 @@
componentIdGenerator, compatUIComponentFactory, mainExecutor));
}
final IntPredicate inDesktopModePredicate =
- desktopModeTaskRepository.<IntPredicate>map(modeTaskRepository -> displayId ->
+ desktopRepository.<IntPredicate>map(modeTaskRepository -> displayId ->
modeTaskRepository.getVisibleTaskCount(displayId) > 0)
.orElseGet(() -> displayId -> false);
return Optional.of(
@@ -707,14 +707,14 @@
ShellCommandHandler shellCommandHandler,
TaskStackListenerImpl taskStackListener,
ActivityTaskManager activityTaskManager,
- Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ Optional<DesktopRepository> desktopRepository,
TaskStackTransitionObserver taskStackTransitionObserver,
@ShellMainThread ShellExecutor mainExecutor
) {
return Optional.ofNullable(
RecentTasksController.create(context, shellInit, shellController,
shellCommandHandler, taskStackListener, activityTaskManager,
- desktopModeTaskRepository, taskStackTransitionObserver, mainExecutor));
+ desktopRepository, taskStackTransitionObserver, mainExecutor));
}
@BindsOptionalOf
@@ -1003,16 +1003,16 @@
@BindsOptionalOf
@DynamicOverride
- abstract DesktopModeTaskRepository optionalDesktopModeTaskRepository();
+ abstract DesktopRepository optionalDesktopRepository();
@WMSingleton
@Provides
- static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository(Context context,
- @DynamicOverride Optional<Lazy<DesktopModeTaskRepository>> desktopModeTaskRepository) {
+ static Optional<DesktopRepository> provideDesktopRepository(Context context,
+ @DynamicOverride Optional<Lazy<DesktopRepository>> desktopRepository) {
// Use optional-of-lazy for the dependency that this provider relies on.
// Lazy ensures that this provider will not be the cause the dependency is created
// when it will not be returned due to the condition below.
- return desktopModeTaskRepository.flatMap((lazy) -> {
+ return desktopRepository.flatMap((lazy) -> {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.of(lazy.get());
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 7293956..b7d9677 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -65,11 +65,12 @@
import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
+import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver;
@@ -245,7 +246,7 @@
IWindowManager windowManager,
ShellCommandHandler shellCommandHandler,
ShellTaskOrganizer taskOrganizer,
- @DynamicOverride DesktopModeTaskRepository desktopRepository,
+ @DynamicOverride DesktopRepository desktopRepository,
DisplayController displayController,
ShellController shellController,
DisplayInsetsController displayInsetsController,
@@ -352,7 +353,7 @@
Context context,
ShellInit shellInit,
ShellTaskOrganizer shellTaskOrganizer,
- Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ Optional<DesktopRepository> desktopRepository,
LaunchAdjacentController launchAdjacentController,
WindowDecorViewModel windowDecorViewModel) {
// TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
@@ -361,7 +362,7 @@
? shellInit
: null;
return new FreeformTaskListener(context, init, shellTaskOrganizer,
- desktopModeTaskRepository, launchAdjacentController, windowDecorViewModel);
+ desktopRepository, launchAdjacentController, windowDecorViewModel);
}
@WMSingleton
@@ -384,9 +385,11 @@
Context context,
ShellInit shellInit,
Transitions transitions,
+ Optional<DesktopFullImmersiveTransitionHandler> desktopImmersiveTransitionHandler,
WindowDecorViewModel windowDecorViewModel) {
return new FreeformTaskTransitionObserver(
- context, shellInit, transitions, windowDecorViewModel);
+ context, shellInit, transitions, desktopImmersiveTransitionHandler,
+ windowDecorViewModel);
}
@WMSingleton
@@ -620,7 +623,8 @@
DesktopModeDragAndDropTransitionHandler desktopModeDragAndDropTransitionHandler,
ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
DragToDesktopTransitionHandler dragToDesktopTransitionHandler,
- @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
+ @DynamicOverride DesktopRepository desktopRepository,
+ Optional<DesktopFullImmersiveTransitionHandler> desktopFullImmersiveTransitionHandler,
DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
LaunchAdjacentController launchAdjacentController,
RecentsTransitionHandler recentsTransitionHandler,
@@ -636,7 +640,8 @@
returnToDragStartAnimator, enterDesktopTransitionHandler,
exitDesktopTransitionHandler, desktopModeDragAndDropTransitionHandler,
toggleResizeDesktopTaskTransitionHandler,
- dragToDesktopTransitionHandler, desktopModeTaskRepository,
+ dragToDesktopTransitionHandler, desktopFullImmersiveTransitionHandler.get(),
+ desktopRepository,
desktopModeLoggerTransitionObserver, launchAdjacentController,
recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter,
recentTasksController.orElse(null), interactionJankMonitor, mainHandler);
@@ -647,7 +652,7 @@
static Optional<DesktopTasksLimiter> provideDesktopTasksLimiter(
Context context,
Transitions transitions,
- @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
+ @DynamicOverride DesktopRepository desktopRepository,
ShellTaskOrganizer shellTaskOrganizer,
InteractionJankMonitor interactionJankMonitor,
@ShellMainThread Handler handler) {
@@ -660,7 +665,7 @@
return Optional.of(
new DesktopTasksLimiter(
transitions,
- desktopModeTaskRepository,
+ desktopRepository,
shellTaskOrganizer,
maxTaskLimit,
interactionJankMonitor,
@@ -671,6 +676,19 @@
@WMSingleton
@Provides
+ static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler(
+ Context context,
+ Transitions transitions,
+ @DynamicOverride DesktopRepository desktopRepository) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
+ return Optional.of(
+ new DesktopFullImmersiveTransitionHandler(transitions, desktopRepository));
+ }
+ return Optional.empty();
+ }
+
+ @WMSingleton
+ @Provides
static ReturnToDragStartAnimator provideReturnToDragStartAnimator(
Context context, InteractionJankMonitor interactionJankMonitor) {
return new ReturnToDragStartAnimator(context, interactionJankMonitor);
@@ -739,13 +757,14 @@
@WMSingleton
@Provides
@DynamicOverride
- static DesktopModeTaskRepository provideDesktopModeTaskRepository(
+
+ static DesktopRepository provideDesktopRepository(
Context context,
ShellInit shellInit,
DesktopPersistentRepository desktopPersistentRepository,
@ShellMainThread CoroutineScope mainScope
) {
- return new DesktopModeTaskRepository(context, shellInit, desktopPersistentRepository,
+ return new DesktopRepository(context, shellInit, desktopPersistentRepository,
mainScope);
}
@@ -757,12 +776,12 @@
ShellTaskOrganizer shellTaskOrganizer,
TaskStackListenerImpl taskStackListener,
ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
- @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository
+ @DynamicOverride DesktopRepository desktopRepository
) {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.of(new DesktopActivityOrientationChangeHandler(
context, shellInit, shellTaskOrganizer, taskStackListener,
- toggleResizeDesktopTaskTransitionHandler, desktopModeTaskRepository));
+ toggleResizeDesktopTaskTransitionHandler, desktopRepository));
}
return Optional.empty();
}
@@ -771,12 +790,12 @@
@Provides
static Optional<DesktopTasksTransitionObserver> provideDesktopTasksTransitionObserver(
Context context,
- Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ Optional<DesktopRepository> desktopRepository,
Transitions transitions,
ShellTaskOrganizer shellTaskOrganizer,
ShellInit shellInit
) {
- return desktopModeTaskRepository.flatMap(repository ->
+ return desktopRepository.flatMap(repository ->
Optional.of(new DesktopTasksTransitionObserver(
context, repository, transitions, shellTaskOrganizer, shellInit))
);
@@ -787,7 +806,7 @@
static Optional<DesktopMixedTransitionHandler> provideDesktopMixedTransitionHandler(
Context context,
Transitions transitions,
- @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
+ @DynamicOverride DesktopRepository desktopRepository,
FreeformTaskTransitionHandler freeformTaskTransitionHandler,
CloseDesktopTaskTransitionHandler closeDesktopTaskTransitionHandler,
InteractionJankMonitor interactionJankMonitor,
@@ -801,7 +820,7 @@
new DesktopMixedTransitionHandler(
context,
transitions,
- desktopModeTaskRepository,
+ desktopRepository,
freeformTaskTransitionHandler,
closeDesktopTaskTransitionHandler,
interactionJankMonitor,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 3464fef..702552e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -77,10 +77,12 @@
PipTaskListener pipTaskListener,
@NonNull PipScheduler pipScheduler,
@NonNull PipTransitionState pipStackListenerController,
+ @NonNull PipDisplayLayoutState pipDisplayLayoutState,
@NonNull PipUiStateChangeController pipUiStateChangeController) {
return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener,
- pipScheduler, pipStackListenerController, pipUiStateChangeController);
+ pipScheduler, pipStackListenerController, pipDisplayLayoutState,
+ pipUiStateChangeController);
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt
index 59e0068..606aa6c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt
@@ -39,7 +39,7 @@
private val shellTaskOrganizer: ShellTaskOrganizer,
private val taskStackListener: TaskStackListenerImpl,
private val resizeHandler: ToggleResizeDesktopTaskTransitionHandler,
- private val taskRepository: DesktopModeTaskRepository,
+ private val taskRepository: DesktopRepository,
) {
init {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
new file mode 100644
index 0000000..f749aa1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
@@ -0,0 +1,245 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.animation.RectEvaluator
+import android.animation.ValueAnimator
+import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.Rect
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.animation.DecelerateInterpolator
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import androidx.core.animation.addListener
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TransitionHandler
+import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
+
+/**
+ * A [TransitionHandler] to move a task in/out of desktop's full immersive state where the task
+ * remains freeform while being able to take fullscreen bounds and have its App Header visibility
+ * be transient below the status bar like in fullscreen immersive mode.
+ */
+class DesktopFullImmersiveTransitionHandler(
+ private val transitions: Transitions,
+ private val desktopRepository: DesktopRepository,
+ private val transactionSupplier: () -> SurfaceControl.Transaction,
+) : TransitionHandler {
+
+ constructor(
+ transitions: Transitions,
+ desktopRepository: DesktopRepository,
+ ) : this(transitions, desktopRepository, { SurfaceControl.Transaction() })
+
+ private var state: TransitionState? = null
+
+ /** Whether there is an immersive transition that hasn't completed yet. */
+ private val inProgress: Boolean
+ get() = state != null
+
+ private val rectEvaluator = RectEvaluator()
+
+ /** A listener to invoke on animation changes during entry/exit. */
+ var onTaskResizeAnimationListener: OnTaskResizeAnimationListener? = null
+
+ /** Starts a transition to enter full immersive state inside the desktop. */
+ fun enterImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
+ if (inProgress) {
+ ProtoLog.v(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "FullImmersive: cannot start entry because transition already in progress."
+ )
+ return
+ }
+
+ val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
+ state = TransitionState(
+ transition = transition,
+ displayId = taskInfo.displayId,
+ taskId = taskInfo.taskId,
+ direction = Direction.ENTER
+ )
+ }
+
+ fun exitImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
+ if (inProgress) {
+ ProtoLog.v(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "$TAG: cannot start exit because transition already in progress."
+ )
+ return
+ }
+
+ val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
+ state = TransitionState(
+ transition = transition,
+ displayId = taskInfo.displayId,
+ taskId = taskInfo.taskId,
+ direction = Direction.EXIT
+ )
+ }
+
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback
+ ): Boolean {
+ val state = requireState()
+ if (transition != state.transition) return false
+ animateResize(
+ transitionState = state,
+ info = info,
+ startTransaction = startTransaction,
+ finishTransaction = finishTransaction,
+ finishCallback = finishCallback
+ )
+ return true
+ }
+
+ private fun animateResize(
+ transitionState: TransitionState,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback
+ ) {
+ val change = info.changes.first { c ->
+ val taskInfo = c.taskInfo
+ return@first taskInfo != null && taskInfo.taskId == transitionState.taskId
+ }
+ val leash = change.leash
+ val startBounds = change.startAbsBounds
+ val endBounds = change.endAbsBounds
+
+ val updateTransaction = transactionSupplier()
+ ValueAnimator.ofObject(rectEvaluator, startBounds, endBounds).apply {
+ duration = FULL_IMMERSIVE_ANIM_DURATION_MS
+ interpolator = DecelerateInterpolator()
+ addListener(
+ onStart = {
+ startTransaction
+ .setPosition(leash, startBounds.left.toFloat(), startBounds.top.toFloat())
+ .setWindowCrop(leash, startBounds.width(), startBounds.height())
+ .show(leash)
+ onTaskResizeAnimationListener
+ ?.onAnimationStart(transitionState.taskId, startTransaction, startBounds)
+ ?: startTransaction.apply()
+ },
+ onEnd = {
+ finishTransaction
+ .setPosition(leash, endBounds.left.toFloat(), endBounds.top.toFloat())
+ .setWindowCrop(leash, endBounds.width(), endBounds.height())
+ .apply()
+ onTaskResizeAnimationListener?.onAnimationEnd(transitionState.taskId)
+ finishCallback.onTransitionFinished(null /* wct */)
+ clearState()
+ }
+ )
+ addUpdateListener { animation ->
+ val rect = animation.animatedValue as Rect
+ updateTransaction
+ .setPosition(leash, rect.left.toFloat(), rect.top.toFloat())
+ .setWindowCrop(leash, rect.width(), rect.height())
+ .apply()
+ onTaskResizeAnimationListener
+ ?.onBoundsChange(transitionState.taskId, updateTransaction, rect)
+ ?: updateTransaction.apply()
+ }
+ start()
+ }
+ }
+
+ override fun handleRequest(
+ transition: IBinder,
+ request: TransitionRequestInfo
+ ): WindowContainerTransaction? = null
+
+ override fun onTransitionConsumed(
+ transition: IBinder,
+ aborted: Boolean,
+ finishTransaction: SurfaceControl.Transaction?
+ ) {
+ val state = this.state ?: return
+ if (transition == state.transition && aborted) {
+ clearState()
+ }
+ super.onTransitionConsumed(transition, aborted, finishTransaction)
+ }
+
+ /**
+ * Called when any transition in the system is ready to play. This is needed to update the
+ * repository state before window decorations are drawn (which happens immediately after
+ * |onTransitionReady|, before this transition actually animates) because drawing decorations
+ * depends in whether the task is in full immersive state or not.
+ */
+ fun onTransitionReady(transition: IBinder) {
+ val state = this.state ?: return
+ // TODO: b/369443668 - this assumes invoking the exit transition is the only way to exit
+ // immersive, which isn't realistic. The app could crash, the user could dismiss it from
+ // overview, etc. This (or its caller) should search all transitions to look for any
+ // immersive task exiting that state to keep the repository properly updated.
+ if (transition == state.transition) {
+ when (state.direction) {
+ Direction.ENTER -> {
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = state.displayId,
+ taskId = state.taskId,
+ immersive = true
+ )
+ }
+ Direction.EXIT -> {
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = state.displayId,
+ taskId = state.taskId,
+ immersive = false
+ )
+ }
+ }
+ }
+ }
+
+ private fun clearState() {
+ state = null
+ }
+
+ private fun requireState(): TransitionState =
+ state ?: error("Expected non-null transition state")
+
+ /** The state of the currently running transition. */
+ private data class TransitionState(
+ val transition: IBinder,
+ val displayId: Int,
+ val taskId: Int,
+ val direction: Direction
+ )
+
+ private enum class Direction {
+ ENTER, EXIT
+ }
+
+ private companion object {
+ private const val TAG = "FullImmersiveHandler"
+
+ private const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index ec3f8c5..4350199 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -40,7 +40,7 @@
class DesktopMixedTransitionHandler(
private val context: Context,
private val transitions: Transitions,
- private val desktopTaskRepository: DesktopModeTaskRepository,
+ private val desktopRepository: DesktopRepository,
private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler,
private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler,
private val interactionJankMonitor: InteractionJankMonitor,
@@ -138,7 +138,7 @@
private fun isLastDesktopTask(change: TransitionInfo.Change): Boolean =
change.taskInfo?.let {
- desktopTaskRepository.getActiveNonMinimizedTaskCount(it.displayId) == 1
+ desktopRepository.getActiveNonMinimizedTaskCount(it.displayId) == 1
} ?: false
private fun findCloseDesktopTaskChange(info: TransitionInfo): TransitionInfo.Change? {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index cca7500..73e55b2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -36,7 +36,7 @@
* @param listener the listener to add.
* @param callbackExecutor the executor to call the listener on.
*/
- void addVisibleTasksListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+ void addVisibleTasksListener(DesktopRepository.VisibleTasksListener listener,
Executor callbackExecutor);
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index 7261919..52b92a8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -21,6 +21,12 @@
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR;
+import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR;
+import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR;
+import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR;
+import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.RectEvaluator;
@@ -125,7 +131,7 @@
mContext = context;
mTaskSurface = taskSurface;
mRootTdaOrganizer = taskDisplayAreaOrganizer;
- mCurrentType = IndicatorType.NO_INDICATOR;
+ mCurrentType = NO_INDICATOR;
mDragStartState = dragStartState;
}
@@ -136,8 +142,16 @@
@NonNull
IndicatorType updateIndicatorType(PointF inputCoordinates) {
final DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId);
+ // Perform a quick check first: any input off the left edge of the display should be split
+ // left, and split right for the right edge. This is universal across all drag event types.
+ if (inputCoordinates.x < 0) return TO_SPLIT_LEFT_INDICATOR;
+ if (inputCoordinates.x > layout.width()) return TO_SPLIT_RIGHT_INDICATOR;
// If we are in freeform, we don't want a visible indicator in the "freeform" drag zone.
- IndicatorType result = IndicatorType.NO_INDICATOR;
+ // In drags not originating on a freeform caption, we should default to a TO_DESKTOP
+ // indicator.
+ IndicatorType result = mDragStartState == DragStartState.FROM_FREEFORM
+ ? NO_INDICATOR
+ : TO_DESKTOP_INDICATOR;
final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize(
com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness);
// Because drags in freeform use task position for indicator calculation, we need to
@@ -149,10 +163,8 @@
captionHeight);
final Region splitRightRegion = calculateSplitRightRegion(layout, transitionAreaWidth,
captionHeight);
- final Region toDesktopRegion = calculateToDesktopRegion(layout, splitLeftRegion,
- splitRightRegion, fullscreenRegion);
if (fullscreenRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) {
- result = IndicatorType.TO_FULLSCREEN_INDICATOR;
+ result = TO_FULLSCREEN_INDICATOR;
}
if (splitLeftRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) {
result = IndicatorType.TO_SPLIT_LEFT_INDICATOR;
@@ -160,9 +172,6 @@
if (splitRightRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) {
result = IndicatorType.TO_SPLIT_RIGHT_INDICATOR;
}
- if (toDesktopRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) {
- result = IndicatorType.TO_DESKTOP_INDICATOR;
- }
if (mDragStartState != DragStartState.DRAGGED_INTENT) {
transitionIndicator(result);
}
@@ -182,7 +191,7 @@
R.dimen.desktop_mode_fullscreen_region_scale);
final float toFullscreenWidth = (layout.width() * toFullscreenScale);
region.union(new Rect((int) ((layout.width() / 2f) - (toFullscreenWidth / 2f)),
- -captionHeight,
+ Short.MIN_VALUE,
(int) ((layout.width() / 2f) + (toFullscreenWidth / 2f)),
transitionHeight));
}
@@ -192,7 +201,7 @@
|| mDragStartState == DragStartState.DRAGGED_INTENT
) {
region.union(new Rect(0,
- -captionHeight,
+ Short.MIN_VALUE,
layout.width(),
transitionHeight));
}
@@ -200,21 +209,6 @@
}
@VisibleForTesting
- Region calculateToDesktopRegion(DisplayLayout layout,
- Region splitLeftRegion, Region splitRightRegion,
- Region toFullscreenRegion) {
- final Region region = new Region();
- // If in desktop, we need no region. Otherwise it's the same for all windowing modes.
- if (mDragStartState != DragStartState.FROM_FREEFORM) {
- region.union(new Rect(0, 0, layout.width(), layout.height()));
- region.op(splitLeftRegion, Region.Op.DIFFERENCE);
- region.op(splitRightRegion, Region.Op.DIFFERENCE);
- region.op(toFullscreenRegion, Region.Op.DIFFERENCE);
- }
- return region;
- }
-
- @VisibleForTesting
Region calculateSplitLeftRegion(DisplayLayout layout,
int transitionEdgeWidth, int captionHeight) {
final Region region = new Region();
@@ -311,7 +305,7 @@
}
});
}
- mCurrentType = IndicatorType.NO_INDICATOR;
+ mCurrentType = NO_INDICATOR;
}
/**
@@ -322,9 +316,9 @@
if (mView == null) {
createView();
}
- if (mCurrentType == IndicatorType.NO_INDICATOR) {
+ if (mCurrentType == NO_INDICATOR) {
fadeInIndicator(newType);
- } else if (newType == IndicatorType.NO_INDICATOR) {
+ } else if (newType == NO_INDICATOR) {
fadeOutIndicator(null /* finishCallback */);
} else {
final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
similarity index 98%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
rename to libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 985224e..7b2a5d3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -42,8 +42,8 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-/** Tracks task data for Desktop Mode. */
-class DesktopModeTaskRepository (
+/** Tracks desktop data for Android Desktop Windowing. */
+class DesktopRepository (
private val context: Context,
shellInit: ShellInit,
private val persistentRepository: DesktopPersistentRepository,
@@ -467,10 +467,9 @@
}
}
-
internal fun dump(pw: PrintWriter, prefix: String) {
val innerPrefix = "$prefix "
- pw.println("${prefix}DesktopModeTaskRepository")
+ pw.println("${prefix}DesktopRepository")
dumpDesktopTaskData(pw, innerPrefix)
pw.println("${innerPrefix}activeTasksListeners=${activeTasksListeners.size}")
pw.println("${innerPrefix}visibleTasksListeners=${visibleTasksListeners.size}")
@@ -512,10 +511,9 @@
}
companion object {
- private const val TAG = "DesktopModeTaskRepository"
+ private const val TAG = "DesktopRepository"
}
}
private fun <T> Iterable<T>.toDumpString(): String =
joinToString(separator = ", ", prefix = "[", postfix = "]")
-
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index fcd2f8c..4e548a6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -73,7 +73,7 @@
import com.android.wm.shell.common.SingleInstanceRemoteListener
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener
+import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener
import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState
import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener
@@ -134,7 +134,8 @@
private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler,
private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
- private val taskRepository: DesktopModeTaskRepository,
+ private val immersiveTransitionHandler: DesktopFullImmersiveTransitionHandler,
+ private val taskRepository: DesktopRepository,
private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
private val launchAdjacentController: LaunchAdjacentController,
private val recentsTransitionHandler: RecentsTransitionHandler,
@@ -231,6 +232,7 @@
toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener
+ immersiveTransitionHandler.onTaskResizeAnimationListener = listener
}
fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) {
@@ -649,6 +651,35 @@
}
}
+ /** Moves a task in/out of full immersive state within the desktop. */
+ fun toggleDesktopTaskFullImmersiveState(taskInfo: RunningTaskInfo) {
+ if (taskRepository.isTaskInFullImmersiveState(taskInfo.taskId)) {
+ exitDesktopTaskFromFullImmersive(taskInfo)
+ } else {
+ moveDesktopTaskToFullImmersive(taskInfo)
+ }
+ }
+
+ private fun moveDesktopTaskToFullImmersive(taskInfo: RunningTaskInfo) {
+ check(taskInfo.isFreeform) { "Task must already be in freeform" }
+ val wct = WindowContainerTransaction().apply {
+ setBounds(taskInfo.token, Rect())
+ }
+ immersiveTransitionHandler.enterImmersive(taskInfo, wct)
+ }
+
+ private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) {
+ check(taskInfo.isFreeform) { "Task must already be in freeform" }
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+ val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
+ val destinationBounds = getMaximizeBounds(taskInfo, stableBounds)
+
+ val wct = WindowContainerTransaction().apply {
+ setBounds(taskInfo.token, destinationBounds)
+ }
+ immersiveTransitionHandler.exitImmersive(taskInfo, wct)
+ }
+
/**
* Quick-resizes a desktop task, toggling between a fullscreen state (represented by the stable
* bounds) and a free floating state (either the last saved bounds if available or the default
@@ -685,18 +716,7 @@
// and toggle to the stable bounds.
taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
- if (taskInfo.isResizeable) {
- // if resizable then expand to entire stable bounds (full display minus insets)
- destinationBounds.set(stableBounds)
- } else {
- // if non-resizable then calculate max bounds according to aspect ratio
- val activityAspectRatio = calculateAspectRatio(taskInfo)
- val newSize = maximizeSizeGivenAspectRatio(taskInfo,
- Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
- val newBounds = centerInArea(
- newSize, stableBounds, stableBounds.left, stableBounds.top)
- destinationBounds.set(newBounds)
- }
+ destinationBounds.set(getMaximizeBounds(taskInfo, stableBounds))
}
@@ -719,6 +739,20 @@
}
}
+ private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect {
+ if (taskInfo.isResizeable) {
+ // if resizable then expand to entire stable bounds (full display minus insets)
+ return Rect(stableBounds)
+ } else {
+ // if non-resizable then calculate max bounds according to aspect ratio
+ val activityAspectRatio = calculateAspectRatio(taskInfo)
+ val newSize = maximizeSizeGivenAspectRatio(taskInfo,
+ Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
+ return centerInArea(
+ newSize, stableBounds, stableBounds.left, stableBounds.top)
+ }
+ }
+
private fun isTaskMaximized(
taskInfo: RunningTaskInfo,
stableBounds: Rect
@@ -1594,7 +1628,7 @@
return
}
- val indicator = visualIndicator ?: return
+ val indicator = getVisualIndicator() ?: return
val indicatorType =
indicator.updateIndicatorType(
PointF(inputCoordinate.x, currentDragBounds.top.toFloat()),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index 37ad0c9..37bec21 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -44,7 +44,7 @@
*/
class DesktopTasksLimiter (
transitions: Transitions,
- private val taskRepository: DesktopModeTaskRepository,
+ private val taskRepository: DesktopRepository,
private val shellTaskOrganizer: ShellTaskOrganizer,
private val maxTasksLimit: Int,
private val interactionJankMonitor: InteractionJankMonitor,
@@ -160,7 +160,7 @@
}
@VisibleForTesting
- inner class LeftoverMinimizedTasksRemover : DesktopModeTaskRepository.ActiveTasksListener {
+ inner class LeftoverMinimizedTasksRemover : DesktopRepository.ActiveTasksListener {
override fun onActiveTasksChanged(displayId: Int) {
// If back navigation is enabled, we shouldn't remove the leftover tasks
if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) return
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index b20c9fc..e086e40 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -36,12 +36,12 @@
/**
* A [Transitions.TransitionObserver] that observes shell transitions and updates the
- * [DesktopModeTaskRepository] state TODO: b/332682201 This observes transitions related to desktop
+ * [DesktopRepository] state TODO: b/332682201 This observes transitions related to desktop
* mode and other transitions that originate both within and outside shell.
*/
class DesktopTasksTransitionObserver(
private val context: Context,
- private val desktopModeTaskRepository: DesktopModeTaskRepository,
+ private val desktopRepository: DesktopRepository,
private val transitions: Transitions,
private val shellTaskOrganizer: ShellTaskOrganizer,
shellInit: ShellInit
@@ -83,10 +83,10 @@
val taskInfo = change.taskInfo
if (taskInfo == null || taskInfo.taskId == -1) continue
- if (desktopModeTaskRepository.isActiveTask(taskInfo.taskId)
+ if (desktopRepository.isActiveTask(taskInfo.taskId)
&& taskInfo.windowingMode != WINDOWING_MODE_FREEFORM
) {
- desktopModeTaskRepository.removeFreeformTask(
+ desktopRepository.removeFreeformTask(
taskInfo.displayId,
taskInfo.taskId
)
@@ -104,11 +104,11 @@
continue
}
- if (desktopModeTaskRepository.getVisibleTaskCount(taskInfo.displayId) > 0 &&
+ if (desktopRepository.getVisibleTaskCount(taskInfo.displayId) > 0 &&
change.mode == TRANSIT_TO_BACK &&
taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
) {
- desktopModeTaskRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId)
+ desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId)
}
}
}
@@ -135,7 +135,7 @@
if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) {
when (change.mode) {
WindowManager.TRANSIT_OPEN -> {
- desktopModeTaskRepository.wallpaperActivityToken = taskInfo.token
+ desktopRepository.wallpaperActivityToken = taskInfo.token
// After the task for the wallpaper is created, set it non-trimmable.
// This is important to prevent recents from trimming and removing the
// task.
@@ -145,7 +145,7 @@
)
}
WindowManager.TRANSIT_CLOSE ->
- desktopModeTaskRepository.wallpaperActivityToken = null
+ desktopRepository.wallpaperActivityToken = null
else -> {}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 7f7f105..73f7011 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -29,7 +29,7 @@
import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.LaunchAdjacentController;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.sysui.ShellInit;
@@ -49,7 +49,7 @@
private final Context mContext;
private final ShellTaskOrganizer mShellTaskOrganizer;
- private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository;
+ private final Optional<DesktopRepository> mDesktopRepository;
private final WindowDecorViewModel mWindowDecorationViewModel;
private final LaunchAdjacentController mLaunchAdjacentController;
@@ -64,13 +64,13 @@
Context context,
ShellInit shellInit,
ShellTaskOrganizer shellTaskOrganizer,
- Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ Optional<DesktopRepository> desktopRepository,
LaunchAdjacentController launchAdjacentController,
WindowDecorViewModel windowDecorationViewModel) {
mContext = context;
mShellTaskOrganizer = shellTaskOrganizer;
mWindowDecorationViewModel = windowDecorationViewModel;
- mDesktopModeTaskRepository = desktopModeTaskRepository;
+ mDesktopRepository = desktopRepository;
mLaunchAdjacentController = launchAdjacentController;
if (shellInit != null) {
shellInit.addInitCallback(this::onInit, this);
@@ -102,7 +102,7 @@
}
if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
- mDesktopModeTaskRepository.ifPresent(repository -> {
+ mDesktopRepository.ifPresent(repository -> {
repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId);
if (taskInfo.isVisible) {
repository.addActiveTask(taskInfo.displayId, taskInfo.taskId);
@@ -121,7 +121,7 @@
mTasks.remove(taskInfo.taskId);
if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
- mDesktopModeTaskRepository.ifPresent(repository -> {
+ mDesktopRepository.ifPresent(repository -> {
// TODO: b/370038902 - Handle Activity#finishAndRemoveTask.
if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()
|| repository.isClosingTask(taskInfo.taskId)) {
@@ -150,7 +150,7 @@
mWindowDecorationViewModel.onTaskInfoChanged(taskInfo);
state.mTaskInfo = taskInfo;
if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
- mDesktopModeTaskRepository.ifPresent(repository -> {
+ mDesktopRepository.ifPresent(repository -> {
if (taskInfo.isVisible) {
repository.addActiveTask(taskInfo.displayId, taskInfo.taskId);
} else if (repository.isClosingTask(taskInfo.taskId)) {
@@ -182,7 +182,7 @@
"Freeform Task Focus Changed: #%d focused=%b",
taskInfo.taskId, taskInfo.isFocused);
if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.isFocused) {
- mDesktopModeTaskRepository.ifPresent(repository -> {
+ mDesktopRepository.ifPresent(repository -> {
repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId);
});
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index ffcc526..cb0354ec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -27,6 +27,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import com.android.window.flags.Flags;
+import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -36,6 +38,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
/**
* The {@link Transitions.TransitionHandler} that handles freeform task launches, closes,
@@ -44,6 +47,7 @@
*/
public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver {
private final Transitions mTransitions;
+ private final Optional<DesktopFullImmersiveTransitionHandler> mImmersiveTransitionHandler;
private final WindowDecorViewModel mWindowDecorViewModel;
private final Map<IBinder, List<ActivityManager.RunningTaskInfo>> mTransitionToTaskInfo =
@@ -53,8 +57,10 @@
Context context,
ShellInit shellInit,
Transitions transitions,
+ Optional<DesktopFullImmersiveTransitionHandler> immersiveTransitionHandler,
WindowDecorViewModel windowDecorViewModel) {
mTransitions = transitions;
+ mImmersiveTransitionHandler = immersiveTransitionHandler;
mWindowDecorViewModel = windowDecorViewModel;
if (Transitions.ENABLE_SHELL_TRANSITIONS && FreeformComponents.isFreeformEnabled(context)) {
shellInit.addInitCallback(this::onInit, this);
@@ -72,6 +78,13 @@
@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startT,
@NonNull SurfaceControl.Transaction finishT) {
+ if (Flags.enableFullyImmersiveInDesktop()) {
+ // TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository
+ // is updated from there **before** the |mWindowDecorViewModel| methods are invoked.
+ // Otherwise window decoration relayout won't run with the immersive state up to date.
+ mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition));
+ }
+
final ArrayList<ActivityManager.RunningTaskInfo> taskInfoList = new ArrayList<>();
final ArrayList<WindowContainerToken> taskParents = new ArrayList<>();
for (TransitionInfo.Change change : info.getChanges()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index e9c4c14..73be8db 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -324,10 +324,16 @@
int launcherRotation, Rect hotseatKeepClearArea) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"getSwipePipToHomeBounds: %s", componentName);
- // preemptively add the keep clear area for Hotseat, so that it is taken into account
- // when calculating the entry destination bounds of PiP window
+ // Preemptively add the keep clear area for Hotseat, so that it is taken into account
+ // when calculating the entry destination bounds of PiP window.
mPipBoundsState.setNamedUnrestrictedKeepClearArea(
PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, hotseatKeepClearArea);
+
+ // Set the display layout rotation early to calculate final orientation bounds that
+ // the animator expects, this will also be used to detect the fixed rotation when
+ // Shell resolves the type of the animation we are undergoing.
+ mPipDisplayLayoutState.rotateTo(launcherRotation);
+
mPipBoundsState.setBoundsStateForEntry(componentName, activityInfo, pictureInPictureParams,
mPipBoundsAlgorithm);
return mPipBoundsAlgorithm.getEntryDestinationBounds();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index dc0bc78..62a60fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -17,6 +17,7 @@
package com.android.wm.shell.pip2.phone;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.view.Surface.ROTATION_270;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_PIP;
@@ -33,6 +34,7 @@
import android.app.ActivityManager;
import android.app.PictureInPictureParams;
import android.content.Context;
+import android.graphics.Matrix;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
@@ -49,6 +51,7 @@
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.pip.PipTransitionController;
@@ -82,7 +85,7 @@
* The fixed start delay in ms when fading out the content overlay from bounds animation.
* The fadeout animation is guaranteed to start after the client has drawn under the new config.
*/
- private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 400;
+ private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 500;
//
// Dependencies
@@ -92,6 +95,7 @@
private final PipTaskListener mPipTaskListener;
private final PipScheduler mPipScheduler;
private final PipTransitionState mPipTransitionState;
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
//
// Transition caches
@@ -124,6 +128,7 @@
PipTaskListener pipTaskListener,
PipScheduler pipScheduler,
PipTransitionState pipTransitionState,
+ PipDisplayLayoutState pipDisplayLayoutState,
PipUiStateChangeController pipUiStateChangeController) {
super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
pipBoundsAlgorithm);
@@ -134,6 +139,7 @@
mPipScheduler.setPipTransitionController(this);
mPipTransitionState = pipTransitionState;
mPipTransitionState.addPipTransitionStateChangedListener(this);
+ mPipDisplayLayoutState = pipDisplayLayoutState;
}
@Override
@@ -321,11 +327,30 @@
(destinationBounds.width() - overlaySize) / 2f,
(destinationBounds.height() - overlaySize) / 2f);
}
-
startTransaction.merge(finishTransaction);
+
+ final int startRotation = pipChange.getStartRotation();
+ final int endRotation = mPipDisplayLayoutState.getRotation();
+ if (endRotation != startRotation) {
+ boolean isClockwise = (endRotation - startRotation) == -ROTATION_270;
+
+ // Display bounds were already updated to represent the final orientation,
+ // so we just need to readjust the origin, and perform rotation about (0, 0).
+ Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds();
+ int originTranslateX = isClockwise ? 0 : -displayBounds.width();
+ int originTranslateY = isClockwise ? -displayBounds.height() : 0;
+
+ Matrix transformTensor = new Matrix();
+ final float[] matrixTmp = new float[9];
+ transformTensor.setTranslate(originTranslateX + destinationBounds.left,
+ originTranslateY + destinationBounds.top);
+ final float degrees = (endRotation - startRotation) * 90f;
+ transformTensor.postRotate(degrees);
+ startTransaction.setMatrix(pipLeash, transformTensor, matrixTmp);
+ }
startTransaction.apply();
finishCallback.onTransitionFinished(null /* finishWct */);
- onClientDrawAtTransitionEnd();
+ finishInner();
return true;
}
@@ -397,7 +422,7 @@
sourceRectHint, PipEnterExitAnimator.BOUNDS_ENTER, Surface.ROTATION_0);
tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(),
- this::onClientDrawAtTransitionEnd);
+ this::finishInner);
finishWct.setBoundsChangeTransaction(pipTaskToken, tx);
animator.setAnimationEndCallback(() ->
@@ -430,7 +455,7 @@
animator.setAnimationEndCallback(() -> {
finishCallback.onTransitionFinished(null);
// This should update the pip transition state accordingly after we stop playing.
- onClientDrawAtTransitionEnd();
+ finishInner();
});
animator.start();
@@ -605,7 +630,7 @@
// Miscellaneous callbacks and listeners
//
- private void onClientDrawAtTransitionEnd() {
+ private void finishInner() {
if (mPipTransitionState.getSwipePipToHomeOverlay() != null) {
startOverlayFadeoutAnimation();
} else if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index c9c0873..95cb3df 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -54,7 +54,7 @@
import com.android.wm.shell.common.SingleInstanceRemoteListener;
import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.shared.GroupedRecentTaskInfo;
import com.android.wm.shell.shared.annotations.ExternalThread;
@@ -81,14 +81,14 @@
* Manages the recent task list from the system, caching it as necessary.
*/
public class RecentTasksController implements TaskStackListenerCallback,
- RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener,
+ RemoteCallable<RecentTasksController>, DesktopRepository.ActiveTasksListener,
TaskStackTransitionObserver.TaskStackTransitionObserverListener {
private static final String TAG = RecentTasksController.class.getSimpleName();
private final Context mContext;
private final ShellController mShellController;
private final ShellCommandHandler mShellCommandHandler;
- private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository;
+ private final Optional<DesktopRepository> mDesktopRepository;
private final ShellExecutor mMainExecutor;
private final TaskStackListenerImpl mTaskStackListener;
private final RecentTasksImpl mImpl = new RecentTasksImpl();
@@ -121,7 +121,7 @@
ShellCommandHandler shellCommandHandler,
TaskStackListenerImpl taskStackListener,
ActivityTaskManager activityTaskManager,
- Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ Optional<DesktopRepository> desktopRepository,
TaskStackTransitionObserver taskStackTransitionObserver,
@ShellMainThread ShellExecutor mainExecutor
) {
@@ -129,7 +129,7 @@
return null;
}
return new RecentTasksController(context, shellInit, shellController, shellCommandHandler,
- taskStackListener, activityTaskManager, desktopModeTaskRepository,
+ taskStackListener, activityTaskManager, desktopRepository,
taskStackTransitionObserver, mainExecutor);
}
@@ -139,7 +139,7 @@
ShellCommandHandler shellCommandHandler,
TaskStackListenerImpl taskStackListener,
ActivityTaskManager activityTaskManager,
- Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ Optional<DesktopRepository> desktopRepository,
TaskStackTransitionObserver taskStackTransitionObserver,
ShellExecutor mainExecutor) {
mContext = context;
@@ -148,7 +148,7 @@
mActivityTaskManager = activityTaskManager;
mPcFeatureEnabled = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
mTaskStackListener = taskStackListener;
- mDesktopModeTaskRepository = desktopModeTaskRepository;
+ mDesktopRepository = desktopRepository;
mTaskStackTransitionObserver = taskStackTransitionObserver;
mMainExecutor = mainExecutor;
shellInit.addInitCallback(this::onInit, this);
@@ -168,7 +168,7 @@
this::createExternalInterface, this);
mShellCommandHandler.addDumpCallback(this::dump, this);
mTaskStackListener.addListener(this);
- mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this));
+ mDesktopRepository.ifPresent(it -> it.addActiveTaskListener(this));
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this,
mMainExecutor);
@@ -417,8 +417,8 @@
}
if (DesktopModeStatus.canEnterDesktopMode(mContext)
- && mDesktopModeTaskRepository.isPresent()
- && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) {
+ && mDesktopRepository.isPresent()
+ && mDesktopRepository.get().isActiveTask(taskInfo.taskId)) {
// Freeform tasks will be added as a separate entry
if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) {
mostRecentFreeformTaskIndex = recentTasks.size();
@@ -434,7 +434,7 @@
taskInfo.lastNonFullscreenBounds.top);
}
freeformTasks.add(taskInfo);
- if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) {
+ if (mDesktopRepository.get().isMinimizedTask(taskInfo.taskId)) {
minimizedFreeformTasks.add(taskInfo.taskId);
}
continue;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index e8eb10c..e527c02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -901,6 +901,23 @@
setEnterInstanceId(instanceId);
}
+
+ @Override
+ public void setExcludeImeInsets(boolean exclude) {
+ if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ if (mRootTaskInfo == null) {
+ ProtoLog.e(WM_SHELL_SPLIT_SCREEN, "setExcludeImeInsets: mRootTaskInfo is null");
+ return;
+ }
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
+ "setExcludeImeInsets: root taskId=%s exclude=%s",
+ mRootTaskInfo.taskId, exclude);
+ wct.setExcludeImeInsets(mRootTaskInfo.token, exclude);
+ mTaskOrganizer.applyTransaction(wct);
+ }
+ }
+
/**
* Checks if either of the apps in the desired split launch is currently in Pip. If so, it will
* launch the non-pipped app as a fullscreen app, otherwise no-op.
@@ -1717,6 +1734,7 @@
final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reparent(mMainStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
wct.reparent(mSideStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
+
// Make the stages adjacent to each other so they occlude what's behind them.
wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
setRootForceTranslucent(true, wct);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 3330f96..bcf48d9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -103,7 +103,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
@@ -155,7 +155,7 @@
private final ActivityTaskManager mActivityTaskManager;
private final ShellCommandHandler mShellCommandHandler;
private final ShellTaskOrganizer mTaskOrganizer;
- private final DesktopModeTaskRepository mDesktopRepository;
+ private final DesktopRepository mDesktopRepository;
private final ShellController mShellController;
private final Context mContext;
private final @ShellMainThread Handler mMainHandler;
@@ -227,7 +227,7 @@
ShellCommandHandler shellCommandHandler,
IWindowManager windowManager,
ShellTaskOrganizer taskOrganizer,
- DesktopModeTaskRepository desktopRepository,
+ DesktopRepository desktopRepository,
DisplayController displayController,
ShellController shellController,
DisplayInsetsController displayInsetsController,
@@ -288,7 +288,7 @@
ShellCommandHandler shellCommandHandler,
IWindowManager windowManager,
ShellTaskOrganizer taskOrganizer,
- DesktopModeTaskRepository desktopRepository,
+ DesktopRepository desktopRepository,
DisplayController displayController,
ShellController shellController,
DisplayInsetsController displayInsetsController,
@@ -538,6 +538,14 @@
decoration.closeMaximizeMenu();
}
+ private void onEnterOrExitImmersive(int taskId) {
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+ if (decoration == null) {
+ return;
+ }
+ mDesktopTasksController.toggleDesktopTaskFullImmersiveState(decoration.mTaskInfo);
+ }
+
private void onSnapResize(int taskId, boolean left) {
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
if (decoration == null) {
@@ -755,7 +763,16 @@
// back to the decoration using
// {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which
// should shared with the maximize menu's maximize/restore actions.
- onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
+ if (Flags.enableFullyImmersiveInDesktop()
+ && TaskInfoKt.getRequestingImmersive(decoration.mTaskInfo)) {
+ // Task is requesting immersive, so it should either enter or exit immersive,
+ // depending on immersive state.
+ onEnterOrExitImmersive(decoration.mTaskInfo.taskId);
+ } else {
+ // Full immersive is disabled or task doesn't request/support it, so just
+ // toggle between maximize/restore states.
+ onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
+ }
} else if (id == R.id.minimize_window) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
mDesktopTasksController.onDesktopWindowMinimize(wct, mTaskId);
@@ -935,14 +952,18 @@
}
final boolean touchingButton = (id == R.id.close_window || id == R.id.maximize_window
|| id == R.id.open_menu_button || id == R.id.minimize_window);
+ final boolean dragAllowed =
+ !mDesktopRepository.isTaskInFullImmersiveState(taskInfo.taskId);
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
- mDragPointerId = e.getPointerId(0);
- final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart(
- 0 /* ctrlType */, e.getRawX(0),
- e.getRawY(0));
- updateDragStatus(e.getActionMasked());
- mOnDragStartInitialBounds.set(initialBounds);
+ if (dragAllowed) {
+ mDragPointerId = e.getPointerId(0);
+ final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart(
+ 0 /* ctrlType */, e.getRawX(0),
+ e.getRawY(0));
+ updateDragStatus(e.getActionMasked());
+ mOnDragStartInitialBounds.set(initialBounds);
+ }
mHasLongClicked = false;
// Do not consume input event if a button is touched, otherwise it would
// prevent the button's ripple effect from showing.
@@ -951,6 +972,9 @@
case ACTION_MOVE: {
// If a decor's resize drag zone is active, don't also try to reposition it.
if (decoration.isHandlingDragResize()) break;
+ // Dragging the header isn't allowed, so skip the positioning work.
+ if (!dragAllowed) break;
+
decoration.closeMaximizeMenu();
if (e.findPointerIndex(mDragPointerId) == -1) {
mDragPointerId = e.getPointerId(0);
@@ -1036,6 +1060,10 @@
&& action != MotionEvent.ACTION_CANCEL)) {
return false;
}
+ if (mDesktopRepository.isTaskInFullImmersiveState(mTaskId)) {
+ // Disallow double-tap to resize when in full immersive.
+ return false;
+ }
onMaximizeOrRestore(mTaskId, "double_tap");
return true;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index ada1e0b..25d37fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -87,13 +87,14 @@
import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
import com.android.wm.shell.apptoweb.AppToWebUtils;
import com.android.wm.shell.apptoweb.AssistContentRequester;
+import com.android.wm.shell.apptoweb.OpenByDefaultDialog;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.CaptionState;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -163,6 +164,8 @@
private MaximizeMenu mMaximizeMenu;
+ private OpenByDefaultDialog mOpenByDefaultDialog;
+
private ResizeVeil mResizeVeil;
private Bitmap mAppIconBitmap;
private Bitmap mResizeVeilBitmap;
@@ -193,14 +196,14 @@
private final Runnable mCapturedLinkExpiredRunnable = this::onCapturedLinkExpired;
private final MultiInstanceHelper mMultiInstanceHelper;
private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
- private final DesktopModeTaskRepository mDesktopRepository;
+ private final DesktopRepository mDesktopRepository;
DesktopModeWindowDecoration(
Context context,
@NonNull Context userContext,
DisplayController displayController,
SplitScreenController splitScreenController,
- DesktopModeTaskRepository desktopRepository,
+ DesktopRepository desktopRepository,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -232,7 +235,7 @@
@NonNull Context userContext,
DisplayController displayController,
SplitScreenController splitScreenController,
- DesktopModeTaskRepository desktopRepository,
+ DesktopRepository desktopRepository,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -448,6 +451,10 @@
mHandleMenu.relayout(startT, mResult.mCaptionX);
}
+ if (isOpenByDefaultDialogActive()) {
+ mOpenByDefaultDialog.relayout(taskInfo);
+ }
+
final boolean inFullImmersive = mDesktopRepository
.isTaskInFullImmersiveState(taskInfo.taskId);
updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
@@ -510,8 +517,8 @@
closeManageWindowsMenu();
closeMaximizeMenu();
}
- updateDragResizeListener(oldDecorationSurface);
- updateMaximizeMenu(startT);
+ updateDragResizeListener(oldDecorationSurface, inFullImmersive);
+ updateMaximizeMenu(startT, inFullImmersive);
Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces
}
@@ -564,11 +571,12 @@
return mUserContext.getUser();
}
- private void updateDragResizeListener(SurfaceControl oldDecorationSurface) {
- if (!isDragResizable(mTaskInfo)) {
+ private void updateDragResizeListener(SurfaceControl oldDecorationSurface,
+ boolean inFullImmersive) {
+ if (!isDragResizable(mTaskInfo, inFullImmersive)) {
if (!mTaskInfo.positionInParent.equals(mPositionInParent)) {
// We still want to track caption bar's exclusion region on a non-resizeable task.
- updateExclusionRegion();
+ updateExclusionRegion(inFullImmersive);
}
closeDragResizeListener();
return;
@@ -602,11 +610,16 @@
getResizeEdgeHandleSize(res), getResizeHandleEdgeInset(res),
getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop)
|| !mTaskInfo.positionInParent.equals(mPositionInParent)) {
- updateExclusionRegion();
+ updateExclusionRegion(inFullImmersive);
}
}
- private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
+ private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo,
+ boolean inFullImmersive) {
+ if (inFullImmersive) {
+ // Task cannot be resized in full immersive.
+ return false;
+ }
if (DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING.isTrue()) {
return taskInfo.isFreeform();
}
@@ -670,8 +683,8 @@
mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
}
- private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
- if (!isDragResizable(mTaskInfo) || !isMaximizeMenuActive()) {
+ private void updateMaximizeMenu(SurfaceControl.Transaction startT, boolean inFullImmersive) {
+ if (!isDragResizable(mTaskInfo, inFullImmersive) || !isMaximizeMenuActive()) {
return;
}
if (!mTaskInfo.isVisible()) {
@@ -940,6 +953,33 @@
return mHandleMenu != null;
}
+ boolean isOpenByDefaultDialogActive() {
+ return mOpenByDefaultDialog != null;
+ }
+
+ void createOpenByDefaultDialog() {
+ mOpenByDefaultDialog = new OpenByDefaultDialog(
+ mContext,
+ mTaskInfo,
+ mTaskSurface,
+ mDisplayController,
+ mSurfaceControlTransactionSupplier,
+ new OpenByDefaultDialog.DialogLifecycleListener() {
+ @Override
+ public void onDialogCreated() {
+ closeHandleMenu();
+ }
+
+ @Override
+ public void onDialogDismissed() {
+ mOpenByDefaultDialog = null;
+ }
+ },
+ mAppIconBitmap,
+ mAppName
+ );
+ }
+
boolean shouldResizeListenerHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) {
return mDragResizeListener != null && mDragResizeListener.shouldHandleEvent(e, offset);
}
@@ -1236,6 +1276,12 @@
onCapturedLinkExpired();
return Unit.INSTANCE;
},
+ /* onOpenByDefaultClickListener= */ () -> {
+ if (!isOpenByDefaultDialogActive()) {
+ createOpenByDefaultDialog();
+ }
+ return Unit.INSTANCE;
+ },
/* onCloseMenuClickListener= */ () -> {
closeHandleMenu();
return Unit.INSTANCE;
@@ -1506,24 +1552,29 @@
mPositionInParent.set(mTaskInfo.positionInParent);
}
- private void updateExclusionRegion() {
+ private void updateExclusionRegion(boolean inFullImmersive) {
// An outdated position in parent is one reason for this to be called; update it here.
updatePositionInParent();
mExclusionRegionListener
- .onExclusionRegionChanged(mTaskInfo.taskId, getGlobalExclusionRegion());
+ .onExclusionRegionChanged(mTaskInfo.taskId,
+ getGlobalExclusionRegion(inFullImmersive));
}
/**
* Create a new exclusion region from the corner rects (if resizeable) and caption bounds
* of this task.
*/
- private Region getGlobalExclusionRegion() {
+ private Region getGlobalExclusionRegion(boolean inFullImmersive) {
Region exclusionRegion;
- if (mDragResizeListener != null && isDragResizable(mTaskInfo)) {
+ if (mDragResizeListener != null && isDragResizable(mTaskInfo, inFullImmersive)) {
exclusionRegion = mDragResizeListener.getCornersRegion();
} else {
exclusionRegion = new Region();
}
+ if (inFullImmersive) {
+ // Task can't be moved in full immersive, so skip excluding the caption region.
+ return exclusionRegion;
+ }
exclusionRegion.union(new Rect(0, 0, mResult.mWidth,
getCaptionHeight(mTaskInfo.getWindowingMode())));
exclusionRegion.translate(mPositionInParent.x, mPositionInParent.y);
@@ -1591,7 +1642,7 @@
@NonNull Context userContext,
DisplayController displayController,
SplitScreenController splitScreenController,
- DesktopModeTaskRepository desktopRepository,
+ DesktopRepository desktopRepository,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 98fef47..2e32703 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -120,6 +120,7 @@
onNewWindowClickListener: () -> Unit,
onManageWindowsClickListener: () -> Unit,
openInBrowserClickListener: (Intent) -> Unit,
+ onOpenByDefaultClickListener: () -> Unit,
onCloseMenuClickListener: () -> Unit,
onOutsideTouchListener: () -> Unit,
) {
@@ -135,6 +136,7 @@
onNewWindowClickListener = onNewWindowClickListener,
onManageWindowsClickListener = onManageWindowsClickListener,
openInBrowserClickListener = openInBrowserClickListener,
+ onOpenByDefaultClickListener = onOpenByDefaultClickListener,
onCloseMenuClickListener = onCloseMenuClickListener,
onOutsideTouchListener = onOutsideTouchListener,
)
@@ -153,6 +155,7 @@
onNewWindowClickListener: () -> Unit,
onManageWindowsClickListener: () -> Unit,
openInBrowserClickListener: (Intent) -> Unit,
+ onOpenByDefaultClickListener: () -> Unit,
onCloseMenuClickListener: () -> Unit,
onOutsideTouchListener: () -> Unit
) {
@@ -174,6 +177,7 @@
this.onOpenInBrowserClickListener = {
openInBrowserClickListener.invoke(openInBrowserIntent!!)
}
+ this.onOpenByDefaultClickListener = onOpenByDefaultClickListener
this.onCloseMenuClickListener = onCloseMenuClickListener
this.onOutsideTouchListener = onOutsideTouchListener
}
@@ -448,7 +452,8 @@
private val openInBrowserPill = rootView.requireViewById<View>(R.id.open_in_browser_pill)
private val browserBtn = openInBrowserPill.requireViewById<Button>(
R.id.open_in_browser_button)
-
+ private val openByDefaultBtn = openInBrowserPill.requireViewById<ImageButton>(
+ R.id.open_by_default_button)
private val decorThemeUtil = DecorThemeUtil(context)
private val animator = HandleMenuAnimator(rootView, menuWidth, captionHeight.toFloat())
@@ -461,6 +466,7 @@
var onNewWindowClickListener: (() -> Unit)? = null
var onManageWindowsClickListener: (() -> Unit)? = null
var onOpenInBrowserClickListener: (() -> Unit)? = null
+ var onOpenByDefaultClickListener: (() -> Unit)? = null
var onCloseMenuClickListener: (() -> Unit)? = null
var onOutsideTouchListener: (() -> Unit)? = null
@@ -469,6 +475,9 @@
splitscreenBtn.setOnClickListener { onToSplitScreenClickListener?.invoke() }
desktopBtn.setOnClickListener { onToDesktopClickListener?.invoke() }
browserBtn.setOnClickListener { onOpenInBrowserClickListener?.invoke() }
+ openByDefaultBtn.setOnClickListener {
+ onOpenByDefaultClickListener?.invoke()
+ }
collapseMenuButton.setOnClickListener { onCloseMenuClickListener?.invoke() }
newWindowBtn.setOnClickListener { onNewWindowClickListener?.invoke() }
manageWindowBtn.setOnClickListener { onManageWindowsClickListener?.invoke() }
@@ -634,6 +643,8 @@
setTextColor(style.textColor)
compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
}
+
+ openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor)
}
private data class MenuStyle(
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp b/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp
index 29a9f10..7412c1d 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp
@@ -15,7 +15,7 @@
//
package {
- default_team: "trendy_team_app_compat",
+ default_team: "trendy_team_lse_app_compat",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index f8f0db9..0373bbd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -26,6 +26,8 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -34,6 +36,12 @@
import android.graphics.Insets;
import android.graphics.Point;
+import android.os.Looper;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.IWindowManager;
import android.view.InsetsSource;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
@@ -47,6 +55,7 @@
import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -61,11 +70,16 @@
*/
@SmallTest
public class DisplayImeControllerTest extends ShellTestCase {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
@Mock
private SurfaceControl.Transaction mT;
@Mock
private ShellInit mShellInit;
+ @Mock
+ private IWindowManager mWm;
+ private DisplayImeController mDisplayImeController;
private DisplayImeController.PerDisplay mPerDisplay;
private Executor mExecutor;
@@ -73,7 +87,8 @@
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mExecutor = spy(Runnable::run);
- mPerDisplay = new DisplayImeController(null, mShellInit, null, null, new TransactionPool() {
+ mDisplayImeController = new DisplayImeController(mWm, mShellInit, null, null,
+ new TransactionPool() {
@Override
public SurfaceControl.Transaction acquire() {
return mT;
@@ -84,8 +99,10 @@
}
}, mExecutor) {
@Override
- void removeImeSurface(int displayId) { }
- }.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0);
+ void removeImeSurface(int displayId) {
+ }
+ };
+ mPerDisplay = mDisplayImeController.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0);
}
@Test
@@ -95,12 +112,14 @@
@Test
public void insetsControlChanged_schedulesNoWorkOnExecutor() {
+ Looper.prepare();
mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl());
verifyZeroInteractions(mExecutor);
}
@Test
public void insetsChanged_schedulesNoWorkOnExecutor() {
+ Looper.prepare();
mPerDisplay.insetsChanged(insetsStateWithIme(false));
verifyZeroInteractions(mExecutor);
}
@@ -117,7 +136,10 @@
verifyZeroInteractions(mExecutor);
}
+ // With the refactor, the control's isInitiallyVisible is used to apply to the IME, therefore
+ // this test is obsolete
@Test
+ @RequiresFlagsDisabled(android.view.inputmethod.Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
public void reappliesVisibilityToChangedLeash() {
verifyZeroInteractions(mT);
mPerDisplay.mImeShowing = false;
@@ -136,6 +158,7 @@
@Test
public void insetsControlChanged_updateImeSourceControl() {
+ Looper.prepare();
mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl());
assertNotNull(mPerDisplay.mImeSourceControl);
@@ -143,6 +166,19 @@
assertNull(mPerDisplay.mImeSourceControl);
}
+ @Test
+ @RequiresFlagsEnabled(android.view.inputmethod.Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
+ public void setImeInputTargetRequestedVisibility_invokeOnImeRequested() {
+ var mockPp = mock(DisplayImeController.ImePositionProcessor.class);
+ mDisplayImeController.addPositionProcessor(mockPp);
+
+ mPerDisplay.setImeInputTargetRequestedVisibility(true);
+ verify(mockPp).onImeRequested(anyInt(), eq(true));
+
+ mPerDisplay.setImeInputTargetRequestedVisibility(false);
+ verify(mockPp).onImeRequested(anyInt(), eq(false));
+ }
+
private InsetsSourceControl[] insetsSourceControl() {
return new InsetsSourceControl[]{
new InsetsSourceControl(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
index 628c9cdd..3e9c732 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
@@ -98,7 +98,7 @@
private lateinit var mockitoSession: StaticMockitoSession
private lateinit var handler: DesktopActivityOrientationChangeHandler
private lateinit var shellInit: ShellInit
- private lateinit var taskRepository: DesktopModeTaskRepository
+ private lateinit var taskRepository: DesktopRepository
private lateinit var testScope: CoroutineScope
// Mock running tasks are registered here so we can get the list from mock shell task organizer.
private val runningTasks = mutableListOf<RunningTaskInfo>()
@@ -116,7 +116,7 @@
testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
shellInit = spy(ShellInit(testExecutor))
taskRepository =
- DesktopModeTaskRepository(context, shellInit, persistentRepository, testScope)
+ DesktopRepository(context, shellInit, persistentRepository, testScope)
whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
new file mode 100644
index 0000000..cae6095
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for [DesktopFullImmersiveTransitionHandler].
+ *
+ * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandler
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
+
+ @Mock private lateinit var mockTransitions: Transitions
+ private lateinit var desktopRepository: DesktopRepository
+ private val transactionSupplier = { SurfaceControl.Transaction() }
+
+ private lateinit var immersiveHandler: DesktopFullImmersiveTransitionHandler
+
+ @Before
+ fun setUp() {
+ desktopRepository = DesktopRepository(
+ context, ShellInit(TestShellExecutor()), mock(), mock()
+ )
+ immersiveHandler = DesktopFullImmersiveTransitionHandler(
+ transitions = mockTransitions,
+ desktopRepository = desktopRepository,
+ transactionSupplier = transactionSupplier
+ )
+ }
+
+ @Test
+ fun enterImmersive_transitionReady_updatesRepository() {
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+ val mockBinder = mock(IBinder::class.java)
+ whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ .thenReturn(mockBinder)
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = task.displayId,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.enterImmersive(task, wct)
+ immersiveHandler.onTransitionReady(mockBinder)
+
+ assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isTrue()
+ }
+
+ @Test
+ fun exitImmersive_transitionReady_updatesRepository() {
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+ val mockBinder = mock(IBinder::class.java)
+ whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ .thenReturn(mockBinder)
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = task.displayId,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersive(task, wct)
+ immersiveHandler.onTransitionReady(mockBinder)
+
+ assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
+ }
+
+ @Test
+ fun enterImmersive_inProgress_ignores() {
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+ val mockBinder = mock(IBinder::class.java)
+ whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ .thenReturn(mockBinder)
+
+ immersiveHandler.enterImmersive(task, wct)
+ immersiveHandler.enterImmersive(task, wct)
+
+ verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+ }
+
+ @Test
+ fun exitImmersive_inProgress_ignores() {
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+ val mockBinder = mock(IBinder::class.java)
+ whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ .thenReturn(mockBinder)
+
+ immersiveHandler.exitImmersive(task, wct)
+ immersiveHandler.exitImmersive(task, wct)
+
+ verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index 2b60200..07de0716 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -60,7 +60,7 @@
class DesktopMixedTransitionHandlerTest : ShellTestCase() {
@Mock lateinit var transitions: Transitions
- @Mock lateinit var desktopTaskRepository: DesktopModeTaskRepository
+ @Mock lateinit var desktopRepository: DesktopRepository
@Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler
@Mock lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler
@Mock lateinit var interactionJankMonitor: InteractionJankMonitor
@@ -75,7 +75,7 @@
DesktopMixedTransitionHandler(
context,
transitions,
- desktopTaskRepository,
+ desktopRepository,
freeformTaskTransitionHandler,
closeDesktopTaskTransitionHandler,
interactionJankMonitor,
@@ -144,7 +144,7 @@
fun startAnimation_withClosingDesktopTask_callsCloseTaskHandler() {
val transition = mock<IBinder>()
val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM))
- whenever(desktopTaskRepository.getActiveNonMinimizedTaskCount(any())).thenReturn(2)
+ whenever(desktopRepository.getActiveNonMinimizedTaskCount(any())).thenReturn(2)
whenever(
closeDesktopTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any())
)
@@ -167,7 +167,7 @@
fun startAnimation_withClosingLastDesktopTask_dispatchesTransition() {
val transition = mock<IBinder>()
val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM))
- whenever(desktopTaskRepository.getActiveNonMinimizedTaskCount(any())).thenReturn(1)
+ whenever(desktopRepository.getActiveNonMinimizedTaskCount(any())).thenReturn(1)
whenever(transitions.dispatchTransition(any(), any(), any(), any(), any(), any()))
.thenReturn(mock())
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
index 2b7f86f..935e6d0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
@@ -17,8 +17,8 @@
package com.android.wm.shell.desktopmode
import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.PointF
import android.graphics.Rect
-import android.graphics.Region
import android.testing.AndroidTestingRunner
import android.view.SurfaceControl
import androidx.test.filters.SmallTest
@@ -33,6 +33,7 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.kotlin.whenever
@@ -58,13 +59,15 @@
whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width())
whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height())
whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS)
+ whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
}
@Test
fun testFullscreenRegionCalculation() {
createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT)
- assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top))
+ assertThat(testRegion.bounds).isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400,
+ 2 * STABLE_INSETS.top))
createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM)
testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT)
@@ -75,17 +78,19 @@
val toFullscreenWidth = displayLayout.width() * toFullscreenScale
assertThat(testRegion.bounds).isEqualTo(Rect(
(DISPLAY_BOUNDS.width() / 2f - toFullscreenWidth / 2f).toInt(),
- -50,
+ Short.MIN_VALUE.toInt(),
(DISPLAY_BOUNDS.width() / 2f + toFullscreenWidth / 2f).toInt(),
transitionHeight))
createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT)
testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT)
- assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top))
+ assertThat(testRegion.bounds).isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400,
+ 2 * STABLE_INSETS.top))
createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT)
testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT)
- assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, transitionHeight))
+ assertThat(testRegion.bounds).isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400,
+ transitionHeight))
}
@Test
@@ -133,22 +138,19 @@
}
@Test
- fun testToDesktopRegionCalculation() {
+ fun testDefaultIndicators() {
createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
- val fullscreenRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
- CAPTION_HEIGHT)
- val splitLeftRegion = visualIndicator.calculateSplitLeftRegion(displayLayout,
- TRANSITION_AREA_WIDTH, CAPTION_HEIGHT)
- val splitRightRegion = visualIndicator.calculateSplitRightRegion(displayLayout,
- TRANSITION_AREA_WIDTH, CAPTION_HEIGHT)
- val desktopRegion = visualIndicator.calculateToDesktopRegion(displayLayout,
- splitLeftRegion, splitRightRegion, fullscreenRegion)
- var testRegion = Region()
- testRegion.union(DISPLAY_BOUNDS)
- testRegion.op(splitLeftRegion, Region.Op.DIFFERENCE)
- testRegion.op(splitRightRegion, Region.Op.DIFFERENCE)
- testRegion.op(fullscreenRegion, Region.Op.DIFFERENCE)
- assertThat(desktopRegion).isEqualTo(testRegion)
+ var result = visualIndicator.updateIndicatorType(PointF(-10000f, 500f))
+ assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR)
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT)
+ result = visualIndicator.updateIndicatorType(PointF(10000f, 500f))
+ assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR)
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT)
+ result = visualIndicator.updateIndicatorType(PointF(500f, 10000f))
+ assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM)
+ result = visualIndicator.updateIndicatorType(PointF(500f, 10000f))
+ assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
}
private fun createVisualIndicator(dragStartState: DesktopModeVisualIndicator.DragStartState) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
similarity index 98%
rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index 97ceecc..55b9724 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -56,9 +56,9 @@
@SmallTest
@RunWith(AndroidTestingRunner::class)
@ExperimentalCoroutinesApi
-class DesktopModeTaskRepositoryTest : ShellTestCase() {
+class DesktopRepositoryTest : ShellTestCase() {
- private lateinit var repo: DesktopModeTaskRepository
+ private lateinit var repo: DesktopRepository
private lateinit var shellInit: ShellInit
private lateinit var datastoreScope: CoroutineScope
@@ -71,7 +71,7 @@
datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
shellInit = spy(ShellInit(testExecutor))
- repo = DesktopModeTaskRepository(context, shellInit, persistentRepository, datastoreScope)
+ repo = DesktopRepository(context, shellInit, persistentRepository, datastoreScope)
whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn(
Desktop.getDefaultInstance()
)
@@ -940,7 +940,7 @@
assertThat(repo.isTaskInFullImmersiveState(taskId = 2)).isTrue()
}
- class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
+ class TestListener : DesktopRepository.ActiveTasksListener {
var activeChangesOnDefaultDisplay = 0
var activeChangesOnSecondaryDisplay = 0
override fun onActiveTasksChanged(displayId: Int) {
@@ -952,7 +952,7 @@
}
}
- class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener {
+ class TestVisibilityListener : DesktopRepository.VisibleTasksListener {
var visibleTasksCountOnDefaultDisplay = 0
var visibleTasksCountOnSecondaryDisplay = 0
@@ -980,4 +980,4 @@
private const val DEFAULT_USER_ID = 1000
private const val DEFAULT_DESKTOP_ID = 0
}
-}
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 2ddb1ac..9e5c1a6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -147,6 +147,7 @@
import org.mockito.Mockito.times
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argThat
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.capture
import org.mockito.kotlin.eq
@@ -183,6 +184,8 @@
@Mock
lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler
@Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
+ @Mock
+ lateinit var mockDesktopFullImmersiveTransitionHandler: DesktopFullImmersiveTransitionHandler
@Mock lateinit var launchAdjacentController: LaunchAdjacentController
@Mock lateinit var splitScreenController: SplitScreenController
@Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
@@ -201,7 +204,7 @@
private lateinit var mockitoSession: StaticMockitoSession
private lateinit var controller: DesktopTasksController
private lateinit var shellInit: ShellInit
- private lateinit var taskRepository: DesktopModeTaskRepository
+ private lateinit var taskRepository: DesktopRepository
private lateinit var desktopTasksLimiter: DesktopTasksLimiter
private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener
private lateinit var testScope: CoroutineScope
@@ -232,7 +235,7 @@
testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
shellInit = spy(ShellInit(testExecutor))
- taskRepository = DesktopModeTaskRepository(context, shellInit, persistentRepository, testScope)
+ taskRepository = DesktopRepository(context, shellInit, persistentRepository, testScope)
desktopTasksLimiter =
DesktopTasksLimiter(
transitions,
@@ -289,6 +292,7 @@
dragAndDropTransitionHandler,
toggleResizeDesktopTaskTransitionHandler,
dragToDesktopTransitionHandler,
+ mockDesktopFullImmersiveTransitionHandler,
taskRepository,
desktopModeLoggerTransitionObserver,
launchAdjacentController,
@@ -2643,13 +2647,17 @@
@Test
fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() {
val task = setUpFreeformTask()
+ val spyController = spy(controller)
val mockSurface = mock(SurfaceControl::class.java)
val mockDisplayLayout = mock(DisplayLayout::class.java)
whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
- controller.onDragPositioningMove(task, mockSurface, 200f, Rect(100, -100, 500, 1000))
+ spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, -100, 500, 1000))
- controller.onDragPositioningEnd(
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
+ spyController.onDragPositioningEnd(
task,
mockSurface,
Point(100, -100), /* position */
@@ -3119,6 +3127,30 @@
verify(shellController, times(1)).addUserChangeListener(any())
}
+ @Test
+ fun toggleImmersive_enter_resizesToDisplayBounds() {
+ val task = setUpFreeformTask(DEFAULT_DISPLAY)
+ taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, false /* immersive */)
+
+ controller.toggleDesktopTaskFullImmersiveState(task)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).enterImmersive(eq(task), argThat { wct ->
+ wct.hasBoundsChange(task.token, Rect())
+ })
+ }
+
+ @Test
+ fun toggleImmersive_exit_resizesToStableBounds() {
+ val task = setUpFreeformTask(DEFAULT_DISPLAY)
+ taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, true /* immersive */)
+
+ controller.toggleDesktopTaskFullImmersiveState(task)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).exitImmersive(eq(task), argThat { wct ->
+ wct.hasBoundsChange(task.token, STABLE_BOUNDS)
+ })
+ }
+
/**
* Assert that an unhandled drag event launches a PendingIntent with the
* windowing mode and bounds we are expecting.
@@ -3484,6 +3516,13 @@
.isEqualTo(windowingMode)
}
+private fun WindowContainerTransaction.hasBoundsChange(
+ token: WindowContainerToken,
+ bounds: Rect
+): Boolean = this.changes.any { change ->
+ change.key == token.asBinder() && change.value.configuration.windowConfiguration.bounds == bounds
+}
+
private fun WindowContainerTransaction?.anyDensityConfigChange(
token: WindowContainerToken
): Boolean {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index bdcb459..596b76d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -92,7 +92,7 @@
private lateinit var mockitoSession: StaticMockitoSession
private lateinit var desktopTasksLimiter: DesktopTasksLimiter
- private lateinit var desktopTaskRepo: DesktopModeTaskRepository
+ private lateinit var desktopTaskRepo: DesktopRepository
private lateinit var shellInit: ShellInit
private lateinit var testScope: CoroutineScope
@@ -106,7 +106,7 @@
testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
desktopTaskRepo =
- DesktopModeTaskRepository(context, shellInit, persistentRepository, testScope)
+ DesktopRepository(context, shellInit, persistentRepository, testScope)
desktopTasksLimiter =
DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, MAX_TASK_LIMIT,
interactionJankMonitor, mContext, handler)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
index 42fcc83..598df34 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -62,7 +62,7 @@
private val transitions = mock<Transitions>()
private val context = mock<Context>()
private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
- private val taskRepository = mock<DesktopModeTaskRepository>()
+ private val taskRepository = mock<DesktopRepository>()
private lateinit var transitionObserver: DesktopTasksTransitionObserver
private lateinit var shellInit: ShellInit
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
index 5596ad7..1e105d9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
@@ -46,6 +46,7 @@
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -61,10 +62,7 @@
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
-/**
- * Tests of [AppHandleEducationController]
- * Usage: atest AppHandleEducationControllerTest
- */
+/** Tests of [AppHandleEducationController] Usage: atest AppHandleEducationControllerTest */
@SmallTest
@RunWith(AndroidTestingRunner::class)
@OptIn(ExperimentalCoroutinesApi::class)
@@ -220,6 +218,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ @Ignore("b/371527084: revisit testcase after refactoring original logic")
fun showWindowingImageButtonTooltip_appHandleExpanded_shouldCallShowEducationTooltipTwice() =
testScope.runTest {
// After first tooltip is dismissed, app handle is expanded. Should show second education
@@ -237,6 +236,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ @Ignore("b/371527084: revisit testcase after refactoring original logic")
fun showWindowingImageButtonTooltip_appHandleExpandedAfterTimeout_shouldCallShowEducationTooltipOnce() =
testScope.runTest {
// After first tooltip is dismissed, app handle is expanded after timeout. Should not show
@@ -258,6 +258,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ @Ignore("b/371527084: revisit testcase after refactoring original logic")
fun showWindowingImageButtonTooltip_appHandleExpandedTwice_shouldCallShowEducationTooltipTwice() =
testScope.runTest {
// After first tooltip is dismissed, app handle is expanded twice. Should show second
@@ -279,6 +280,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ @Ignore("b/371527084: revisit testcase after refactoring original logic")
fun showWindowingImageButtonTooltip_appHandleNotExpanded_shouldCallShowEducationTooltipOnce() =
testScope.runTest {
// After first tooltip is dismissed, app handle is not expanded. Should not show second
@@ -296,6 +298,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ @Ignore("b/371527084: revisit testcase after refactoring original logic")
fun showExitWindowingButtonTooltip_appHeaderVisible_shouldCallShowEducationTooltipThrice() =
testScope.runTest {
// After first two tooltips are dismissed, app header is visible. Should show third
@@ -313,6 +316,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ @Ignore("b/371527084: revisit testcase after refactoring original logic")
fun showExitWindowingButtonTooltip_appHeaderVisibleAfterTimeout_shouldCallShowEducationTooltipTwice() =
testScope.runTest {
// After first two tooltips are dismissed, app header is visible after timeout. Should not
@@ -334,6 +338,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ @Ignore("b/371527084: revisit testcase after refactoring original logic")
fun showExitWindowingButtonTooltip_appHeaderVisibleTwice_shouldCallShowEducationTooltipThrice() =
testScope.runTest {
// After first two tooltips are dismissed, app header is visible twice. Should show third
@@ -354,6 +359,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ @Ignore("b/371527084: revisit testcase after refactoring original logic")
fun showExitWindowingButtonTooltip_appHeaderExpanded_shouldCallShowEducationTooltipTwice() =
testScope.runTest {
// After first two tooltips are dismissed, app header is visible but expanded. Should not
@@ -393,6 +399,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ @Ignore("b/371527084: revisit testcase after refactoring original logic")
fun setAppHandleEducationTooltipCallbacks_onWindowingImageButtonTooltipClicked_callbackInvoked() =
testScope.runTest {
// After first tooltip is dismissed, app handle is expanded. Should show second education
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index 3b2c7e6..956ef14 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -41,7 +41,7 @@
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
import com.android.wm.shell.common.LaunchAdjacentController;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -73,7 +73,7 @@
@Mock
private SurfaceControl mMockSurfaceControl;
@Mock
- private DesktopModeTaskRepository mDesktopModeTaskRepository;
+ private DesktopRepository mDesktopRepository;
@Mock
private LaunchAdjacentController mLaunchAdjacentController;
private FreeformTaskListener mFreeformTaskListener;
@@ -89,7 +89,7 @@
mContext,
mShellInit,
mTaskOrganizer,
- Optional.of(mDesktopModeTaskRepository),
+ Optional.of(mDesktopRepository),
mLaunchAdjacentController,
mWindowDecorViewModel);
}
@@ -102,7 +102,7 @@
mFreeformTaskListener.onFocusTaskChanged(task);
- verify(mDesktopModeTaskRepository)
+ verify(mDesktopRepository)
.addOrMoveFreeformTaskToTop(task.displayId, task.taskId);
}
@@ -114,7 +114,7 @@
mFreeformTaskListener.onFocusTaskChanged(fullscreenTask);
- verify(mDesktopModeTaskRepository, never())
+ verify(mDesktopRepository, never())
.addOrMoveFreeformTaskToTop(fullscreenTask.displayId, fullscreenTask.taskId);
}
@@ -156,7 +156,7 @@
task.displayId = INVALID_DISPLAY;
mFreeformTaskListener.onTaskVanished(task);
- verify(mDesktopModeTaskRepository).minimizeTask(task.displayId, task.taskId);
+ verify(mDesktopRepository).minimizeTask(task.displayId, task.taskId);
}
@Test
@@ -168,13 +168,13 @@
mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl);
- when(mDesktopModeTaskRepository.isClosingTask(task.taskId)).thenReturn(true);
+ when(mDesktopRepository.isClosingTask(task.taskId)).thenReturn(true);
task.isVisible = false;
task.displayId = INVALID_DISPLAY;
mFreeformTaskListener.onTaskVanished(task);
- verify(mDesktopModeTaskRepository, never()).minimizeTask(task.displayId, task.taskId);
- verify(mDesktopModeTaskRepository).removeFreeformTask(task.displayId, task.taskId);
+ verify(mDesktopRepository, never()).minimizeTask(task.displayId, task.taskId);
+ verify(mDesktopRepository).removeFreeformTask(task.displayId, task.taskId);
}
@After
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index 499e339..77b2b0d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -17,6 +17,7 @@
package com.android.wm.shell.freeform;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
@@ -30,6 +31,8 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.IBinder;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.view.SurfaceControl;
import android.window.IWindowContainerToken;
import android.window.TransitionInfo;
@@ -37,28 +40,38 @@
import androidx.test.filters.SmallTest;
+import com.android.window.flags.Flags;
+import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.TransitionInfoBuilder;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Optional;
+
/**
* Tests of {@link FreeformTaskTransitionObserver}
*/
@SmallTest
public class FreeformTaskTransitionObserverTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Mock
private ShellInit mShellInit;
@Mock
private Transitions mTransitions;
@Mock
+ private DesktopFullImmersiveTransitionHandler mDesktopFullImmersiveTransitionHandler;
+ @Mock
private WindowDecorViewModel mWindowDecorViewModel;
private FreeformTaskTransitionObserver mTransitionObserver;
@@ -74,7 +87,9 @@
doReturn(pm).when(context).getPackageManager();
mTransitionObserver = new FreeformTaskTransitionObserver(
- context, mShellInit, mTransitions, mWindowDecorViewModel);
+ context, mShellInit, mTransitions,
+ Optional.of(mDesktopFullImmersiveTransitionHandler),
+ mWindowDecorViewModel);
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(
Runnable.class);
@@ -223,6 +238,19 @@
verify(mWindowDecorViewModel).destroyWindowDecoration(change2.getTaskInfo());
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ public void onTransitionReady_forwardsToDesktopImmersiveHandler() {
+ final IBinder transition = mock(IBinder.class);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CHANGE, 0).build();
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+
+ verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition);
+ }
+
private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {
final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
taskInfo.taskId = taskId;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 0364b51..9b73d53 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -70,7 +70,7 @@
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.shared.GroupedRecentTaskInfo;
import com.android.wm.shell.shared.ShellSharedConstants;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -109,7 +109,7 @@
@Mock
private ShellCommandHandler mShellCommandHandler;
@Mock
- private DesktopModeTaskRepository mDesktopModeTaskRepository;
+ private DesktopRepository mDesktopRepository;
@Mock
private ActivityTaskManager mActivityTaskManager;
@Mock
@@ -146,7 +146,7 @@
mDisplayInsetsController, mMainExecutor));
mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit,
mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager,
- Optional.of(mDesktopModeTaskRepository), mTaskStackTransitionObserver,
+ Optional.of(mDesktopRepository), mTaskStackTransitionObserver,
mMainExecutor);
mRecentTasksController = spy(mRecentTasksControllerReal);
mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
@@ -305,8 +305,8 @@
ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4);
setRawList(t1, t2, t3, t4);
- when(mDesktopModeTaskRepository.isActiveTask(1)).thenReturn(true);
- when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true);
+ when(mDesktopRepository.isActiveTask(1)).thenReturn(true);
+ when(mDesktopRepository.isActiveTask(3)).thenReturn(true);
ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
@@ -344,8 +344,8 @@
new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_2_50_50);
mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, pair1Bounds);
- when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true);
- when(mDesktopModeTaskRepository.isActiveTask(5)).thenReturn(true);
+ when(mDesktopRepository.isActiveTask(3)).thenReturn(true);
+ when(mDesktopRepository.isActiveTask(5)).thenReturn(true);
ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
@@ -384,8 +384,8 @@
ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4);
setRawList(t1, t2, t3, t4);
- when(mDesktopModeTaskRepository.isActiveTask(1)).thenReturn(true);
- when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true);
+ when(mDesktopRepository.isActiveTask(1)).thenReturn(true);
+ when(mDesktopRepository.isActiveTask(3)).thenReturn(true);
ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
@@ -412,10 +412,10 @@
ActivityManager.RecentTaskInfo t5 = makeTaskInfo(5);
setRawList(t1, t2, t3, t4, t5);
- when(mDesktopModeTaskRepository.isActiveTask(1)).thenReturn(true);
- when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true);
- when(mDesktopModeTaskRepository.isActiveTask(5)).thenReturn(true);
- when(mDesktopModeTaskRepository.isMinimizedTask(3)).thenReturn(true);
+ when(mDesktopRepository.isActiveTask(1)).thenReturn(true);
+ when(mDesktopRepository.isActiveTask(3)).thenReturn(true);
+ when(mDesktopRepository.isActiveTask(5)).thenReturn(true);
+ when(mDesktopRepository.isMinimizedTask(3)).thenReturn(true);
ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
@@ -452,8 +452,8 @@
t2.lastNonFullscreenBounds = new Rect(150, 250, 350, 450);
setRawList(t1, t2);
- when(mDesktopModeTaskRepository.isActiveTask(1)).thenReturn(true);
- when(mDesktopModeTaskRepository.isActiveTask(2)).thenReturn(true);
+ when(mDesktopRepository.isActiveTask(1)).thenReturn(true);
+ when(mDesktopRepository.isActiveTask(2)).thenReturn(true);
ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
index 769acf7..0effc3e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
@@ -48,7 +48,7 @@
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
@@ -82,7 +82,7 @@
@Mock
private ShellCommandHandler mShellCommandHandler;
@Mock
- private DesktopModeTaskRepository mDesktopModeTaskRepository;
+ private DesktopRepository mDesktopRepository;
@Mock
private ActivityTaskManager mActivityTaskManager;
@Mock
@@ -120,7 +120,7 @@
mDisplayInsetsController, mMainExecutor));
mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit,
mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager,
- Optional.of(mDesktopModeTaskRepository), mTaskStackTransitionObserver,
+ Optional.of(mDesktopRepository), mTaskStackTransitionObserver,
mMainExecutor);
mRecentTasksController = spy(mRecentTasksControllerReal);
mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 5ae4ca8..4aa7e18 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -87,7 +87,7 @@
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository
+import com.android.wm.shell.desktopmode.DesktopRepository
import com.android.wm.shell.desktopmode.DesktopTasksController
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
import com.android.wm.shell.desktopmode.DesktopTasksLimiter
@@ -161,7 +161,7 @@
@Mock private lateinit var mockTaskOrganizer: ShellTaskOrganizer
@Mock private lateinit var mockDisplayController: DisplayController
@Mock private lateinit var mockSplitScreenController: SplitScreenController
- @Mock private lateinit var mockDesktopRepository: DesktopModeTaskRepository
+ @Mock private lateinit var mockDesktopRepository: DesktopRepository
@Mock private lateinit var mockDisplayLayout: DisplayLayout
@Mock private lateinit var displayInsetsController: DisplayInsetsController
@Mock private lateinit var mockSyncQueue: SyncTransactionQueue
@@ -1221,9 +1221,48 @@
assertEquals(decor.mTaskInfo.token.asBinder(), wct.getHierarchyOps().get(0).getContainer())
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun testMaximizeButtonClick_requestingImmersive_togglesDesktopImmersiveState() {
+ val onClickListenerCaptor = forClass(View.OnClickListener::class.java)
+ as ArgumentCaptor<View.OnClickListener>
+ val decor = createOpenTaskDecoration(
+ windowingMode = WINDOWING_MODE_FREEFORM,
+ onCaptionButtonClickListener = onClickListenerCaptor,
+ requestingImmersive = true,
+ )
+ val view = mock(View::class.java)
+ whenever(view.id).thenReturn(R.id.maximize_window)
+
+ onClickListenerCaptor.value.onClick(view)
+
+ verify(mockDesktopTasksController)
+ .toggleDesktopTaskFullImmersiveState(decor.mTaskInfo)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun testMaximizeButtonClick_notRequestingImmersive_togglesDesktopTaskSize() {
+ val onClickListenerCaptor = forClass(View.OnClickListener::class.java)
+ as ArgumentCaptor<View.OnClickListener>
+ val decor = createOpenTaskDecoration(
+ windowingMode = WINDOWING_MODE_FREEFORM,
+ onCaptionButtonClickListener = onClickListenerCaptor,
+ requestingImmersive = false,
+ )
+ val view = mock(View::class.java)
+ whenever(view.id).thenReturn(R.id.maximize_window)
+
+ onClickListenerCaptor.value.onClick(view)
+
+ verify(mockDesktopTasksController)
+ .toggleDesktopTaskSize(decor.mTaskInfo)
+ }
+
private fun createOpenTaskDecoration(
@WindowingMode windowingMode: Int,
taskSurface: SurfaceControl = SurfaceControl(),
+ requestingImmersive: Boolean = false,
onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> =
forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
onLeftSnapClickListenerCaptor: ArgumentCaptor<Function0<Unit>> =
@@ -1243,7 +1282,10 @@
onCaptionButtonTouchListener: ArgumentCaptor<View.OnTouchListener> =
forClass(View.OnTouchListener::class.java) as ArgumentCaptor<View.OnTouchListener>
): DesktopModeWindowDecoration {
- val decor = setUpMockDecorationForTask(createTask(windowingMode = windowingMode))
+ val decor = setUpMockDecorationForTask(createTask(
+ windowingMode = windowingMode,
+ requestingImmersive = requestingImmersive
+ ))
onTaskOpening(decor.mTaskInfo, taskSurface)
verify(decor).setOnMaximizeOrRestoreClickListener(onMaxOrRestoreListenerCaptor.capture())
verify(decor).setOnLeftSnapClickListener(onLeftSnapClickListenerCaptor.capture())
@@ -1282,6 +1324,7 @@
activityType: Int = ACTIVITY_TYPE_STANDARD,
focused: Boolean = true,
activityInfo: ActivityInfo = ActivityInfo(),
+ requestingImmersive: Boolean = false
): RunningTaskInfo {
return TestRunningTaskInfoBuilder()
.setDisplayId(displayId)
@@ -1292,6 +1335,11 @@
topActivityInfo = activityInfo
isFocused = focused
isResizeable = true
+ requestedVisibleTypes = if (requestingImmersive) {
+ statusBars().inv()
+ } else {
+ statusBars()
+ }
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index ed752fd..35be80e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -106,7 +106,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.CaptionState;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -163,7 +163,7 @@
@Mock
private ShellTaskOrganizer mMockShellTaskOrganizer;
@Mock
- private DesktopModeTaskRepository mMockDesktopRepository;
+ private DesktopRepository mMockDesktopRepository;
@Mock
private Choreographer mMockChoreographer;
@Mock
@@ -1046,6 +1046,7 @@
any(),
openInBrowserCaptor.capture(),
any(),
+ any(),
any()
);
openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1));
@@ -1074,6 +1075,7 @@
any(),
openInBrowserCaptor.capture(),
any(),
+ any(),
any()
);
@@ -1124,6 +1126,7 @@
any(),
any(),
any(),
+ any(),
closeClickListener.capture(),
any()
);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
index cabd472..1820133 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -240,7 +240,7 @@
null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50,
captionX = captionX
)
- handleMenu.show(mock(), mock(), mock(), mock(), mock(), mock(), mock(), mock())
+ handleMenu.show(mock(), mock(), mock(), mock(), mock(), mock(), mock(), mock(), mock())
return handleMenu
}
diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp
index a39f30b..385fbfe 100644
--- a/libs/androidfw/Android.bp
+++ b/libs/androidfw/Android.bp
@@ -75,6 +75,7 @@
"BigBufferStream.cpp",
"ChunkIterator.cpp",
"ConfigDescription.cpp",
+ "CursorWindow.cpp",
"FileStream.cpp",
"Idmap.cpp",
"LoadedArsc.cpp",
@@ -113,7 +114,6 @@
srcs: [
"BackupData.cpp",
"BackupHelpers.cpp",
- "CursorWindow.cpp",
],
shared_libs: [
"libbase",
@@ -147,11 +147,6 @@
"libz",
],
},
- host_linux: {
- srcs: [
- "CursorWindow.cpp",
- ],
- },
windows: {
enabled: true,
},
diff --git a/libs/androidfw/CursorWindow.cpp b/libs/androidfw/CursorWindow.cpp
index cbb1e8f..abf2b0a 100644
--- a/libs/androidfw/CursorWindow.cpp
+++ b/libs/androidfw/CursorWindow.cpp
@@ -139,6 +139,7 @@
return UNKNOWN_ERROR;
}
+#ifdef __linux__
status_t CursorWindow::createFromParcel(Parcel* parcel, CursorWindow** outWindow) {
*outWindow = nullptr;
@@ -240,6 +241,7 @@
fail_silent:
return UNKNOWN_ERROR;
}
+#endif
status_t CursorWindow::clear() {
if (mReadOnly) {
diff --git a/libs/androidfw/include/androidfw/CursorWindow.h b/libs/androidfw/include/androidfw/CursorWindow.h
index c2eac12..0996355 100644
--- a/libs/androidfw/include/androidfw/CursorWindow.h
+++ b/libs/androidfw/include/androidfw/CursorWindow.h
@@ -23,7 +23,9 @@
#include <string>
#include "android-base/stringprintf.h"
+#ifdef __linux__
#include "binder/Parcel.h"
+#endif
#include "utils/String8.h"
#include "android-base/mapped_file.h"
@@ -82,9 +84,11 @@
~CursorWindow();
static status_t create(const String8& name, size_t size, CursorWindow** outCursorWindow);
+#ifdef __linux__
static status_t createFromParcel(Parcel* parcel, CursorWindow** outCursorWindow);
status_t writeToParcel(Parcel* parcel);
+#endif
inline String8 name() { return mName; }
inline size_t size() { return mSize; }
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 8b9ec38..739c7d6 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1651,6 +1651,9 @@
<!-- Name of the usb audio device mic. [CHAR LIMIT=50] -->
<string name="media_transfer_usb_device_mic_name">USB microphone</string>
+ <!-- Name of the bluetooth audio device mic. [CHAR LIMIT=50] -->
+ <string name="media_transfer_bt_device_mic_name">BT microphone</string>
+
<!-- Label for Wifi hotspot switch on. Toggles hotspot on [CHAR LIMIT=30] -->
<string name="wifi_hotspot_switch_on_text">On</string>
<!-- Label for Wifi hotspot switch off. Toggles hotspot off [CHAR LIMIT=30] -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
index e44a134..1d17b00 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
@@ -15,6 +15,7 @@
*/
package com.android.settingslib.media;
+import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_SCO;
import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
import static android.media.AudioDeviceInfo.TYPE_USB_ACCESSORY;
import static android.media.AudioDeviceInfo.TYPE_USB_DEVICE;
@@ -90,7 +91,8 @@
TYPE_WIRED_HEADSET,
TYPE_USB_DEVICE,
TYPE_USB_HEADSET,
- TYPE_USB_ACCESSORY ->
+ TYPE_USB_ACCESSORY,
+ TYPE_BLUETOOTH_SCO ->
true;
default -> false;
};
@@ -103,6 +105,8 @@
R.string.media_transfer_wired_device_mic_name);
case TYPE_USB_DEVICE, TYPE_USB_HEADSET, TYPE_USB_ACCESSORY -> mContext.getString(
R.string.media_transfer_usb_device_mic_name);
+ case TYPE_BLUETOOTH_SCO -> mContext.getString(
+ R.string.media_transfer_bt_device_mic_name);
default -> mContext.getString(R.string.media_transfer_this_device_name_desktop);
};
return name.toString();
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
index 2f0aa1c..30e4637 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
@@ -37,6 +37,7 @@
private final int BUILTIN_MIC_ID = 1;
private final int WIRED_HEADSET_ID = 2;
private final int USB_HEADSET_ID = 3;
+ private final int BT_HEADSET_ID = 4;
private final int MAX_VOLUME = 1;
private final int CURRENT_VOLUME = 0;
private final boolean IS_VOLUME_FIXED = true;
@@ -108,4 +109,19 @@
assertThat(usbMediaDevice.getName())
.isEqualTo(mContext.getString(R.string.media_transfer_usb_device_mic_name));
}
+
+ @Test
+ public void getName_returnCorrectName_btHeadset() {
+ InputMediaDevice btMediaDevice =
+ InputMediaDevice.create(
+ mContext,
+ String.valueOf(BT_HEADSET_ID),
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
+ MAX_VOLUME,
+ CURRENT_VOLUME,
+ IS_VOLUME_FIXED);
+ assertThat(btMediaDevice).isNotNull();
+ assertThat(btMediaDevice.getName())
+ .isEqualTo(mContext.getString(R.string.media_transfer_bt_device_mic_name));
+ }
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 0773bd7..8f58e8c 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -434,7 +434,7 @@
VALIDATORS.put(Global.Wearable.PHONE_SWITCHING_STATUS,
new InclusiveIntegerRangeValidator(
Global.Wearable.PHONE_SWITCHING_STATUS_NOT_STARTED,
- Global.Wearable.PHONE_SWITCHING_STATUS_IN_PROGRESS_MIGRATION_SUCCESS));
+ Global.Wearable.PHONE_SWITCHING_STATUS_ACCOUNTS_MATCHED));
VALIDATORS.put(Global.Wearable.REDUCE_MOTION, BOOLEAN_VALIDATOR);
VALIDATORS.put(Global.Wearable.RTL_SWIPE_TO_DISMISS_ENABLED_DEV, BOOLEAN_VALIDATOR);
VALIDATORS.put(
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index b491b5a..d39b564 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -223,7 +223,6 @@
Settings.Global.ENABLE_DELETION_HELPER_NO_THRESHOLD_TOGGLE,
Settings.Global.ENABLE_DISKSTATS_LOGGING,
Settings.Global.ENABLE_EPHEMERAL_FEATURE,
- Settings.Global.ENABLE_USE_APP_INFO_NOT_LAUNCHED,
Settings.Global.DYNAMIC_POWER_SAVINGS_ENABLED,
Settings.Global.DYNAMIC_POWER_SAVINGS_DISABLE_THRESHOLD,
Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS,
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index f3c5a18..456fedf 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -948,6 +948,9 @@
<!-- Permission required for CTS test - CtsNfcTestCases -->
<uses-permission android:name="android.permission.NFC_SET_CONTROLLER_ALWAYS_ON" />
+ <!-- Permission required for CTS test - CtsAppTestCases -->
+ <uses-permission android:name="android.permission.KILL_UID" />
+
<application
android:label="@string/app_label"
android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index aaa527d..c6238e8 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1473,3 +1473,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "check_lockscreen_gone_transition"
+ namespace: "systemui"
+ description: "Run notification pipeline when the lockscreen is not in gone transition for avoiding janky frames during unlocking animation"
+ bug: "358301118"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
index f5d01d7..907c39d 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
@@ -944,9 +944,26 @@
}
override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
- startController.onTransitionAnimationEnd(isExpandingFullyAbove)
- endController.onTransitionAnimationEnd(isExpandingFullyAbove)
- onLaunchAnimationEnd()
+ // onLaunchAnimationEnd is called by an Animator at the end of the animation,
+ // on a Choreographer animation tick. The following calls will move the animated
+ // content from the dialog overlay back to its original position, and this
+ // change must be reflected in the next frame given that we then sync the next
+ // frame of both the content and dialog ViewRoots. However, in case that content
+ // is rendered by Compose, whose compositions are also scheduled on a
+ // Choreographer frame, any state change made *right now* won't be reflected in
+ // the next frame given that a Choreographer frame can't schedule another and
+ // have it happen in the same frame. So we post the forwarded calls to
+ // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring
+ // that the move of the content back to its original window will be reflected in
+ // the next frame right after [onLaunchAnimationEnd] is called.
+ //
+ // TODO(b/330672236): Move this to TransitionAnimator.
+ dialog.context.mainExecutor.execute {
+ startController.onTransitionAnimationEnd(isExpandingFullyAbove)
+ endController.onTransitionAnimationEnd(isExpandingFullyAbove)
+
+ onLaunchAnimationEnd()
+ }
}
override fun onTransitionAnimationProgress(
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index 859fc4e0..fc4cf1d 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -379,26 +379,13 @@
Log.d(TAG, "Animation ended")
}
- // onAnimationEnd is called at the end of the animation, on a Choreographer
- // animation tick. During dialog launches, the following calls will move the
- // animated content from the dialog overlay back to its original position, and
- // this change must be reflected in the next frame given that we then sync the
- // next frame of both the content and dialog ViewRoots. During SysUI activity
- // launches, we will instantly collapse the shade at the end of the transition.
- // However, if those are rendered by Compose, whose compositions are also
- // scheduled on a Choreographer frame, any state change made *right now* won't
- // be reflected in the next frame given that a Choreographer frame can't
- // schedule another and have it happen in the same frame. So we post the
- // forwarded calls to [Controller.onLaunchAnimationEnd] in the main executor,
- // leaving this Choreographer frame, ensuring that any state change applied by
- // onTransitionAnimationEnd() will be reflected in the same frame.
- mainExecutor.execute {
- controller.onTransitionAnimationEnd(isExpandingFullyAbove)
- transitionContainerOverlay.remove(windowBackgroundLayer)
+ // TODO(b/330672236): Post this to the main thread instead so that it does not
+ // flicker with Flexiglass enabled.
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove)
+ transitionContainerOverlay.remove(windowBackgroundLayer)
- if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
- openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
- }
+ if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
+ openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
}
}
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Bounceable.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Bounceable.kt
new file mode 100644
index 0000000..3f2f84b
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Bounceable.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.compose.animation
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import kotlin.math.roundToInt
+
+/** A component that can bounce in one dimension, for instance when it is tapped. */
+interface Bounceable {
+ val bounce: Dp
+}
+
+/**
+ * Bounce a composable in the given [orientation] when this [bounceable], the [previousBounceable]
+ * or [nextBounceable] is bouncing.
+ *
+ * Important: This modifier should be used on composables that have a fixed size in [orientation],
+ * i.e. they should be placed *after* modifiers like Modifier.fillMaxWidth() or Modifier.height().
+ *
+ * @param bounceable the [Bounceable] associated to the current composable that will make this
+ * composable size grow when bouncing.
+ * @param previousBounceable the [Bounceable] associated to the previous composable in [orientation]
+ * that will make this composable shrink when bouncing.
+ * @param nextBounceable the [Bounceable] associated to the next composable in [orientation] that
+ * will make this composable shrink when bouncing.
+ * @param orientation the orientation in which this bounceable should grow/shrink.
+ * @param bounceEnd whether this bounceable should bounce on the end (right in LTR layouts, left in
+ * RTL layouts) side. This can be used for grids for which the last item does not align perfectly
+ * with the end of the grid.
+ */
+fun Modifier.bounceable(
+ bounceable: Bounceable,
+ previousBounceable: Bounceable?,
+ nextBounceable: Bounceable?,
+ orientation: Orientation,
+ bounceEnd: Boolean = nextBounceable != null,
+): Modifier {
+ return layout { measurable, constraints ->
+ // The constraints in the orientation should be fixed, otherwise there is no way to know
+ // what the size of our child node will be without this animation code.
+ checkFixedSize(constraints, orientation)
+
+ var sizePrevious = 0f
+ var sizeNext = 0f
+
+ if (previousBounceable != null) {
+ sizePrevious += bounceable.bounce.toPx() - previousBounceable.bounce.toPx()
+ }
+
+ if (nextBounceable != null) {
+ sizeNext += bounceable.bounce.toPx() - nextBounceable.bounce.toPx()
+ } else if (bounceEnd) {
+ sizeNext += bounceable.bounce.toPx()
+ }
+
+ when (orientation) {
+ Orientation.Horizontal -> {
+ val idleWidth = constraints.maxWidth
+ val animatedWidth = (idleWidth + sizePrevious + sizeNext).roundToInt()
+ val animatedConstraints =
+ constraints.copy(minWidth = animatedWidth, maxWidth = animatedWidth)
+
+ val placeable = measurable.measure(animatedConstraints)
+
+ // Important: we still place the element using the idle size coming from the
+ // constraints, otherwise the parent will automatically center this node given the
+ // size that it expects us to be. This allows us to then place the element where we
+ // want it to be.
+ layout(idleWidth, placeable.height) {
+ placeable.placeRelative(-sizePrevious.roundToInt(), 0)
+ }
+ }
+ Orientation.Vertical -> {
+ val idleHeight = constraints.maxHeight
+ val animatedHeight = (idleHeight + sizePrevious + sizeNext).roundToInt()
+ val animatedConstraints =
+ constraints.copy(minHeight = animatedHeight, maxHeight = animatedHeight)
+
+ val placeable = measurable.measure(animatedConstraints)
+ layout(placeable.width, idleHeight) {
+ placeable.placeRelative(0, -sizePrevious.roundToInt())
+ }
+ }
+ }
+ }
+}
+
+private fun checkFixedSize(constraints: Constraints, orientation: Orientation) {
+ when (orientation) {
+ Orientation.Horizontal -> {
+ check(constraints.hasFixedWidth) {
+ "Modifier.bounceable() should receive a fixed width from its parent. Make sure " +
+ "that it is used *after* a fixed-width Modifier in the horizontal axis (like" +
+ " Modifier.fillMaxWidth() or Modifier.width())."
+ }
+ }
+ Orientation.Vertical -> {
+ check(constraints.hasFixedHeight) {
+ "Modifier.bounceable() should receive a fixed height from its parent. Make sure " +
+ "that it is used *after* a fixed-height Modifier in the vertical axis (like" +
+ " Modifier.fillMaxHeight() or Modifier.height())."
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/BounceableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/BounceableTest.kt
new file mode 100644
index 0000000..335e9f8
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/BounceableTest.kt
@@ -0,0 +1,212 @@
+/*
+ * 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.compose.animation
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.times
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BounceableTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun bounceable_horizontal() {
+ var bounceables by mutableStateOf(List(4) { bounceable(0.dp) })
+
+ rule.setContent {
+ Row(Modifier.size(100.dp, 50.dp)) {
+ repeat(bounceables.size) { i ->
+ Box(
+ Modifier.weight(1f)
+ .fillMaxHeight()
+ .bounceable(bounceables, i, orientation = Orientation.Horizontal)
+ )
+ }
+ }
+ }
+
+ // All bounceables have a width of (100dp / bounceables.size) = 25dp and height of 50dp.
+ repeat(bounceables.size) { i ->
+ rule
+ .onNodeWithTag(bounceableTag(i))
+ .assertWidthIsEqualTo(25.dp)
+ .assertHeightIsEqualTo(50.dp)
+ .assertPositionInRootIsEqualTo(i * 25.dp, 0.dp)
+ }
+
+ // If all bounceables have the same bounce, it's the same as if they didn't have any.
+ bounceables = List(4) { bounceable(10.dp) }
+ repeat(bounceables.size) { i ->
+ rule
+ .onNodeWithTag(bounceableTag(i))
+ .assertWidthIsEqualTo(25.dp)
+ .assertHeightIsEqualTo(50.dp)
+ .assertPositionInRootIsEqualTo(i * 25.dp, 0.dp)
+ }
+
+ // Bounce the first and third one.
+ bounceables =
+ listOf(
+ bounceable(bounce = 5.dp),
+ bounceable(bounce = 0.dp),
+ bounceable(bounce = 10.dp),
+ bounceable(bounce = 0.dp),
+ )
+
+ // First one has a width of 25dp + 5dp, located in (0, 0).
+ rule
+ .onNodeWithTag(bounceableTag(0))
+ .assertWidthIsEqualTo(30.dp)
+ .assertHeightIsEqualTo(50.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+ // Second one has a width of 25dp - 5dp - 10dp, located in (30, 0).
+ rule
+ .onNodeWithTag(bounceableTag(1))
+ .assertWidthIsEqualTo(10.dp)
+ .assertHeightIsEqualTo(50.dp)
+ .assertPositionInRootIsEqualTo(30.dp, 0.dp)
+
+ // Third one has a width of 25 + 2 * 10dp, located in (40, 0).
+ rule
+ .onNodeWithTag(bounceableTag(2))
+ .assertWidthIsEqualTo(45.dp)
+ .assertHeightIsEqualTo(50.dp)
+ .assertPositionInRootIsEqualTo(40.dp, 0.dp)
+
+ // First one has a width of 25dp - 10dp, located in (85, 0).
+ rule
+ .onNodeWithTag(bounceableTag(3))
+ .assertWidthIsEqualTo(15.dp)
+ .assertHeightIsEqualTo(50.dp)
+ .assertPositionInRootIsEqualTo(85.dp, 0.dp)
+ }
+
+ @Test
+ fun bounceable_vertical() {
+ var bounceables by mutableStateOf(List(4) { bounceable(0.dp) })
+
+ rule.setContent {
+ Column(Modifier.size(50.dp, 100.dp)) {
+ repeat(bounceables.size) { i ->
+ Box(
+ Modifier.weight(1f)
+ .fillMaxWidth()
+ .bounceable(bounceables, i, Orientation.Vertical)
+ )
+ }
+ }
+ }
+
+ // All bounceables have a height of (100dp / bounceables.size) = 25dp and width of 50dp.
+ repeat(bounceables.size) { i ->
+ rule
+ .onNodeWithTag(bounceableTag(i))
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(25.dp)
+ .assertPositionInRootIsEqualTo(0.dp, i * 25.dp)
+ }
+
+ // If all bounceables have the same bounce, it's the same as if they didn't have any.
+ bounceables = List(4) { bounceable(10.dp) }
+ repeat(bounceables.size) { i ->
+ rule
+ .onNodeWithTag(bounceableTag(i))
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(25.dp)
+ .assertPositionInRootIsEqualTo(0.dp, i * 25.dp)
+ }
+
+ // Bounce the first and third one.
+ bounceables =
+ listOf(
+ bounceable(bounce = 5.dp),
+ bounceable(bounce = 0.dp),
+ bounceable(bounce = 10.dp),
+ bounceable(bounce = 0.dp),
+ )
+
+ // First one has a height of 25dp + 5dp, located in (0, 0).
+ rule
+ .onNodeWithTag(bounceableTag(0))
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(30.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+ // Second one has a height of 25dp - 5dp - 10dp, located in (0, 30).
+ rule
+ .onNodeWithTag(bounceableTag(1))
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(10.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 30.dp)
+
+ // Third one has a height of 25 + 2 * 10dp, located in (0, 40).
+ rule
+ .onNodeWithTag(bounceableTag(2))
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(45.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 40.dp)
+
+ // First one has a height of 25dp - 10dp, located in (0, 85).
+ rule
+ .onNodeWithTag(bounceableTag(3))
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(15.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 85.dp)
+ }
+
+ private fun bounceable(bounce: Dp): Bounceable {
+ return object : Bounceable {
+ override val bounce: Dp = bounce
+ }
+ }
+
+ private fun Modifier.bounceable(
+ bounceables: List<Bounceable>,
+ i: Int,
+ orientation: Orientation,
+ ): Modifier {
+ val previous = if (i > 0) bounceables[i - 1] else null
+ val next = if (i < bounceables.lastIndex) bounceables[i + 1] else null
+ return this.bounceable(bounceables[i], previous, next, orientation)
+ .testTag(bounceableTag(i))
+ }
+
+ private fun bounceableTag(i: Int) = "bounceable$i"
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/DreamSceneModule.kt
similarity index 63%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
copy to packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/DreamSceneModule.kt
index 3a7d7ba..2f82369 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/DreamSceneModule.kt
@@ -14,10 +14,15 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.notification.row.domain.interactor
+package com.android.systemui.scene
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import com.android.systemui.dream.ui.composable.DreamScene
+import com.android.systemui.scene.ui.composable.Scene
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
-fun Kosmos.getNotificationRowInteractor(repository: NotificationRowRepository) =
- NotificationRowInteractor(repository = repository)
+@Module
+interface DreamSceneModule {
+ @Binds @IntoSet fun dreamScene(scene: DreamScene): Scene
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
new file mode 100644
index 0000000..fda46b8
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
@@ -0,0 +1,242 @@
+/*
+ * 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.communal.ui.compose
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.gestures.AnchoredDraggableState
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.anchoredDraggable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastIsFinite
+import com.android.compose.theme.LocalAndroidColorScheme
+import com.android.systemui.communal.ui.viewmodel.DragHandle
+import com.android.systemui.communal.ui.viewmodel.ResizeInfo
+import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
+import com.android.systemui.lifecycle.rememberViewModel
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+
+@Composable
+private fun UpdateGridLayoutInfo(
+ viewModel: ResizeableItemFrameViewModel,
+ index: Int,
+ gridState: LazyGridState,
+ minItemSpan: Int,
+ gridContentPadding: PaddingValues,
+ verticalArrangement: Arrangement.Vertical,
+) {
+ val density = LocalDensity.current
+ LaunchedEffect(
+ density,
+ viewModel,
+ index,
+ gridState,
+ minItemSpan,
+ gridContentPadding,
+ verticalArrangement,
+ ) {
+ val verticalItemSpacingPx = with(density) { verticalArrangement.spacing.toPx() }
+ val verticalContentPaddingPx =
+ with(density) {
+ (gridContentPadding.calculateTopPadding() +
+ gridContentPadding.calculateBottomPadding())
+ .toPx()
+ }
+
+ combine(
+ snapshotFlow { gridState.layoutInfo.maxSpan },
+ snapshotFlow { gridState.layoutInfo.viewportSize.height },
+ snapshotFlow {
+ gridState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
+ }
+ .filterNotNull(),
+ ::Triple,
+ )
+ .collectLatest { (maxItemSpan, viewportHeightPx, itemInfo) ->
+ viewModel.setGridLayoutInfo(
+ verticalItemSpacingPx,
+ verticalContentPaddingPx,
+ viewportHeightPx,
+ maxItemSpan,
+ minItemSpan,
+ itemInfo.row,
+ itemInfo.span,
+ )
+ }
+ }
+}
+
+@Composable
+private fun BoxScope.DragHandle(
+ handle: DragHandle,
+ dragState: AnchoredDraggableState<Int>,
+ outlinePadding: Dp,
+ brush: Brush,
+ alpha: () -> Float,
+ modifier: Modifier = Modifier,
+) {
+ val directionalModifier = if (handle == DragHandle.TOP) -1 else 1
+ val alignment = if (handle == DragHandle.TOP) Alignment.TopCenter else Alignment.BottomCenter
+ Box(
+ modifier
+ .align(alignment)
+ .graphicsLayer {
+ translationY =
+ directionalModifier * (size.height / 2 + outlinePadding.toPx()) +
+ (dragState.offset.takeIf { it.fastIsFinite() } ?: 0f)
+ }
+ .anchoredDraggable(dragState, Orientation.Vertical)
+ ) {
+ Canvas(modifier = Modifier.fillMaxSize()) {
+ if (dragState.anchors.size > 1) {
+ drawCircle(
+ brush = brush,
+ radius = outlinePadding.toPx(),
+ center = Offset(size.width / 2, size.height / 2),
+ alpha = alpha(),
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Draws a frame around the content with drag handles on the top and bottom of the content.
+ *
+ * @param index The index of this item in the [LazyGridState].
+ * @param gridState The [LazyGridState] for the grid containing this item.
+ * @param minItemSpan The minimum span that an item may occupy. Items are resized in multiples of
+ * this span.
+ * @param gridContentPadding The content padding used for the grid, needed for determining offsets.
+ * @param verticalArrangement The vertical arrangement of the grid items.
+ * @param modifier Optional modifier to apply to the frame.
+ * @param enabled Whether resizing is enabled.
+ * @param outlinePadding The padding to apply around the entire frame, in [Dp]
+ * @param outlineColor Optional color to make the outline around the content.
+ * @param cornerRadius Optional radius to give to the outline around the content.
+ * @param strokeWidth Optional stroke width to draw the outline with.
+ * @param alpha Optional function to provide an alpha value for the outline. Can be used to fade the
+ * outline in and out. This is wrapped in a function for performance, as the value is only
+ * accessed during the draw phase.
+ * @param onResize Optional callback which gets executed when the item is resized to a new span.
+ * @param content The content to draw inside the frame.
+ */
+@Composable
+fun ResizableItemFrame(
+ index: Int,
+ gridState: LazyGridState,
+ minItemSpan: Int,
+ gridContentPadding: PaddingValues,
+ verticalArrangement: Arrangement.Vertical,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ outlinePadding: Dp = 8.dp,
+ outlineColor: Color = LocalAndroidColorScheme.current.primary,
+ cornerRadius: Dp = 37.dp,
+ strokeWidth: Dp = 3.dp,
+ alpha: () -> Float = { 1f },
+ onResize: (info: ResizeInfo) -> Unit = {},
+ content: @Composable () -> Unit,
+) {
+ val brush = SolidColor(outlineColor)
+ val viewModel =
+ rememberViewModel(traceName = "ResizeableItemFrame.viewModel") {
+ ResizeableItemFrameViewModel()
+ }
+
+ val dragHandleHeight = verticalArrangement.spacing - outlinePadding * 2
+
+ // Draw content surrounded by drag handles at top and bottom. Allow drag handles
+ // to overlap content.
+ Box(modifier) {
+ content()
+
+ if (enabled) {
+ DragHandle(
+ handle = DragHandle.TOP,
+ dragState = viewModel.topDragState,
+ outlinePadding = outlinePadding,
+ brush = brush,
+ alpha = alpha,
+ modifier = Modifier.fillMaxWidth().height(dragHandleHeight),
+ )
+
+ DragHandle(
+ handle = DragHandle.BOTTOM,
+ dragState = viewModel.bottomDragState,
+ outlinePadding = outlinePadding,
+ brush = brush,
+ alpha = alpha,
+ modifier = Modifier.fillMaxWidth().height(dragHandleHeight),
+ )
+
+ // Draw outline around the element.
+ Canvas(modifier = Modifier.matchParentSize()) {
+ val paddingPx = outlinePadding.toPx()
+ val topOffset = viewModel.topDragState.offset.takeIf { it.fastIsFinite() } ?: 0f
+ val bottomOffset =
+ viewModel.bottomDragState.offset.takeIf { it.fastIsFinite() } ?: 0f
+ drawRoundRect(
+ brush,
+ alpha = alpha(),
+ topLeft = Offset(-paddingPx, topOffset + -paddingPx),
+ size =
+ Size(
+ width = size.width + paddingPx * 2,
+ height = -topOffset + bottomOffset + size.height + paddingPx * 2,
+ ),
+ cornerRadius = CornerRadius(cornerRadius.toPx()),
+ style = Stroke(width = strokeWidth.toPx()),
+ )
+ }
+
+ UpdateGridLayoutInfo(
+ viewModel,
+ index,
+ gridState,
+ minItemSpan,
+ gridContentPadding,
+ verticalArrangement,
+ )
+ LaunchedEffect(viewModel) { viewModel.resizeInfo.collectLatest(onResize) }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/dream/ui/composable/DreamScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/dream/ui/composable/DreamScene.kt
new file mode 100644
index 0000000..f4374c6
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/dream/ui/composable/DreamScene.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.dream.ui.composable
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dreams.ui.viewmodel.DreamUserActionsViewModel
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.composable.Scene
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** The dream scene shows when a dream activity is showing. */
+@SysUISingleton
+class DreamScene
+@Inject
+constructor(private val actionsViewModelFactory: DreamUserActionsViewModel.Factory) :
+ ExclusiveActivatable(), Scene {
+ override val key = Scenes.Dream
+
+ private val actionsViewModel: DreamUserActionsViewModel by lazy {
+ actionsViewModelFactory.create()
+ }
+
+ override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions
+
+ override suspend fun onActivated(): Nothing {
+ actionsViewModel.activate()
+ }
+
+ @Composable
+ override fun SceneScope.Content(modifier: Modifier) {
+ Box(modifier = modifier.fillMaxSize()) {
+ // Render a sleep emoji to make the scene appear visible.
+ Text(
+ modifier = Modifier.padding(16.dp).align(Alignment.BottomStart),
+ text = "\uD83D\uDCA4",
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index 340ac32..8728521 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -15,11 +15,13 @@
import com.android.systemui.scene.ui.composable.transitions.bouncerToLockscreenPreview
import com.android.systemui.scene.ui.composable.transitions.communalToBouncerTransition
import com.android.systemui.scene.ui.composable.transitions.communalToShadeTransition
+import com.android.systemui.scene.ui.composable.transitions.dreamToGoneTransition
import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition
import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition
import com.android.systemui.scene.ui.composable.transitions.goneToSplitShadeTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToBouncerTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToCommunalTransition
+import com.android.systemui.scene.ui.composable.transitions.lockscreenToDreamTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToGoneTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToQuickSettingsTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToShadeTransition
@@ -52,6 +54,7 @@
// Scene transitions
from(Scenes.Bouncer, to = Scenes.Gone) { bouncerToGoneTransition() }
+ from(Scenes.Dream, to = Scenes.Gone) { dreamToGoneTransition() }
from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() }
from(Scenes.Gone, to = Scenes.Shade, key = ToSplitShade) { goneToSplitShadeTransition() }
from(Scenes.Gone, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) {
@@ -72,6 +75,7 @@
lockscreenToBouncerTransition()
}
from(Scenes.Lockscreen, to = Scenes.Communal) { lockscreenToCommunalTransition() }
+ from(Scenes.Lockscreen, to = Scenes.Dream) { lockscreenToDreamTransition() }
from(Scenes.Lockscreen, to = Scenes.Shade) { lockscreenToShadeTransition() }
from(Scenes.Lockscreen, to = Scenes.Shade, key = ToSplitShade) {
lockscreenToSplitShadeTransition()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToGoneTransition.kt
similarity index 63%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
copy to packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToGoneTransition.kt
index 3a7d7ba..60dc4c05 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToGoneTransition.kt
@@ -14,10 +14,14 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.notification.row.domain.interactor
+package com.android.systemui.scene.ui.composable.transitions
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.scene.shared.model.Scenes
-fun Kosmos.getNotificationRowInteractor(repository: NotificationRowRepository) =
- NotificationRowInteractor(repository = repository)
+fun TransitionBuilder.dreamToGoneTransition() {
+ spec = tween(durationMillis = 1000)
+
+ fade(Scenes.Dream.rootElementKey)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToDreamTransition.kt
similarity index 62%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
copy to packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToDreamTransition.kt
index 3a7d7ba..7092e3f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToDreamTransition.kt
@@ -14,10 +14,14 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.notification.row.domain.interactor
+package com.android.systemui.scene.ui.composable.transitions
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.scene.shared.model.Scenes
-fun Kosmos.getNotificationRowInteractor(repository: NotificationRowRepository) =
- NotificationRowInteractor(repository = repository)
+fun TransitionBuilder.lockscreenToDreamTransition() {
+ spec = tween(durationMillis = 1000)
+
+ fade(Scenes.Lockscreen.rootElementKey)
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 94d3b2c..176824f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -61,6 +61,7 @@
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.statusbar.policy.UserSwitcherController.UserSwitchCallback;
import com.android.systemui.user.data.source.UserRecord;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.settings.GlobalSettings;
@@ -70,6 +71,8 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -96,6 +99,9 @@
private UserSwitcherController mUserSwitcherController;
@Mock
private FalsingA11yDelegate mFalsingA11yDelegate;
+ @Captor
+ private ArgumentCaptor<UserSwitchCallback> mUserSwitchCallbackCaptor =
+ ArgumentCaptor.forClass(UserSwitchCallback.class);
private KeyguardSecurityContainer mKeyguardSecurityContainer;
private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
@@ -360,6 +366,18 @@
}
@Test
+ public void goingOutOfUserSwitcherRemovesCallback() {
+ // WHEN UserSwitcherViewMode is initialized
+ setupUserSwitcher();
+ verify(mUserSwitcherController).addUserSwitchCallback(mUserSwitchCallbackCaptor.capture());
+
+ // Back to default mode, as SIM PIN would be
+ initMode(MODE_DEFAULT);
+ verify(mUserSwitcherController).removeUserSwitchCallback(
+ mUserSwitchCallbackCaptor.getValue());
+ }
+
+ @Test
public void testOnDensityOrFontScaleChanged() {
setupUserSwitcher();
View oldUserSwitcher = mKeyguardSecurityContainer.findViewById(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
new file mode 100644
index 0000000..e1946fc
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
@@ -0,0 +1,323 @@
+/*
+ * 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.communal.ui.viewmodel
+
+import androidx.compose.foundation.gestures.DraggableAnchors
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ResizeableItemFrameViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val underTest = kosmos.resizeableItemFrameViewModel
+
+ /** Total viewport height of the entire grid */
+ private val viewportHeightPx = 100
+ /** Total amount of vertical padding around the viewport */
+ private val verticalContentPaddingPx = 20f
+
+ private val singleSpanGrid =
+ GridLayout(
+ verticalItemSpacingPx = 10f,
+ verticalContentPaddingPx = verticalContentPaddingPx,
+ viewportHeightPx = viewportHeightPx,
+ maxItemSpan = 1,
+ minItemSpan = 1,
+ currentSpan = 1,
+ currentRow = 0,
+ )
+
+ @Before
+ fun setUp() {
+ underTest.activateIn(testScope)
+ }
+
+ @Test
+ fun testDefaultState() {
+ val topState = underTest.topDragState
+ assertThat(topState.currentValue).isEqualTo(0)
+ assertThat(topState.offset).isEqualTo(0f)
+ assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+
+ val bottomState = underTest.bottomDragState
+ assertThat(bottomState.currentValue).isEqualTo(0)
+ assertThat(bottomState.offset).isEqualTo(0f)
+ assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
+ }
+
+ @Test
+ fun testSingleSpanGrid() =
+ testScope.runTest(timeout = Duration.INFINITE) {
+ updateGridLayout(singleSpanGrid)
+
+ val topState = underTest.topDragState
+ assertThat(topState.currentValue).isEqualTo(0)
+ assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+
+ val bottomState = underTest.bottomDragState
+ assertThat(bottomState.currentValue).isEqualTo(0)
+ assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
+ }
+
+ /**
+ * Verifies element in first row which is already at the minimum size can only be expanded
+ * downwards.
+ */
+ @Test
+ fun testTwoSpanGrid_elementInFirstRow_sizeSingleSpan() =
+ testScope.runTest {
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2))
+
+ val topState = underTest.topDragState
+ assertThat(topState.currentValue).isEqualTo(0)
+ assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+
+ val bottomState = underTest.bottomDragState
+ assertThat(bottomState.currentValue).isEqualTo(0)
+ assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 45f)
+ }
+
+ /**
+ * Verifies element in second row which is already at the minimum size can only be expanded
+ * upwards.
+ */
+ @Test
+ fun testTwoSpanGrid_elementInSecondRow_sizeSingleSpan() =
+ testScope.runTest {
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, currentRow = 1))
+
+ val topState = underTest.topDragState
+ assertThat(topState.currentValue).isEqualTo(0)
+ assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -1 to -45f)
+
+ val bottomState = underTest.bottomDragState
+ assertThat(bottomState.currentValue).isEqualTo(0)
+ assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
+ }
+
+ /**
+ * Verifies element in first row which is already at full size (2 span) can only be shrunk from
+ * the bottom.
+ */
+ @Test
+ fun testTwoSpanGrid_elementInFirstRow_sizeTwoSpan() =
+ testScope.runTest {
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, currentSpan = 2))
+
+ val topState = underTest.topDragState
+ assertThat(topState.currentValue).isEqualTo(0)
+ assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+
+ val bottomState = underTest.bottomDragState
+ assertThat(bottomState.currentValue).isEqualTo(0)
+ assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, -1 to -45f)
+ }
+
+ /**
+ * Verifies element in a middle row at minimum size can be expanded from either top or bottom.
+ */
+ @Test
+ fun testThreeSpanGrid_elementInMiddleRow_sizeOneSpan() =
+ testScope.runTest {
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 3, currentRow = 1))
+
+ val topState = underTest.topDragState
+ assertThat(topState.currentValue).isEqualTo(0)
+ assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -1 to -30f)
+
+ val bottomState = underTest.bottomDragState
+ assertThat(bottomState.currentValue).isEqualTo(0)
+ assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 30f)
+ }
+
+ @Test
+ fun testThreeSpanGrid_elementInTopRow_sizeOneSpan() =
+ testScope.runTest {
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 3))
+
+ val topState = underTest.topDragState
+ assertThat(topState.currentValue).isEqualTo(0)
+ assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+
+ val bottomState = underTest.bottomDragState
+ assertThat(bottomState.currentValue).isEqualTo(0)
+ assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 30f, 2 to 60f)
+ }
+
+ @Test
+ fun testSixSpanGrid_minSpanThree_itemInThirdRow_sizeThreeSpans() =
+ testScope.runTest {
+ updateGridLayout(
+ singleSpanGrid.copy(
+ maxItemSpan = 6,
+ currentRow = 3,
+ currentSpan = 3,
+ minItemSpan = 3,
+ )
+ )
+
+ val topState = underTest.topDragState
+ assertThat(topState.currentValue).isEqualTo(0)
+ assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -3 to -45f)
+
+ val bottomState = underTest.bottomDragState
+ assertThat(bottomState.currentValue).isEqualTo(0)
+ assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
+ }
+
+ @Test
+ fun testTwoSpanGrid_elementMovesFromFirstRowToSecondRow() =
+ testScope.runTest {
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2))
+
+ val topState = underTest.topDragState
+ val bottomState = underTest.bottomDragState
+
+ assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+ assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 45f)
+
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, currentRow = 1))
+
+ assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -1 to -45f)
+ assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
+ }
+
+ @Test
+ fun testTwoSpanGrid_expandElementFromBottom() = runTestWithSnapshots {
+ val resizeInfo by collectLastValue(underTest.resizeInfo)
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2))
+
+ assertThat(resizeInfo).isNull()
+ underTest.bottomDragState.anchoredDrag { dragTo(45f) }
+ assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.BOTTOM))
+ }
+
+ @Test
+ fun testThreeSpanGrid_expandMiddleElementUpwards() = runTestWithSnapshots {
+ val resizeInfo by collectLastValue(underTest.resizeInfo)
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 3, currentRow = 1))
+
+ assertThat(resizeInfo).isNull()
+ underTest.topDragState.anchoredDrag { dragTo(-30f) }
+ assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.TOP))
+ }
+
+ @Test
+ fun testThreeSpanGrid_expandTopElementDownBy2Spans() = runTestWithSnapshots {
+ val resizeInfo by collectLastValue(underTest.resizeInfo)
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 3))
+
+ assertThat(resizeInfo).isNull()
+ underTest.bottomDragState.anchoredDrag { dragTo(60f) }
+ assertThat(resizeInfo).isEqualTo(ResizeInfo(2, DragHandle.BOTTOM))
+ }
+
+ @Test
+ fun testTwoSpanGrid_shrinkElementFromBottom() = runTestWithSnapshots {
+ val resizeInfo by collectLastValue(underTest.resizeInfo)
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, currentSpan = 2))
+
+ assertThat(resizeInfo).isNull()
+ underTest.bottomDragState.anchoredDrag { dragTo(-45f) }
+ assertThat(resizeInfo).isEqualTo(ResizeInfo(-1, DragHandle.BOTTOM))
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun testIllegalState_maxSpanSmallerThanMinSpan() =
+ testScope.runTest {
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, minItemSpan = 3))
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun testIllegalState_minSpanOfZero() =
+ testScope.runTest {
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, minItemSpan = 0))
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun testIllegalState_maxSpanOfZero() =
+ testScope.runTest {
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 0, minItemSpan = 0))
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun testIllegalState_currentRowNotMultipleOfMinSpan() =
+ testScope.runTest {
+ updateGridLayout(singleSpanGrid.copy(maxItemSpan = 6, minItemSpan = 3, currentSpan = 2))
+ }
+
+ private fun TestScope.updateGridLayout(gridLayout: GridLayout) {
+ underTest.setGridLayoutInfo(
+ gridLayout.verticalItemSpacingPx,
+ gridLayout.verticalContentPaddingPx,
+ gridLayout.viewportHeightPx,
+ gridLayout.maxItemSpan,
+ gridLayout.minItemSpan,
+ gridLayout.currentRow,
+ gridLayout.currentSpan,
+ )
+ runCurrent()
+ }
+
+ private fun DraggableAnchors<Int>.toList() = buildList {
+ for (index in 0 until this@toList.size) {
+ add(anchorAt(index) to positionAt(index))
+ }
+ }
+
+ private fun runTestWithSnapshots(testBody: suspend TestScope.() -> Unit) {
+ val globalWriteObserverHandle =
+ Snapshot.registerGlobalWriteObserver {
+ // This is normally done by the compose runtime.
+ Snapshot.sendApplyNotifications()
+ }
+
+ try {
+ testScope.runTest(testBody = testBody)
+ } finally {
+ globalWriteObserverHandle.dispose()
+ }
+ }
+
+ private data class GridLayout(
+ val verticalItemSpacingPx: Float,
+ val verticalContentPaddingPx: Float,
+ val viewportHeightPx: Int,
+ val maxItemSpan: Int,
+ val minItemSpan: Int,
+ val currentRow: Int,
+ val currentSpan: Int,
+ )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
index d7fe263..dd83702 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
@@ -71,7 +71,7 @@
@Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper
@get:Rule val mockitoRule = MockitoJUnit.rule()
private var toastContent = ""
- private val timeoutMillis = 3500L
+ private val timeoutMillis = 5000L
@Before
fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 4ec0802..b632a8a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -176,8 +176,8 @@
sceneContainerConfig.sceneKeys
.filter { it != currentScene }
.filter {
- // Moving to the Communal scene is not currently falsing protected.
- it != Scenes.Communal
+ // Moving to the Communal and Dream scene is not currently falsing protected.
+ it != Scenes.Communal && it != Scenes.Dream
}
.forEach { toScene ->
assertWithMessage("Protected scene $toScene not properly protected")
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
index beba162..ea5c29e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
@@ -20,8 +20,6 @@
import static junit.framework.Assert.assertFalse;
-import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
-
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -31,12 +29,16 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
+
+import android.platform.test.annotations.EnableFlags;
import android.testing.TestableLooper;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.compose.animation.scene.ObservableTransitionState;
+import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.communal.shared.model.CommunalScenes;
import com.android.systemui.dump.DumpManager;
@@ -67,9 +69,6 @@
import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.time.FakeSystemClock;
-import kotlinx.coroutines.flow.MutableStateFlow;
-import kotlinx.coroutines.test.TestScope;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -79,6 +78,9 @@
import org.mockito.MockitoAnnotations;
import org.mockito.verification.VerificationMode;
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.test.TestScope;
+
@SmallTest
@RunWith(AndroidJUnit4.class)
@TestableLooper.RunWithLooper
@@ -517,6 +519,32 @@
}
@Test
+ @EnableFlags(Flags.FLAG_CHECK_LOCKSCREEN_GONE_TRANSITION)
+ public void testNotLockscreenInGoneTransition_invalidationCalled() {
+ // GIVEN visual stability is being maintained b/c animation is playing
+ mKosmos.getKeyguardTransitionRepository().sendTransitionStepJava(
+ mTestScope, new TransitionStep(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GONE,
+ 1f,
+ TransitionState.RUNNING), /* validateStep = */ false);
+ mTestScope.getTestScheduler().runCurrent();
+ assertFalse(mNotifStabilityManager.isPipelineRunAllowed());
+
+ // WHEN the animation has stopped playing
+ mKosmos.getKeyguardTransitionRepository().sendTransitionStepJava(
+ mTestScope, new TransitionStep(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GONE,
+ 1f,
+ TransitionState.FINISHED), /* validateStep = */ false);
+ mTestScope.getTestScheduler().runCurrent();
+
+ // invalidate is called, b/c we were previously suppressing the pipeline from running
+ verifyStabilityManagerWasInvalidated(times(1));
+ }
+
+ @Test
public void testNeverSuppressPipelineRunFromPanelCollapse_noInvalidationCalled() {
// GIVEN animation is playing
setPanelCollapsing(true);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelTest.kt
deleted file mode 100644
index a310ef4..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelTest.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package com.android.systemui.statusbar.notification.row.ui.viewmodel
-
-import android.platform.test.annotations.EnableFlags
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.statusbar.notification.row.data.repository.fakeNotificationRowRepository
-import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel
-import com.android.systemui.statusbar.notification.row.shared.IconModel
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-@EnableFlags(RichOngoingNotificationFlag.FLAG_NAME)
-class EnRouteViewModelTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
- private val repository = kosmos.fakeNotificationRowRepository
-
- private var contentModel: EnRouteContentModel?
- get() = repository.richOngoingContentModel.value as? EnRouteContentModel
- set(value) {
- repository.richOngoingContentModel.value = value
- }
-
- private lateinit var underTest: EnRouteViewModel
-
- @Before
- fun setup() {
- underTest = kosmos.getEnRouteViewModel(repository)
- }
-
- @Test
- fun viewModelShowsContent() =
- testScope.runTest {
- val title by collectLastValue(underTest.title)
- val text by collectLastValue(underTest.text)
- contentModel =
- exampleEnRouteContent(
- title = "Example EnRoute Title",
- text = "Example EnRoute Text",
- )
- assertThat(title).isEqualTo("Example EnRoute Title")
- assertThat(text).isEqualTo("Example EnRoute Text")
- }
-
- private fun exampleEnRouteContent(
- icon: IconModel = mock(),
- title: CharSequence = "example text",
- text: CharSequence = "example title",
- ) =
- EnRouteContentModel(
- smallIcon = icon,
- title = title,
- text = text,
- )
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
deleted file mode 100644
index 61873ad..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package com.android.systemui.statusbar.notification.row.ui.viewmodel
-
-import android.app.Notification
-import android.app.PendingIntent
-import android.platform.test.annotations.EnableFlags
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.statusbar.notification.row.data.repository.fakeNotificationRowRepository
-import com.android.systemui.statusbar.notification.row.shared.IconModel
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
-import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
-import com.android.systemui.statusbar.notification.row.shared.TimerContentModel.TimerState.Paused
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import java.time.Duration
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-@EnableFlags(RichOngoingNotificationFlag.FLAG_NAME)
-class TimerViewModelTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
- private val repository = kosmos.fakeNotificationRowRepository
-
- private var contentModel: TimerContentModel?
- get() = repository.richOngoingContentModel.value as? TimerContentModel
- set(value) {
- repository.richOngoingContentModel.value = value
- }
-
- private lateinit var underTest: TimerViewModel
-
- @Before
- fun setup() {
- underTest = kosmos.getTimerViewModel(repository)
- }
-
- @Test
- fun labelShowsTheTimerName() =
- testScope.runTest {
- val label by collectLastValue(underTest.label)
- contentModel = pausedTimer(name = "Example Timer Name")
- assertThat(label).isEqualTo("Example Timer Name")
- }
-
- @Test
- fun pausedTimeRemainingFormatsWell() =
- testScope.runTest {
- val label by collectLastValue(underTest.pausedTime)
- contentModel = pausedTimer(timeRemaining = Duration.ofMinutes(3))
- assertThat(label).isEqualTo("3:00")
- contentModel = pausedTimer(timeRemaining = Duration.ofSeconds(119))
- assertThat(label).isEqualTo("1:59")
- contentModel = pausedTimer(timeRemaining = Duration.ofSeconds(121))
- assertThat(label).isEqualTo("2:01")
- contentModel = pausedTimer(timeRemaining = Duration.ofHours(1))
- assertThat(label).isEqualTo("1:00:00")
- contentModel = pausedTimer(timeRemaining = Duration.ofHours(24))
- assertThat(label).isEqualTo("24:00:00")
- }
-
- private fun pausedTimer(
- icon: IconModel = mock(),
- name: String = "example",
- timeRemaining: Duration = Duration.ofMinutes(3),
- resumeIntent: PendingIntent? = null,
- addMinuteAction: Notification.Action? = null,
- resetAction: Notification.Action? = null
- ) =
- TimerContentModel(
- icon = icon,
- name = name,
- state =
- Paused(
- timeRemaining = timeRemaining,
- resumeIntent = resumeIntent,
- addMinuteAction = addMinuteAction,
- resetAction = resetAction,
- )
- )
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt
new file mode 100644
index 0000000..7ce421a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import android.app.ActivityManager
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.fakeVolumeDialogController
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.days
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val dialogTimeoutDuration = 3.seconds
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper()
+class VolumeDialogVisibilityInteractorTest : SysuiTestCase() {
+
+ private val kosmos: Kosmos = testKosmos()
+
+ private lateinit var underTest: VolumeDialogVisibilityInteractor
+
+ @Before
+ fun setUp() {
+ underTest = kosmos.volumeDialogVisibilityInteractor
+ }
+
+ @Test
+ fun testShowRequest_visible() =
+ with(kosmos) {
+ testScope.runTest {
+ runCurrent()
+ val visibilityModel by collectLastValue(underTest.dialogVisibility)
+ fakeVolumeDialogController.onShowRequested(
+ Events.SHOW_REASON_VOLUME_CHANGED,
+ false,
+ ActivityManager.LOCK_TASK_MODE_LOCKED,
+ )
+ runCurrent()
+
+ assertThat(visibilityModel!!)
+ .isEqualTo(
+ VolumeDialogVisibilityModel.Visible(
+ Events.SHOW_REASON_VOLUME_CHANGED,
+ false,
+ ActivityManager.LOCK_TASK_MODE_LOCKED,
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDismissRequest_dismissed() =
+ with(kosmos) {
+ testScope.runTest {
+ runCurrent()
+ val visibilityModel by collectLastValue(underTest.dialogVisibility)
+ fakeVolumeDialogController.onShowRequested(
+ Events.SHOW_REASON_VOLUME_CHANGED,
+ false,
+ ActivityManager.LOCK_TASK_MODE_LOCKED,
+ )
+ runCurrent()
+
+ fakeVolumeDialogController.onDismissRequested(Events.DISMISS_REASON_SCREEN_OFF)
+
+ assertThat(visibilityModel!!)
+ .isEqualTo(
+ VolumeDialogVisibilityModel.Dismissed(Events.DISMISS_REASON_SCREEN_OFF)
+ )
+ }
+ }
+
+ @Test
+ fun testTimeout_dismissed() =
+ with(kosmos) {
+ testScope.runTest {
+ runCurrent()
+ underTest.resetDismissTimeout()
+ val visibilityModel by collectLastValue(underTest.dialogVisibility)
+ fakeVolumeDialogController.onShowRequested(
+ Events.SHOW_REASON_VOLUME_CHANGED,
+ false,
+ ActivityManager.LOCK_TASK_MODE_LOCKED,
+ )
+ runCurrent()
+
+ advanceTimeBy(1.days)
+
+ assertThat(visibilityModel!!)
+ .isEqualTo(VolumeDialogVisibilityModel.Dismissed(Events.DISMISS_REASON_TIMEOUT))
+ }
+ }
+
+ @Test
+ fun testResetTimeoutInterruptsEvents() =
+ with(kosmos) {
+ testScope.runTest {
+ runCurrent()
+ underTest.resetDismissTimeout()
+ val visibilityModel by collectLastValue(underTest.dialogVisibility)
+ fakeVolumeDialogController.onShowRequested(
+ Events.SHOW_REASON_VOLUME_CHANGED,
+ false,
+ ActivityManager.LOCK_TASK_MODE_LOCKED,
+ )
+ runCurrent()
+
+ advanceTimeBy(dialogTimeoutDuration / 2)
+ underTest.resetDismissTimeout()
+ advanceTimeBy(dialogTimeoutDuration / 2)
+ underTest.resetDismissTimeout()
+ advanceTimeBy(dialogTimeoutDuration / 2)
+
+ assertThat(visibilityModel)
+ .isInstanceOf(VolumeDialogVisibilityModel.Visible::class.java)
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt
index e1be6b0..d2688a8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt
@@ -53,7 +53,7 @@
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.kotlin.JavaAdapter
import com.android.wm.shell.desktopmode.DesktopMode
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener
+import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener
import com.android.wm.shell.onehanded.OneHanded
import com.android.wm.shell.onehanded.OneHandedEventCallback
import com.android.wm.shell.onehanded.OneHandedTransitionCallback
diff --git a/packages/SystemUI/res-keyguard/layout-land/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout-land/keyguard_pattern_view.xml
new file mode 100644
index 0000000..8a77d88
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout-land/keyguard_pattern_view.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+
+<!-- This is the screen that shows the 9 circle unlock widget and instructs
+ the user how to unlock their device, or make an emergency call. This
+ is the landscape layout. -->
+<com.android.keyguard.KeyguardPatternView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/keyguard_pattern_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_horizontal|bottom"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="horizontal">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="2"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:layoutDirection="ltr"
+ android:orientation="vertical">
+
+ <include layout="@layout/keyguard_bouncer_message_area"/>
+
+ <com.android.systemui.bouncer.ui.BouncerMessageView
+ android:id="@+id/bouncer_message_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ androidprv:layout_constraintBottom_toTopOf="@+id/lockPatternView"
+ androidprv:layout_constraintTop_toTopOf="parent"
+ androidprv:layout_constraintVertical_chainStyle="packed" />
+
+ <include
+ android:id="@+id/keyguard_selector_fade_container"
+ layout="@layout/keyguard_eca"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|center_horizontal"
+ android:layout_marginBottom="@dimen/keyguard_eca_bottom_margin"
+ android:layout_marginTop="@dimen/keyguard_eca_top_margin"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ androidprv:layout_constraintBottom_toBottomOf="parent" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/pattern_container"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="3"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:layoutDirection="ltr"
+ android:orientation="vertical">
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/pattern_top_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ androidprv:layout_constraintGuide_percent="0" />
+
+ <com.android.internal.widget.LockPatternView
+ android:id="@+id/lockPatternView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="horizontal"
+ androidprv:layout_constraintDimensionRatio="1.0"
+ androidprv:layout_constraintVertical_bias="1.0"
+ androidprv:layout_constraintLeft_toLeftOf="parent"
+ androidprv:layout_constraintRight_toRightOf="parent"
+ androidprv:layout_constraintBottom_toBottomOf="parent"
+ androidprv:layout_constraintTop_toBottomOf="@id/pattern_top_guideline"/>
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</com.android.keyguard.KeyguardPatternView>
diff --git a/packages/SystemUI/res-keyguard/layout-land/keyguard_sim_pin_view.xml b/packages/SystemUI/res-keyguard/layout-land/keyguard_sim_pin_view.xml
new file mode 100644
index 0000000..4b8b63f
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout-land/keyguard_sim_pin_view.xml
@@ -0,0 +1,221 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<!-- This is the SIM PIN view that allows the user to enter a SIM PIN to unlock the device. -->
+<com.android.keyguard.KeyguardSimPinView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/res-auto"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/keyguard_sim_pin_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_horizontal|bottom"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="horizontal">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="2"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:layoutDirection="ltr"
+ android:orientation="vertical">
+
+ <include layout="@layout/keyguard_bouncer_message_area"/>
+
+ <ImageView
+ android:id="@+id/keyguard_sim"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_marginBottom="3dp"
+ android:src="@drawable/ic_lockscreen_sim"
+ androidprv:layout_constraintBottom_toTopOf="@+id/pin_entry_area"
+ androidprv:layout_constraintLeft_toLeftOf="parent"
+ androidprv:layout_constraintRight_toRightOf="parent"
+ app:tint="@color/background_protected"/>
+
+ <include
+ android:id="@+id/keyguard_esim_area"
+ layout="@layout/keyguard_esim_area"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ androidprv:layout_constraintLeft_toLeftOf="parent"
+ androidprv:layout_constraintRight_toRightOf="parent"
+ androidprv:layout_constraintBottom_toTopOf="@+id/pin_entry_area"/>
+
+ <com.android.keyguard.AlphaOptimizedRelativeLayout
+ android:id="@+id/pin_entry_area"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/num_pad_entry_row_margin_bottom"
+ androidprv:layout_constraintBottom_toTopOf="@+id/keyguard_selector_fade_container">
+
+ <com.android.keyguard.PasswordTextView
+ android:id="@+id/simPinEntry"
+ style="@style/Widget.TextView.Password"
+ android:layout_width="@dimen/keyguard_security_width"
+ android:layout_height="@dimen/keyguard_password_height"
+ android:layout_centerHorizontal="true"
+ android:layout_marginRight="72dp"
+ android:contentDescription="@string/keyguard_accessibility_sim_pin_area"
+ androidprv:scaledTextSize="@integer/scaled_password_text_size" />
+ </com.android.keyguard.AlphaOptimizedRelativeLayout>
+
+ <include
+ android:id="@+id/keyguard_selector_fade_container"
+ layout="@layout/keyguard_eca"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|center_horizontal"
+ android:layout_marginBottom="@dimen/keyguard_eca_bottom_margin"
+ android:layout_marginTop="@dimen/keyguard_eca_top_margin"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ androidprv:layout_constraintBottom_toBottomOf="parent"/>
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/sim_pin_container"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="3"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:layoutDirection="ltr"
+ android:orientation="vertical">
+
+ <androidx.constraintlayout.helper.widget.Flow
+ android:id="@+id/flow1"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="horizontal"
+ androidprv:constraint_referenced_ids="key1,key2,key3,key4,key5,key6,key7,key8,key9,delete_button,key0,key_enter"
+ androidprv:flow_horizontalGap="@dimen/num_pad_key_margin_end"
+ androidprv:flow_horizontalStyle="packed"
+ androidprv:flow_maxElementsWrap="3"
+ androidprv:flow_verticalBias="0.5"
+ androidprv:flow_verticalGap="@dimen/num_pad_entry_row_margin_bottom"
+ androidprv:flow_verticalStyle="packed"
+ androidprv:flow_wrapMode="aligned"
+ androidprv:layout_constraintLeft_toLeftOf="parent"
+ androidprv:layout_constraintRight_toRightOf="parent"
+ androidprv:layout_constraintTop_toTopOf="parent"
+ androidprv:layout_constraintBottom_toBottomOf="parent"/>
+
+ <com.android.keyguard.NumPadButton
+ android:id="@+id/delete_button"
+ style="@style/NumPadKey.Delete"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key0"
+ android:contentDescription="@string/keyboardview_keycode_delete" />
+
+ <com.android.keyguard.NumPadButton
+ android:id="@+id/key_enter"
+ style="@style/NumPadKey.Enter"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:contentDescription="@string/keyboardview_keycode_enter" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key1"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key2"
+ androidprv:digit="1"
+ androidprv:textView="@+id/simPinEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key2"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key3"
+ androidprv:digit="2"
+ androidprv:textView="@+id/simPinEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key3"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key4"
+ androidprv:digit="3"
+ androidprv:textView="@+id/simPinEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key4"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key5"
+ androidprv:digit="4"
+ androidprv:textView="@+id/simPinEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key5"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key6"
+ androidprv:digit="5"
+ androidprv:textView="@+id/simPinEntry" />
+
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key6"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key7"
+ androidprv:digit="6"
+ androidprv:textView="@+id/simPinEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key7"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key8"
+ androidprv:digit="7"
+ androidprv:textView="@+id/simPinEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key8"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key9"
+ androidprv:digit="8"
+ androidprv:textView="@+id/simPinEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key9"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/delete_button"
+ androidprv:digit="9"
+ androidprv:textView="@+id/simPinEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key0"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key_enter"
+ androidprv:digit="0"
+ androidprv:textView="@+id/simPinEntry" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</com.android.keyguard.KeyguardSimPinView>
diff --git a/packages/SystemUI/res-keyguard/layout-land/keyguard_sim_puk_view.xml b/packages/SystemUI/res-keyguard/layout-land/keyguard_sim_puk_view.xml
new file mode 100644
index 0000000..9012856
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout-land/keyguard_sim_puk_view.xml
@@ -0,0 +1,223 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<!-- This is the SIM PUK view that allows the user to recover their device by entering the
+ carrier-provided PUK code and entering a new SIM PIN for it. -->
+<com.android.keyguard.KeyguardSimPukView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/res-auto"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/keyguard_sim_puk_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_horizontal|bottom"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="horizontal">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="2"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:layoutDirection="ltr"
+ android:orientation="vertical">
+
+ <include layout="@layout/keyguard_bouncer_message_area"/>
+
+ <ImageView
+ android:id="@+id/keyguard_sim"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_marginBottom="3dp"
+ android:src="@drawable/ic_lockscreen_sim"
+ androidprv:layout_constraintBottom_toTopOf="@+id/pin_entry_area"
+ androidprv:layout_constraintLeft_toLeftOf="parent"
+ androidprv:layout_constraintRight_toRightOf="parent"
+ app:tint="@color/background_protected"/>
+
+ <include
+ android:id="@+id/keyguard_esim_area"
+ layout="@layout/keyguard_esim_area"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ androidprv:layout_constraintLeft_toLeftOf="parent"
+ androidprv:layout_constraintRight_toRightOf="parent"
+ androidprv:layout_constraintBottom_toTopOf="@+id/pin_entry_area"/>
+
+ <com.android.keyguard.AlphaOptimizedRelativeLayout
+ android:id="@+id/pin_entry_area"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/num_pad_entry_row_margin_bottom"
+ androidprv:layout_constraintBottom_toTopOf="@+id/keyguard_selector_fade_container">
+
+ <com.android.keyguard.PasswordTextView
+ android:id="@+id/pukEntry"
+ style="@style/Widget.TextView.Password"
+ android:layout_width="@dimen/keyguard_security_width"
+ android:layout_height="@dimen/keyguard_password_height"
+ android:layout_centerHorizontal="true"
+ android:layout_marginRight="72dp"
+ android:contentDescription="@string/keyguard_accessibility_sim_pin_area"
+ androidprv:scaledTextSize="@integer/scaled_password_text_size" />
+ </com.android.keyguard.AlphaOptimizedRelativeLayout>
+
+ <include
+ android:id="@+id/keyguard_selector_fade_container"
+ layout="@layout/keyguard_eca"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|center_horizontal"
+ android:layout_marginBottom="@dimen/keyguard_eca_bottom_margin"
+ android:layout_marginTop="@dimen/keyguard_eca_top_margin"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ androidprv:layout_constraintBottom_toBottomOf="parent"/>
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/sim_puk_container"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="3"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:layoutDirection="ltr"
+ android:orientation="vertical">
+
+ <androidx.constraintlayout.helper.widget.Flow
+ android:id="@+id/flow1"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="horizontal"
+ androidprv:constraint_referenced_ids="key1,key2,key3,key4,key5,key6,key7,key8,key9,delete_button,key0,key_enter"
+ androidprv:flow_horizontalGap="@dimen/num_pad_key_margin_end"
+ androidprv:flow_horizontalStyle="packed"
+ androidprv:flow_maxElementsWrap="3"
+ androidprv:flow_verticalBias="0.5"
+ androidprv:flow_verticalGap="@dimen/num_pad_entry_row_margin_bottom"
+ androidprv:flow_verticalStyle="packed"
+ androidprv:flow_wrapMode="aligned"
+ androidprv:layout_constraintLeft_toLeftOf="parent"
+ androidprv:layout_constraintRight_toRightOf="parent"
+ androidprv:layout_constraintTop_toTopOf="parent"
+ androidprv:layout_constraintBottom_toBottomOf="parent"/>
+
+ <com.android.keyguard.NumPadButton
+ android:id="@+id/delete_button"
+ style="@style/NumPadKey.Delete"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key0"
+ android:contentDescription="@string/keyboardview_keycode_delete" />
+
+ <com.android.keyguard.NumPadButton
+ android:id="@+id/key_enter"
+ style="@style/NumPadKey.Enter"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:contentDescription="@string/keyboardview_keycode_enter" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key1"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key2"
+ androidprv:digit="1"
+ androidprv:textView="@+id/pukEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key2"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key3"
+ androidprv:digit="2"
+ androidprv:textView="@+id/pukEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key3"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key4"
+ androidprv:digit="3"
+ androidprv:textView="@+id/pukEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key4"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key5"
+ androidprv:digit="4"
+ androidprv:textView="@+id/pukEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key5"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key6"
+ androidprv:digit="5"
+ androidprv:textView="@+id/pukEntry" />
+
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key6"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key7"
+ androidprv:digit="6"
+ androidprv:textView="@+id/pukEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key7"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key8"
+ androidprv:digit="7"
+ androidprv:textView="@+id/pukEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key8"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key9"
+ androidprv:digit="8"
+ androidprv:textView="@+id/pukEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key9"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/delete_button"
+ androidprv:digit="9"
+ androidprv:textView="@+id/pukEntry" />
+
+ <com.android.keyguard.NumPadKey
+ android:id="@+id/key0"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key_enter"
+ androidprv:digit="0"
+ androidprv:textView="@+id/pukEntry" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</com.android.keyguard.KeyguardSimPukView>
diff --git a/packages/SystemUI/res/drawable/touchpad_tutorial_home_icon.xml b/packages/SystemUI/res/drawable/touchpad_tutorial_home_icon.xml
new file mode 100644
index 0000000..9f3d075
--- /dev/null
+++ b/packages/SystemUI/res/drawable/touchpad_tutorial_home_icon.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ 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="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportHeight="960"
+ android:viewportWidth="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M240,760L360,760L360,520L600,520L600,760L720,760L720,400L480,220L240,400L240,760ZM160,840L160,360L480,120L800,360L800,840L520,840L520,600L440,600L440,840L160,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z" />
+</vector>
diff --git a/packages/SystemUI/res/drawable/touchpad_tutorial_recents_icon.xml b/packages/SystemUI/res/drawable/touchpad_tutorial_recents_icon.xml
new file mode 100644
index 0000000..113908a
--- /dev/null
+++ b/packages/SystemUI/res/drawable/touchpad_tutorial_recents_icon.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ 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="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="?attr/colorControlNormal"
+ android:viewportHeight="960"
+ android:viewportWidth="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M120,800Q87,800 63.5,776.5Q40,753 40,720L40,240Q40,207 63.5,183.5Q87,160 120,160L200,160Q233,160 256.5,183.5Q280,207 280,240L280,720Q280,753 256.5,776.5Q233,800 200,800L120,800ZM120,721L200,721Q200,721 200,721Q200,721 200,721L200,239Q200,239 200,239Q200,239 200,239L120,239Q120,239 120,239Q120,239 120,239L120,721Q120,721 120,721Q120,721 120,721ZM440,800Q407,800 383.5,776.5Q360,753 360,720L360,240Q360,207 383.5,183.5Q407,160 440,160L840,160Q873,160 896.5,183.5Q920,207 920,240L920,720Q920,753 896.5,776.5Q873,800 840,800L440,800ZM440,721L840,721Q840,721 840,721Q840,721 840,721L840,239Q840,239 840,239Q840,239 840,239L440,239Q440,239 440,239Q440,239 440,239L440,721Q440,721 440,721Q440,721 440,721ZM200,721Q200,721 200,721Q200,721 200,721L200,239Q200,239 200,239Q200,239 200,239L200,239Q200,239 200,239Q200,239 200,239L200,721Q200,721 200,721Q200,721 200,721ZM440,721Q440,721 440,721Q440,721 440,721L440,239Q440,239 440,239Q440,239 440,239L440,239Q440,239 440,239Q440,239 440,239L440,721Q440,721 440,721Q440,721 440,721Z" />
+</vector>
diff --git a/packages/SystemUI/res/layout/notification_template_en_route_contracted.xml b/packages/SystemUI/res/layout/notification_template_en_route_contracted.xml
deleted file mode 100644
index e7a40d1..0000000
--- a/packages/SystemUI/res/layout/notification_template_en_route_contracted.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?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.
- -->
-
-<com.android.systemui.statusbar.notification.row.ui.view.EnRouteView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@*android:id/status_bar_latest_event_content"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:minHeight="@*android:dimen/notification_headerless_min_height"
- android:tag="enroute"
- >
-
- <include layout="@*android:layout/notification_template_material_base" />
-
-</com.android.systemui.statusbar.notification.row.ui.view.EnRouteView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/notification_template_en_route_expanded.xml b/packages/SystemUI/res/layout/notification_template_en_route_expanded.xml
deleted file mode 100644
index ca6d66a..0000000
--- a/packages/SystemUI/res/layout/notification_template_en_route_expanded.xml
+++ /dev/null
@@ -1,86 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright (C) 2014 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
- -->
-<com.android.systemui.statusbar.notification.row.ui.view.EnRouteView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@*android:id/status_bar_latest_event_content"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:clipChildren="false"
- android:tag="big"
- >
-
- <LinearLayout
- android:id="@*android:id/notification_action_list_margin_target"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="@*android:dimen/notification_content_margin"
- android:orientation="vertical"
- >
-
- <FrameLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:layout_gravity="top"
- >
-
- <include layout="@*android:layout/notification_template_header" />
-
- <LinearLayout
- android:id="@*android:id/notification_main_column"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="@*android:dimen/notification_content_margin_start"
- android:layout_marginEnd="@*android:dimen/notification_content_margin_end"
- android:layout_marginTop="@*android:dimen/notification_content_margin_top"
- android:orientation="vertical"
- >
-
- <include layout="@*android:layout/notification_template_part_line1" />
-
- <include layout="@*android:layout/notification_template_text_multiline" />
-
- <include
- android:layout_width="match_parent"
- android:layout_height="@*android:dimen/notification_progress_bar_height"
- android:layout_marginTop="@*android:dimen/notification_progress_margin_top"
- layout="@*android:layout/notification_template_progress"
- />
- </LinearLayout>
-
- <include layout="@*android:layout/notification_template_right_icon" />
- </FrameLayout>
-
- <ViewStub
- android:layout="@*android:layout/notification_material_reply_text"
- android:id="@*android:id/notification_material_reply_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- />
-
- <include
- layout="@*android:layout/notification_template_smart_reply_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="@*android:dimen/notification_content_margin_start"
- android:layout_marginEnd="@*android:dimen/notification_content_margin_end"
- android:layout_marginTop="@*android:dimen/notification_content_margin"
- />
-
- <include layout="@*android:layout/notification_material_action_list" />
- </LinearLayout>
-</com.android.systemui.statusbar.notification.row.ui.view.EnRouteView>
diff --git a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
deleted file mode 100644
index 3a679e3..0000000
--- a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
+++ /dev/null
@@ -1,122 +0,0 @@
-<?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
- -->
-<com.android.systemui.statusbar.notification.row.ui.view.TimerView
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- xmlns:app="http://schemas.android.com/apk/res-auto">
-
- <androidx.constraintlayout.widget.Guideline
- android:id="@+id/topBaseline"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- app:layout_constraintGuide_begin="22sp"
- />
-
- <ImageView
- android:id="@+id/icon"
- android:layout_width="24dp"
- android:layout_height="24dp"
- app:tint="@android:color/white"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@id/label"
- android:baseline="18dp"
- app:layout_constraintBaseline_toTopOf="@id/topBaseline"
- />
- <TextView
- android:id="@+id/label"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:layout_constraintStart_toEndOf="@id/icon"
- app:layout_constraintEnd_toStartOf="@id/chronoRemaining"
- android:singleLine="true"
- tools:text="15s Timer"
- app:layout_constraintBaseline_toTopOf="@id/topBaseline"
- android:paddingEnd="4dp"
- />
- <Chronometer
- android:id="@+id/chronoRemaining"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:singleLine="true"
- android:textSize="20sp"
- android:gravity="end"
- tools:text="0:12"
- app:layout_constraintBaseline_toTopOf="@id/topBaseline"
- app:layout_constraintEnd_toStartOf="@id/pausedTimeRemaining"
- app:layout_constraintStart_toEndOf="@id/label"
- android:countDown="true"
- android:paddingEnd="4dp"
- />
- <TextView
- android:id="@+id/pausedTimeRemaining"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:singleLine="true"
- android:textSize="20sp"
- android:gravity="end"
- tools:text="0:12"
- app:layout_constraintBaseline_toTopOf="@id/topBaseline"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@id/chronoRemaining"
- android:paddingEnd="4dp"
- />
-
- <androidx.constraintlayout.widget.Barrier
- android:id="@+id/bottomOfTop"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:barrierDirection="bottom"
- app:constraint_referenced_ids="icon,label,chronoRemaining,pausedTimeRemaining"
- />
-
- <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
- style="@*android:style/NotificationEmphasizedAction"
- android:id="@+id/mainButton"
- android:layout_width="124dp"
- android:layout_height="wrap_content"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@id/altButton"
- app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
- app:layout_constraintHorizontal_chainStyle="spread"
- android:paddingEnd="4dp"
- />
-
- <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
- style="@*android:style/NotificationEmphasizedAction"
- android:id="@+id/altButton"
- android:layout_width="124dp"
- android:layout_height="wrap_content"
- app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
- app:layout_constraintStart_toEndOf="@id/mainButton"
- app:layout_constraintEnd_toEndOf="@id/resetButton"
- android:paddingEnd="4dp"
- />
-
- <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
- style="@*android:style/NotificationEmphasizedAction"
- android:id="@+id/resetButton"
- android:layout_width="124dp"
- android:layout_height="wrap_content"
- app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
- app:layout_constraintStart_toEndOf="@id/altButton"
- app:layout_constraintEnd_toEndOf="parent"
- android:paddingEnd="4dp"
- />
-</com.android.systemui.statusbar.notification.row.ui.view.TimerView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2ddaa56..96a85d7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3752,9 +3752,9 @@
<!-- TOUCHPAD TUTORIAL-->
<!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] -->
- <string name="touchpad_tutorial_back_gesture_button">Back gesture</string>
+ <string name="touchpad_tutorial_back_gesture_button">Go back</string>
<!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] -->
- <string name="touchpad_tutorial_home_gesture_button">Home gesture</string>
+ <string name="touchpad_tutorial_home_gesture_button">Go home</string>
<!-- Label for button opening tutorial for "view recent apps" gesture on touchpad [CHAR LIMIT=NONE] -->
<string name="touchpad_tutorial_recent_apps_gesture_button">View recent apps</string>
<!-- Label for button finishing touchpad tutorial [CHAR LIMIT=NONE] -->
@@ -3763,26 +3763,25 @@
<!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] -->
<string name="touchpad_back_gesture_action_title">Go back</string>
<!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
- <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.\n\nYou can also use the keyboard shortcut
-Action + ESC for this.</string>
+ <string name="touchpad_back_gesture_guidance">Swipe left or right using three fingers on your touchpad</string>
<!-- Screen title after back gesture was done successfully [CHAR LIMIT=NONE] -->
- <string name="touchpad_back_gesture_success_title">Great job!</string>
+ <string name="touchpad_back_gesture_success_title">Nice!</string>
<!-- Text shown to the user after they complete back gesture tutorial [CHAR LIMIT=NONE] -->
<string name="touchpad_back_gesture_success_body">You completed the go back gesture.</string>
<!-- HOME GESTURE -->
<!-- Touchpad home gesture action name in tutorial [CHAR LIMIT=NONE] -->
<string name="touchpad_home_gesture_action_title">Go home</string>
<!-- Touchpad home gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
- <string name="touchpad_home_gesture_guidance">To go to your home screen at any time, swipe up with three fingers from the bottom of your screen.</string>
+ <string name="touchpad_home_gesture_guidance">Swipe up with three fingers on your touchpad</string>
<!-- Screen title after home gesture was done successfully [CHAR LIMIT=NONE] -->
- <string name="touchpad_home_gesture_success_title">Nice!</string>
+ <string name="touchpad_home_gesture_success_title">Great job!</string>
<!-- Text shown to the user after they complete home gesture tutorial [CHAR LIMIT=NONE] -->
- <string name="touchpad_home_gesture_success_body">You completed the go home gesture.</string>
+ <string name="touchpad_home_gesture_success_body">You completed the go home gesture</string>
<!-- RECENT APPS GESTURE -->
<!-- Touchpad recent apps gesture action name in tutorial [CHAR LIMIT=NONE] -->
<string name="touchpad_recent_apps_gesture_action_title">View recent apps</string>
<!-- Touchpad recent apps gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
- <string name="touchpad_recent_apps_gesture_guidance">Swipe up and hold using three fingers on your touchpad.</string>
+ <string name="touchpad_recent_apps_gesture_guidance">Swipe up and hold using three fingers on your touchpad</string>
<!-- Screen title after recent apps gesture was done successfully [CHAR LIMIT=NONE] -->
<string name="touchpad_recent_apps_gesture_success_title">Great job!</string>
<!-- Text shown to the user after they complete recent apps gesture tutorial [CHAR LIMIT=NONE] -->
@@ -3790,13 +3789,13 @@
<!-- KEYBOARD TUTORIAL-->
<!-- Action key tutorial title [CHAR LIMIT=NONE] -->
- <string name="tutorial_action_key_title">Action key</string>
+ <string name="tutorial_action_key_title">View all apps</string>
<!-- Action key tutorial guidance[CHAR LIMIT=NONE] -->
- <string name="tutorial_action_key_guidance">To access your apps, press the action key on your keyboard.</string>
+ <string name="tutorial_action_key_guidance">Press the action key on your keyboard</string>
<!-- Screen title after action key pressed successfully [CHAR LIMIT=NONE] -->
- <string name="tutorial_action_key_success_title">Congratulations!</string>
+ <string name="tutorial_action_key_success_title">Well done!</string>
<!-- Text shown to the user after they complete action key tutorial [CHAR LIMIT=NONE] -->
- <string name="tutorial_action_key_success_body">You completed the action key gesture.\n\nAction + / shows all the shortcuts you have available.</string>
+ <string name="tutorial_action_key_success_body">You completed the view all apps gesture</string>
<!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] -->
<string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 1c09f84..94b0b5f 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -540,6 +540,7 @@
<!-- Overridden by values-television/styles.xml with tv-specific settings -->
<style name="volume_dialog_theme" parent="Theme.SystemUI">
<item name="android:windowIsFloating">true</item>
+ <item name="android:showWhenLocked">true</item>
</style>
<style name="Theme.SystemUI.DayNightDialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog"/>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index f05cbf4..2d27f1c0 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -1067,6 +1067,8 @@
@Override
public void onDestroy() {
+ mUserSwitcherController.removeUserSwitchCallback(mUserSwitchCallback);
+
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(mView);
constraintSet.clear(mUserSwitcherViewGroup.getId());
@@ -1075,6 +1077,8 @@
mView.removeView(mUserSwitcherViewGroup);
mView.removeView(mUserSwitcher);
+ mUserSwitcher = null;
+ mUserSwitcherViewGroup = null;
}
private void findLargeUserIcon(int userId, Consumer<Drawable> consumer) {
@@ -1102,6 +1106,10 @@
return;
}
+ if (mUserSwitcherViewGroup == null) {
+ return;
+ }
+
mUserSwitcherViewGroup.setAlpha(0f);
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
int yTrans = mView.getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry);
@@ -1110,14 +1118,18 @@
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- mUserSwitcherViewGroup.setAlpha(1f);
- mUserSwitcherViewGroup.setTranslationY(0f);
+ if (mUserSwitcherViewGroup != null) {
+ mUserSwitcherViewGroup.setAlpha(1f);
+ mUserSwitcherViewGroup.setTranslationY(0f);
+ }
}
});
animator.addUpdateListener(animation -> {
- float value = (float) animation.getAnimatedValue();
- mUserSwitcherViewGroup.setAlpha(value);
- mUserSwitcherViewGroup.setTranslationY(yTrans - yTrans * value);
+ if (mUserSwitcherViewGroup != null) {
+ float value = (float) animation.getAnimatedValue();
+ mUserSwitcherViewGroup.setAlpha(value);
+ mUserSwitcherViewGroup.setTranslationY(yTrans - yTrans * value);
+ }
});
animator.start();
}
@@ -1148,6 +1160,10 @@
Log.e(TAG, "Current user in user switcher is null.");
return;
}
+ if (mUserSwitcher == null) {
+ Log.w(TAG, "User switcher is not inflated, cannot setupUserSwitcher");
+ return;
+ }
final String currentUserName = mUserSwitcherController.getCurrentUserName();
findLargeUserIcon(currentUser.info.id,
(Drawable userIcon) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 60edaae..158623f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -280,6 +280,14 @@
if (mLocalBluetoothManager == null) {
return;
}
+
+ // Remove the default padding of the system ui dialog
+ View container = dialog.findViewById(android.R.id.custom);
+ if (container != null && container.getParent() != null) {
+ View containerParent = (View) container.getParent();
+ containerParent.setPadding(0, 0, 0, 0);
+ }
+
mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DIALOG_SHOW, mLaunchSourceId);
mPairButton = dialog.requireViewById(R.id.pair_new_device_button);
mDeviceList = dialog.requireViewById(R.id.device_list);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
index 664f3f8..9367cb5 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
@@ -108,6 +108,7 @@
private final ImageView mIconView;
private final ImageView mGearIcon;
private final View mGearView;
+ private final View mDividerView;
DeviceItemViewHolder(@NonNull View itemView, Context context) {
super(itemView);
@@ -118,6 +119,7 @@
mIconView = itemView.requireViewById(R.id.bluetooth_device_icon);
mGearIcon = itemView.requireViewById(R.id.gear_icon_image);
mGearView = itemView.requireViewById(R.id.gear_icon);
+ mDividerView = itemView.requireViewById(R.id.divider);
}
public void bindView(DeviceItem item, HearingDeviceItemCallback callback) {
@@ -153,6 +155,7 @@
mGearIcon.getDrawable().mutate().setTint(tintColor);
mGearView.setOnClickListener(view -> callback.onDeviceItemGearClicked(item, view));
+ mDividerView.setBackgroundColor(tintColor);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
new file mode 100644
index 0000000..7aad33d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.communal.ui.viewmodel
+
+import androidx.compose.foundation.gestures.AnchoredDraggableState
+import androidx.compose.foundation.gestures.DraggableAnchors
+import androidx.compose.runtime.snapshotFlow
+import com.android.app.tracing.coroutines.coroutineScope
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+
+enum class DragHandle {
+ TOP,
+ BOTTOM,
+}
+
+data class ResizeInfo(
+ /**
+ * The number of spans to resize by. A positive number indicates expansion, whereas a negative
+ * number indicates shrinking.
+ */
+ val spans: Int,
+ /** The drag handle which was used to resize the element. */
+ val fromHandle: DragHandle,
+)
+
+class ResizeableItemFrameViewModel : ExclusiveActivatable() {
+ private data class GridLayoutInfo(
+ val minSpan: Int,
+ val maxSpan: Int,
+ val heightPerSpanPx: Float,
+ val verticalItemSpacingPx: Float,
+ val currentRow: Int,
+ val currentSpan: Int,
+ )
+
+ /**
+ * The layout information necessary in order to calculate the pixel offsets of the drag anchor
+ * points.
+ */
+ private val gridLayoutInfo = MutableStateFlow<GridLayoutInfo?>(null)
+
+ val topDragState = AnchoredDraggableState(0, DraggableAnchors { 0 at 0f })
+ val bottomDragState = AnchoredDraggableState(0, DraggableAnchors { 0 at 0f })
+
+ /** Emits a [ResizeInfo] when the element is resized using a drag gesture. */
+ val resizeInfo: Flow<ResizeInfo> =
+ merge(
+ snapshotFlow { topDragState.settledValue }.map { ResizeInfo(-it, DragHandle.TOP) },
+ snapshotFlow { bottomDragState.settledValue }
+ .map { ResizeInfo(it, DragHandle.BOTTOM) },
+ )
+ .dropWhile { it.spans == 0 }
+ .distinctUntilChanged()
+
+ /**
+ * Sets the necessary grid layout information needed for calculating the pixel offsets of the
+ * drag anchors.
+ */
+ fun setGridLayoutInfo(
+ verticalItemSpacingPx: Float,
+ verticalContentPaddingPx: Float,
+ viewportHeightPx: Int,
+ maxItemSpan: Int,
+ minItemSpan: Int,
+ currentRow: Int,
+ currentSpan: Int,
+ ) {
+ require(maxItemSpan >= minItemSpan) {
+ "Maximum item span of $maxItemSpan cannot be less than the minimum span of $minItemSpan"
+ }
+ require(minItemSpan in 1..maxItemSpan) {
+ "Minimum span must be between 1 and $maxItemSpan, but was $minItemSpan"
+ }
+ require(currentSpan % minItemSpan == 0) {
+ "Current span of $currentSpan is not a multiple of the minimum span of $minItemSpan"
+ }
+ val availableHeight = viewportHeightPx - verticalContentPaddingPx
+ val totalSpacing = verticalItemSpacingPx * ((maxItemSpan / minItemSpan) - 1)
+ val heightPerSpanPx = (availableHeight - totalSpacing) / maxItemSpan
+ gridLayoutInfo.value =
+ GridLayoutInfo(
+ minSpan = minItemSpan,
+ maxSpan = maxItemSpan,
+ heightPerSpanPx = heightPerSpanPx,
+ verticalItemSpacingPx = verticalItemSpacingPx,
+ currentRow = currentRow,
+ currentSpan = currentSpan,
+ )
+ }
+
+ private fun calculateAnchorsForHandle(
+ handle: DragHandle,
+ layoutInfo: GridLayoutInfo,
+ ): DraggableAnchors<Int> {
+
+ if (!isDragAllowed(handle, layoutInfo)) {
+ return DraggableAnchors { 0 at 0f }
+ }
+
+ val (
+ minItemSpan,
+ maxItemSpan,
+ heightPerSpanPx,
+ verticalSpacingPx,
+ currentRow,
+ currentSpan,
+ ) = layoutInfo
+
+ // The maximum row this handle can be dragged to.
+ val maxRow =
+ if (handle == DragHandle.TOP) {
+ (currentRow + currentSpan - minItemSpan).coerceAtLeast(0)
+ } else {
+ maxItemSpan
+ }
+
+ // The minimum row this handle can be dragged to.
+ val minRow =
+ if (handle == DragHandle.TOP) {
+ 0
+ } else {
+ (currentRow + minItemSpan).coerceAtMost(maxItemSpan)
+ }
+
+ // The current row position of this handle
+ val currentPosition = if (handle == DragHandle.TOP) currentRow else currentRow + currentSpan
+
+ return DraggableAnchors {
+ for (targetRow in minRow..maxRow step minItemSpan) {
+ val diff = targetRow - currentPosition
+ val spacing = diff / minItemSpan * verticalSpacingPx
+ diff at diff * heightPerSpanPx + spacing
+ }
+ }
+ }
+
+ private fun isDragAllowed(handle: DragHandle, layoutInfo: GridLayoutInfo): Boolean {
+ val minItemSpan = layoutInfo.minSpan
+ val maxItemSpan = layoutInfo.maxSpan
+ val currentRow = layoutInfo.currentRow
+ val currentSpan = layoutInfo.currentSpan
+ val atMinSize = currentSpan == minItemSpan
+
+ // If already at the minimum size and in the first row, item cannot be expanded from the top
+ if (handle == DragHandle.TOP && currentRow == 0 && atMinSize) {
+ return false
+ }
+
+ // If already at the minimum size and occupying the last row, item cannot be expanded from
+ // the
+ // bottom
+ if (handle == DragHandle.BOTTOM && (currentRow + currentSpan) == maxItemSpan && atMinSize) {
+ return false
+ }
+
+ // If at maximum size, item can only be shrunk from the bottom and not the top.
+ if (handle == DragHandle.TOP && currentSpan == maxItemSpan) {
+ return false
+ }
+
+ return true
+ }
+
+ override suspend fun onActivated(): Nothing {
+ coroutineScope("ResizeableItemFrameViewModel.onActivated") {
+ gridLayoutInfo
+ .filterNotNull()
+ .onEach { layoutInfo ->
+ topDragState.updateAnchors(
+ calculateAnchorsForHandle(DragHandle.TOP, layoutInfo)
+ )
+ bottomDragState.updateAnchors(
+ calculateAnchorsForHandle(DragHandle.BOTTOM, layoutInfo)
+ )
+ }
+ .launchIn(this)
+ awaitCancellation()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
index 373279c..462e820 100644
--- a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
@@ -20,6 +20,8 @@
import com.android.systemui.display.data.repository.DeviceStateRepositoryImpl
import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.display.data.repository.DisplayRepositoryImpl
+import com.android.systemui.display.data.repository.FocusedDisplayRepository
+import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractorImpl
import dagger.Binds
@@ -39,4 +41,9 @@
fun bindsDeviceStateRepository(
deviceStateRepository: DeviceStateRepositoryImpl
): DeviceStateRepository
+
+ @Binds
+ fun bindsFocusedDisplayRepository(
+ focusedDisplayRepository: FocusedDisplayRepositoryImpl
+ ): FocusedDisplayRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/FocusedDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/FocusedDisplayRepository.kt
index 3bfc7a1..6fc08f6 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/FocusedDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/FocusedDisplayRepository.kt
@@ -37,16 +37,21 @@
import kotlinx.coroutines.flow.stateIn
/** Repository tracking display focus. */
+interface FocusedDisplayRepository {
+ /** Provides the currently focused display. */
+ val focusedDisplayId: StateFlow<Int>
+}
+
@SysUISingleton
@MainThread
-class FocusedDisplayRepository
+class FocusedDisplayRepositoryImpl
@Inject
constructor(
@Background val backgroundScope: CoroutineScope,
@Background private val backgroundExecutor: Executor,
transitions: ShellTransitions,
@FocusedDisplayRepoLog logBuffer: LogBuffer,
-) {
+) : FocusedDisplayRepository {
val focusedTask: Flow<Int> =
conflatedCallbackFlow<Int> {
val listener =
@@ -67,7 +72,6 @@
)
}
- /** Provides the currently focused display. */
- val focusedDisplayId: StateFlow<Int>
+ override val focusedDisplayId: StateFlow<Int>
get() = focusedTask.stateIn(backgroundScope, SharingStarted.Eagerly, DEFAULT_DISPLAY)
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
new file mode 100644
index 0000000..8b6cc8c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.dreams.ui.viewmodel
+
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** Handles user input for the dream scene. */
+class DreamUserActionsViewModel @AssistedInject constructor() : UserActionsViewModel() {
+
+ override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
+ setActions(emptyMap())
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): DreamUserActionsViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
index 32e7f41..5563969 100644
--- a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
@@ -48,7 +48,7 @@
) {
companion object {
- const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 3500
+ const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 5000
}
private val timeoutMillis: Long
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 1e9541e..6d1d9cb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -189,6 +189,7 @@
internalTransitionInteractor.currentTransitionInfoInternal,
keyguardInteractor.statusBarState,
keyguardInteractor.isKeyguardDismissible,
+ keyguardInteractor.isKeyguardOccluded,
)
.collect {
(
@@ -196,7 +197,8 @@
startedStep,
currentTransitionInfo,
statusBarState,
- isKeyguardUnlocked) ->
+ isKeyguardUnlocked,
+ isKeyguardOccluded) ->
val id = transitionId
if (id != null) {
if (startedStep.to == KeyguardState.PRIMARY_BOUNCER) {
@@ -236,9 +238,13 @@
if (nextState == TransitionState.CANCELED) {
transitionRepository.startTransition(
TransitionInfo(
- ownerName = name,
+ ownerName =
+ "$name " +
+ "(on behalf of FromPrimaryBouncerInteractor)",
from = KeyguardState.PRIMARY_BOUNCER,
- to = KeyguardState.LOCKSCREEN,
+ to =
+ if (isKeyguardOccluded) KeyguardState.OCCLUDED
+ else KeyguardState.LOCKSCREEN,
modeOnCanceled = TransitionModeOnCanceled.REVERSE,
animator =
getDefaultAnimatorForTransitionsToState(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 51d2329..65c29b8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -52,8 +52,8 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.approachLayout
import androidx.compose.ui.layout.onPlaced
-import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.dimensionResource
@@ -66,6 +66,9 @@
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneScope
@@ -288,7 +291,7 @@
transitions =
transitions {
from(QuickQuickSettings, QuickSettings) {
- quickQuickSettingsToQuickSettings()
+ quickQuickSettingsToQuickSettings(viewModel::inFirstPage::get)
}
},
)
@@ -533,6 +536,10 @@
onDispose { qqsVisible.value = false }
}
+ val squishiness by
+ viewModel.containerViewModel.quickQuickSettingsViewModel.squishinessViewModel
+ .squishiness
+ .collectAsStateWithLifecycle()
Column(modifier = Modifier.sysuiResTag("quick_qs_panel")) {
Box(
modifier =
@@ -546,7 +553,16 @@
topFromRoot + coordinates.size.height,
)
}
- .onSizeChanged { size -> qqsHeight.value = size.height }
+ // Use an approach layout to determien the height without squishiness, as
+ // that's the value that NPVC and QuickSettingsController care about
+ // (measured height).
+ .approachLayout(isMeasurementApproachInProgress = { squishiness < 1f }) {
+ measurable,
+ constraints ->
+ qqsHeight.value = lookaheadSize.height
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+ }
.padding(top = { qqsPadding }, bottom = { bottomPadding.roundToPx() })
) {
val qsEnabled by viewModel.qsEnabled.collectAsStateWithLifecycle()
@@ -704,6 +720,14 @@
else -> QuickSettings
}
}
+
+ val QqsTileElementMatcher =
+ object : ElementMatcher {
+ override fun matches(key: ElementKey, content: ContentKey): Boolean {
+ return content == SceneKeys.QuickQuickSettings &&
+ ElementKeys.TileElementMatcher.matches(key, content)
+ }
+ }
}
suspend fun synchronizeQsState(state: MutableSceneTransitionLayoutState, expansion: Flow<Float>) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt
index 1514986..9e3945e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt
@@ -17,13 +17,23 @@
package com.android.systemui.qs.composefragment.ui
import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.qs.composefragment.SceneKeys
import com.android.systemui.qs.shared.ui.ElementKeys
-fun TransitionBuilder.quickQuickSettingsToQuickSettings() {
+fun TransitionBuilder.quickQuickSettingsToQuickSettings(inFirstPage: () -> Boolean = { true }) {
fractionRange(start = 0.5f) { fade(ElementKeys.QuickSettingsContent) }
fractionRange(start = 0.9f) { fade(ElementKeys.FooterActions) }
anchoredTranslate(ElementKeys.QuickSettingsContent, ElementKeys.GridAnchor)
+
+ sharedElement(ElementKeys.TileElementMatcher, enabled = inFirstPage())
+
+ // This will animate between 0f (QQS) and 0.6, fading in the QQS tiles when coming back
+ // from non first page QS. The QS content ends fading out at 0.5f, so there's a brief
+ // overlap, but because they are really faint, it looks better than complete black without
+ // overlap.
+ fractionRange(end = 0.6f) { fade(SceneKeys.QqsTileElementMatcher) }
+ anchoredTranslate(SceneKeys.QqsTileElementMatcher, ElementKeys.GridAnchor)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index 2d4e358..7a8b2c2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -31,6 +31,7 @@
import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel.QSExpansionState
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor
+import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel
import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
import com.android.systemui.shade.LargeScreenHeaderHelper
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
@@ -71,6 +72,7 @@
private val configurationInteractor: ConfigurationInteractor,
private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
private val squishinessInteractor: TileSquishinessInteractor,
+ private val paginatedGridViewModel: PaginatedGridViewModel,
@Assisted private val lifecycleScope: LifecycleCoroutineScope,
) : Dumpable, ExclusiveActivatable() {
val footerActionsViewModel =
@@ -292,6 +294,9 @@
*/
var collapseExpandAccessibilityAction: Runnable? = null
+ val inFirstPage: Boolean
+ get() = paginatedGridViewModel.inFirstPage
+
override suspend fun onActivated(): Nothing {
hydrateSquishinessInteractor()
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
index 083f529..e749475 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
@@ -31,8 +31,10 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -76,6 +78,11 @@
val pagerState = rememberPagerState(0) { pages.size }
+ // Used to track if this is currently in the first page or not, for animations
+ LaunchedEffect(key1 = pagerState) {
+ snapshotFlow { pagerState.currentPage == 0 }.collect { viewModel.inFirstPage = it }
+ }
+
Column {
HorizontalPager(
state = pagerState,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt
index ada1ef4..91f7641 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt
@@ -17,7 +17,7 @@
package com.android.systemui.qs.panels.ui.compose.infinitegrid
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.approachLayout
import kotlin.math.roundToInt
/**
@@ -27,17 +27,22 @@
* [squishiness] on the measure/layout pass.
*
* The squished composable will be center aligned.
+ *
+ * Use an [approachLayout] to indicate that this should be measured in the lookahead step without
+ * using squishiness. If a parent of this node needs to determine unsquished height, they should
+ * also use an approachLayout tracking the squishiness.
*/
fun Modifier.verticalSquish(squishiness: () -> Float): Modifier {
- return layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- val actualHeight = placeable.height
- val squishedHeight = actualHeight * squishiness()
- // Center the content by moving it UP (squishedHeight < actualHeight)
- val scroll = (squishedHeight - actualHeight) / 2
+ return approachLayout(isMeasurementApproachInProgress = { squishiness() < 1 }) { measurable, _
+ ->
+ val squishinessValue = squishiness()
+ val expectedHeight = lookaheadSize.height
- layout(placeable.width, squishedHeight.roundToInt()) {
- placeable.place(0, scroll.roundToInt())
- }
+ val placeable = measurable.measure(lookaheadConstraints)
+ val squishedHeight = (expectedHeight * squishinessValue).roundToInt()
+ // Center the content by moving it UP (squishedHeight < actualHeight)
+ val scroll = (squishedHeight - expectedHeight) / 2
+
+ layout(placeable.width, squishedHeight) { placeable.place(0, scroll) }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
index 28bf474..d4f8298 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
@@ -43,4 +43,10 @@
SharingStarted.WhileSubscribed(),
paginatedGridInteractor.defaultRows,
)
+
+ /*
+ * Tracks whether the current HorizontalPager (using this viewmodel) is in the first page.
+ * This requires it to be a `@SysUISingleton` to be shared between viewmodels.
+ */
+ var inFirstPage = true
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt b/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt
index 625459d..2425f13 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt
@@ -25,7 +25,10 @@
val GridAnchor = ElementKey("QuickSettingsGridAnchor")
val FooterActions = ElementKey("FooterActions")
- class TileElementKey(spec: TileSpec, val position: Int) : ElementKey(spec.spec, spec.spec)
+ fun TileSpec.toElementKey(positionInGrid: Int) =
+ ElementKey(this.spec, TileIdentity(this, positionInGrid))
- fun TileSpec.toElementKey(positionInGrid: Int) = TileElementKey(this, positionInGrid)
+ val TileElementMatcher = ElementKey.withIdentity { it is TileIdentity }
}
+
+private data class TileIdentity(val spec: TileSpec, val position: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index a89f752..4beec10 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -44,6 +44,7 @@
[
BouncerSceneModule::class,
CommunalSceneModule::class,
+ DreamSceneModule::class,
EmptySceneModule::class,
GoneSceneModule::class,
LockscreenSceneModule::class,
@@ -98,6 +99,7 @@
listOfNotNull(
Scenes.Gone,
Scenes.Communal,
+ Scenes.Dream,
Scenes.Lockscreen,
Scenes.Bouncer,
Scenes.QuickSettings.takeUnless { DualShade.isEnabled },
@@ -114,9 +116,10 @@
Scenes.Gone to 0,
Scenes.Lockscreen to 0,
Scenes.Communal to 1,
- Scenes.Shade to 2.takeUnless { DualShade.isEnabled },
- Scenes.QuickSettings to 3.takeUnless { DualShade.isEnabled },
- Scenes.Bouncer to 4,
+ Scenes.Dream to 2,
+ Scenes.Shade to 3.takeUnless { DualShade.isEnabled },
+ Scenes.QuickSettings to 4.takeUnless { DualShade.isEnabled },
+ Scenes.Bouncer to 5,
)
.filterValues { it != null }
.mapValues { checkNotNull(it.value) },
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
index 82b4b1c..16492ef 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
@@ -33,6 +33,9 @@
/** The communal scene shows the glanceable hub when device is locked and docked. */
@JvmField val Communal = SceneKey("communal")
+ /** The dream scene shows up when a dream activity is showing. */
+ @JvmField val Dream = SceneKey("dream")
+
/**
* "Gone" is not a real scene but rather the absence of scenes when we want to skip showing any
* content from the scene framework.
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
index ea19020..f0f476e 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
@@ -25,6 +25,7 @@
import android.view.WindowInsets
import android.widget.FrameLayout
import androidx.core.view.updateMargins
+import com.android.systemui.Flags
import com.android.systemui.compose.ComposeInitializer
import com.android.systemui.res.R
@@ -103,6 +104,8 @@
private fun applyMargins() {
val count = childCount
+ val hasFlagsEnabled = Flags.checkLockscreenGoneTransition()
+ var hasChildMarginUpdated = false
for (i in 0 until count) {
val child = getChildAt(i)
if (child.layoutParams is LayoutParams) {
@@ -113,10 +116,17 @@
layoutParams.leftMargin != leftInset)
) {
layoutParams.updateMargins(left = leftInset, right = rightInset)
- child.requestLayout()
+ hasChildMarginUpdated = true
+ if (!hasFlagsEnabled) {
+ child.requestLayout()
+ }
}
}
}
+ if (hasFlagsEnabled && hasChildMarginUpdated) {
+ // Request layout at once after all children's margins has updated
+ requestLayout()
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt b/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt
index 6730d2d..7b56688 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt
@@ -49,7 +49,7 @@
override fun handleScreenshot(
screenshot: ScreenshotData,
finisher: Consumer<Uri?>,
- requestCallback: TakeScreenshotService.RequestCallback
+ requestCallback: TakeScreenshotService.RequestCallback,
) {
if (screenshot.type == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) {
screenshot.bitmap = imageCapture.captureDisplay(screenshot.displayId, crop = null)
@@ -69,8 +69,8 @@
Executors.newSingleThreadExecutor(),
UUID.randomUUID(),
screenshot.bitmap,
- screenshot.getUserOrDefault(),
- screenshot.displayId
+ screenshot.userHandle,
+ screenshot.displayId,
)
future.addListener(
{
@@ -86,7 +86,7 @@
requestCallback.reportError()
}
},
- mainExecutor
+ mainExecutor,
)
}
@@ -98,11 +98,11 @@
.notifyScreenshotError(R.string.screenshot_failed_to_save_text)
} else {
uiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, screenshot.packageNameString)
- if (userManager.isManagedProfile(screenshot.getUserOrDefault().identifier)) {
+ if (userManager.isManagedProfile(screenshot.userHandle.identifier)) {
uiEventLogger.log(
ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE,
0,
- screenshot.packageNameString
+ screenshot.packageNameString,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
index 7724abd..e589600 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
@@ -301,7 +301,7 @@
saveScreenshotInBackground(screenshot, requestId, finisher, result -> {
if (result.uri != null) {
ScreenshotSavedResult savedScreenshot = new ScreenshotSavedResult(
- result.uri, screenshot.getUserOrDefault(), result.timestamp);
+ result.uri, screenshot.getUserHandle(), result.timestamp);
mActionsController.setCompletedScreenshot(requestId, savedScreenshot);
}
});
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt
index 29208f8..0806be8 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt
@@ -214,11 +214,7 @@
saveScreenshotInBackground(screenshot, requestId, finisher) { result ->
if (result.uri != null) {
val savedScreenshot =
- ScreenshotSavedResult(
- result.uri,
- screenshot.getUserOrDefault(),
- result.timestamp,
- )
+ ScreenshotSavedResult(result.uri, screenshot.userHandle, result.timestamp)
actionsController.setCompletedScreenshot(requestId, savedScreenshot)
}
}
@@ -235,7 +231,7 @@
window.setFocusable(true)
viewProxy.requestFocus()
- enqueueScrollCaptureRequest(requestId, screenshot.userHandle!!)
+ enqueueScrollCaptureRequest(requestId, screenshot.userHandle)
window.attachWindow()
@@ -267,7 +263,7 @@
private fun prepareViewForNewScreenshot(screenshot: ScreenshotData, oldPackageName: String?) {
window.whenWindowAttached {
- announcementResolver.getScreenshotAnnouncement(screenshot.userHandle!!.identifier) {
+ announcementResolver.getScreenshotAnnouncement(screenshot.userHandle.identifier) {
viewProxy.announceForAccessibility(it)
}
}
@@ -517,7 +513,7 @@
bgExecutor,
requestId,
screenshot.bitmap,
- screenshot.getUserOrDefault(),
+ screenshot.userHandle,
display.displayId,
)
future.addListener(
@@ -525,7 +521,7 @@
try {
val result = future.get()
Log.d(TAG, "Saved screenshot: $result")
- logScreenshotResultStatus(result.uri, screenshot.userHandle!!)
+ logScreenshotResultStatus(result.uri, screenshot.userHandle)
onResult.accept(result)
if (LogConfig.DEBUG_CALLBACK) {
Log.d(TAG, "finished bg processing, calling back with uri: ${result.uri}")
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
index fb7c34f..2df1e8a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
@@ -18,7 +18,7 @@
@ScreenshotType val type: Int,
@ScreenshotSource val source: Int,
/** UserHandle for the owner of the app being screenshotted, if known. */
- val userHandle: UserHandle?,
+ val userHandle: UserHandle,
/** ComponentName of the top-most app in the screenshot. */
val topComponent: ComponentName?,
var screenBounds: Rect?,
@@ -40,7 +40,7 @@
ScreenshotData(
type = request.type,
source = request.source,
- userHandle = if (request.userId >= 0) UserHandle.of(request.userId) else null,
+ userHandle = UserHandle.of(request.userId),
topComponent = request.topComponent,
screenBounds = request.boundsInScreen,
taskId = request.taskId,
@@ -51,7 +51,7 @@
@VisibleForTesting
fun forTesting(
- userHandle: UserHandle? = null,
+ userHandle: UserHandle = UserHandle.CURRENT,
source: Int = ScreenshotSource.SCREENSHOT_KEY_CHORD,
topComponent: ComponentName? = null,
bitmap: Bitmap? = null,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index ab8a953..a755746 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -28,6 +28,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.display.data.repository.DisplayRepository
+import com.android.systemui.display.data.repository.FocusedDisplayRepository
import com.android.systemui.res.R
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
@@ -83,6 +84,7 @@
private val uiEventLogger: UiEventLogger,
private val screenshotNotificationControllerFactory: ScreenshotNotificationsController.Factory,
private val headlessScreenshotHandler: HeadlessScreenshotHandler,
+ private val focusedDisplayRepository: FocusedDisplayRepository,
) : TakeScreenshotExecutor {
private val displays = displayRepository.displays
private var screenshotController: InteractiveScreenshotHandler? = null
@@ -216,14 +218,13 @@
?: error("Can't find default display")
// All other invocations use the focused display
- else -> focusedDisplay()
+ else ->
+ displayRepository.getDisplay(focusedDisplayRepository.focusedDisplayId.value)
+ ?: displayRepository.getDisplay(Display.DEFAULT_DISPLAY)
+ ?: error("Can't find default display")
}
}
- // TODO(b/367394043): Determine the focused display here.
- private suspend fun focusedDisplay() =
- displayRepository.getDisplay(Display.DEFAULT_DISPLAY) ?: error("Can't find default display")
-
/** Propagates the close system dialog signal to the ScreenshotController. */
override fun onCloseSystemDialogsReceived() {
if (screenshotController?.isPendingSharedTransition() == false) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt
index b3d5c9e..b67ad8a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt
@@ -92,7 +92,7 @@
updates.component,
updates.owner,
type.taskId,
- type.taskBounds
+ type.taskBounds,
)
is FullScreen ->
replaceWithScreenshot(
@@ -122,7 +122,7 @@
componentName = topMainRootTask?.topActivity ?: defaultComponent,
taskId = topMainRootTask?.taskId,
owner = defaultOwner,
- displayId = original.displayId
+ displayId = original.displayId,
)
}
@@ -141,14 +141,14 @@
userHandle = owner,
taskId = taskId,
topComponent = componentName,
- screenBounds = taskBounds
+ screenBounds = taskBounds,
)
}
private suspend fun replaceWithScreenshot(
original: ScreenshotData,
componentName: ComponentName?,
- owner: UserHandle?,
+ owner: UserHandle,
displayId: Int,
taskId: Int? = null,
): ScreenshotData {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
index a4906c1..91efa0a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
@@ -41,7 +41,7 @@
class ScreenshotAnimationController(
private val view: ScreenshotShelfView,
- private val viewModel: ScreenshotViewModel
+ private val viewModel: ScreenshotViewModel,
) {
private var animator: Animator? = null
private val screenshotPreview = view.requireViewById<ImageView>(R.id.screenshot_preview)
@@ -56,7 +56,7 @@
listOf<View>(
view.requireViewById(R.id.screenshot_preview_border),
view.requireViewById(R.id.screenshot_badge),
- view.requireViewById(R.id.screenshot_dismiss_button)
+ view.requireViewById(R.id.screenshot_dismiss_button),
)
private val fadeUI =
listOf<View>(
@@ -70,9 +70,11 @@
fun getEntranceAnimation(
bounds: Rect,
showFlash: Boolean,
- onRevealMilestone: () -> Unit
+ onRevealMilestone: () -> Unit,
): Animator {
val entranceAnimation = AnimatorSet()
+ view.alpha = 1f
+ view.translationX = 0f
val previewAnimator = getPreviewAnimator(bounds)
@@ -142,7 +144,7 @@
fun runLongScreenshotTransition(
destRect: Rect,
longScreenshot: ScrollCaptureController.LongScreenshot,
- onTransitionEnd: Runnable
+ onTransitionEnd: Runnable,
): Animator {
val animSet = AnimatorSet()
@@ -165,7 +167,7 @@
matrix.setScale(currentScale, currentScale)
matrix.postTranslate(
longScreenshot.left * currentScale,
- longScreenshot.top * currentScale
+ longScreenshot.top * currentScale,
)
scrollTransitionPreview.setImageMatrix(matrix)
val destinationScale: Float = destRect.width() / scrollTransitionPreview.width.toFloat()
@@ -315,7 +317,7 @@
}
private fun getAdjustedVelocity(requestedVelocity: Float?): Float {
- return if (requestedVelocity == null) {
+ return if (requestedVelocity == null || abs(requestedVelocity) < .005f) {
val isLTR = view.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
// dismiss to the left in LTR locales, to the right in RTL
if (isLTR) -MINIMUM_VELOCITY else MINIMUM_VELOCITY
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/SwipeGestureListener.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/SwipeGestureListener.kt
index 61d4489..ecb45e1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/SwipeGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/SwipeGestureListener.kt
@@ -25,7 +25,7 @@
class SwipeGestureListener(
private val view: View,
private val onDismiss: (Float?) -> Unit,
- private val onCancel: () -> Unit
+ private val onCancel: () -> Unit,
) {
private val velocityTracker = VelocityTracker.obtain()
private val displayMetrics = view.resources.displayMetrics
@@ -54,9 +54,9 @@
onDismiss.invoke(xVelocity)
return true
} else {
- velocityTracker.clear()
onCancel.invoke()
}
+ velocityTracker.clear()
}
MotionEvent.ACTION_MOVE -> {
velocityTracker.addMovement(ev)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 5896659..2bff7c86 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -30,6 +30,7 @@
import static com.android.systemui.classifier.Classifier.GENERIC;
import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
import static com.android.systemui.classifier.Classifier.UNLOCK;
+import static com.android.systemui.keyguard.shared.model.KeyguardState.AOD;
import static com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING;
import static com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING_LOCKSCREEN_HOSTED;
import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE;
@@ -1213,6 +1214,16 @@
}, mMainDispatcher);
}
+ if (MigrateClocksToBlueprint.isEnabled()) {
+ collectFlow(mView, mKeyguardTransitionInteractor.transition(
+ Edge.Companion.create(AOD, LOCKSCREEN)),
+ (TransitionStep step) -> {
+ if (step.getTransitionState() == TransitionState.FINISHED) {
+ updateExpandedHeightToMaxHeight();
+ }
+ }, mMainDispatcher);
+ }
+
// Ensures that flags are updated when an activity launches
collectFlow(mView,
mShadeAnimationInteractor.isLaunchingActivity(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index e47952f..a79b78f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -58,6 +58,7 @@
import com.android.internal.statusbar.NotificationVisibility;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.Dumpable;
+import com.android.systemui.Flags;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
@@ -813,11 +814,17 @@
private void notifyNotificationStateChanged() {
if (!Looper.getMainLooper().isCurrentThread()) {
- mMainExecutor.execute(() -> {
+ if (Flags.checkLockscreenGoneTransition()) {
for (NotificationStateChangedListener listener : mNotifStateChangedListeners) {
- listener.onNotificationStateChanged();
+ mMainExecutor.execute(listener::onNotificationStateChanged);
}
- });
+ } else {
+ mMainExecutor.execute(() -> {
+ for (NotificationStateChangedListener listener : mNotifStateChangedListeners) {
+ listener.onNotificationStateChanged();
+ }
+ });
+ }
} else {
for (NotificationStateChangedListener listener : mNotifStateChangedListeners) {
listener.onNotificationStateChanged();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
index 5558ab1..0a7f08d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
@@ -2,11 +2,13 @@
# Bug component: 78010
-aioana@google.com
-aroederer@google.com
-iyz@google.com
jeffdq@google.com
juliacr@google.com
+
+aioana@google.com
+aroederer@google.com
+asc@google.com
+iyz@google.com
juliatuttle@google.com
kurucz@google.com
liuyining@google.com
@@ -15,4 +17,4 @@
valiiftime@google.com
yurilin@google.com
-per-file MediaNotificationProcessor.java = ethibodeau@google.com, asc@google.com
+per-file MediaNotificationProcessor.java = ethibodeau@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index cb133ec..e74ed8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -68,11 +68,9 @@
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController;
import com.android.systemui.statusbar.notification.row.NotificationGuts;
-import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository;
import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel;
import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel;
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor;
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel;
import com.android.systemui.statusbar.notification.stack.PriorityBucket;
import com.android.systemui.util.ListenerSet;
@@ -99,7 +97,7 @@
* At the moment, there are many things here that shouldn't be and vice-versa. Hopefully we can
* clean this up in the future.
*/
-public final class NotificationEntry extends ListEntry implements NotificationRowRepository {
+public final class NotificationEntry extends ListEntry {
private final String mKey;
private StatusBarNotification mSbn;
@@ -161,8 +159,6 @@
StateFlowKt.MutableStateFlow(null);
private final MutableStateFlow<CharSequence> mHeadsUpStatusBarTextPublic =
StateFlowKt.MutableStateFlow(null);
- private final MutableStateFlow<RichOngoingContentModel> mRichOngoingContentModel =
- StateFlowKt.MutableStateFlow(null);
// indicates when this entry's view was first attached to a window
// this value will reset when the view is completely removed from the shade (ie: filtered out)
@@ -969,12 +965,6 @@
return mHeadsUpStatusBarTextPublic;
}
- /** Gets the current RON content model, which may be null */
- @NonNull
- public StateFlow<RichOngoingContentModel> getRichOngoingContentModel() {
- return mRichOngoingContentModel;
- }
-
/**
* Sets the text to be displayed on the StatusBar, when this notification is the top pinned
* heads up, and its content is sensitive right now.
@@ -1069,7 +1059,6 @@
HeadsUpStatusBarModel headsUpStatusBarModel = contentModel.getHeadsUpStatusBarModel();
this.mHeadsUpStatusBarText.setValue(headsUpStatusBarModel.getPrivateText());
this.mHeadsUpStatusBarTextPublic.setValue(headsUpStatusBarModel.getPublicText());
- this.mRichOngoingContentModel.setValue(contentModel.getRichOngoingContentModel());
}
/** Information about a suggestion that is being edited. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index 41419f3..8660cd1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -22,15 +22,18 @@
import androidx.annotation.VisibleForTesting;
import com.android.systemui.Dumpable;
+import com.android.systemui.Flags;
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.Edge;
import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
+import com.android.systemui.scene.shared.model.Scenes;
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor;
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
@@ -85,6 +88,7 @@
private boolean mNotifPanelLaunchingActivity;
private boolean mCommunalShowing = false;
private boolean mLockscreenShowing = false;
+ private boolean mLockscreenInGoneTransition = false;
private boolean mPipelineRunAllowed;
private boolean mReorderingAllowed;
@@ -158,6 +162,13 @@
KeyguardState.LOCKSCREEN),
this::onLockscreenKeyguardStateTransitionValueChanged);
}
+ if (Flags.checkLockscreenGoneTransition()) {
+ mJavaAdapter.alwaysCollectFlow(mKeyguardTransitionInteractor.isInTransition(
+ Edge.create(KeyguardState.LOCKSCREEN, Scenes.Gone),
+ Edge.create(KeyguardState.LOCKSCREEN, KeyguardState.GONE)),
+ this::onLockscreenInGoneTransitionChanged);
+ }
+
pipeline.setVisualStabilityManager(mNotifStabilityManager);
}
@@ -239,7 +250,9 @@
private void updateAllowedStates(String field, boolean value) {
boolean wasPipelineRunAllowed = mPipelineRunAllowed;
boolean wasReorderingAllowed = mReorderingAllowed;
- mPipelineRunAllowed = !isPanelCollapsingOrLaunchingActivity();
+ // No need to run notification pipeline when the lockscreen is in fading animation.
+ mPipelineRunAllowed = !(isPanelCollapsingOrLaunchingActivity()
+ || (Flags.checkLockscreenGoneTransition() && mLockscreenInGoneTransition));
mReorderingAllowed = isReorderingAllowed();
if (wasPipelineRunAllowed != mPipelineRunAllowed
|| wasReorderingAllowed != mReorderingAllowed) {
@@ -330,7 +343,6 @@
updateAllowedStates("fullyDozed", fullyDozed);
}
};
-
final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
@Override
public void onFinishedGoingToSleep() {
@@ -353,6 +365,9 @@
pw.println("pipelineRunAllowed: " + mPipelineRunAllowed);
pw.println(" notifPanelCollapsing: " + mNotifPanelCollapsing);
pw.println(" launchingNotifActivity: " + mNotifPanelLaunchingActivity);
+ if (Flags.checkLockscreenGoneTransition()) {
+ pw.println(" lockscreenInGoneTransition: " + mLockscreenInGoneTransition);
+ }
pw.println("reorderingAllowed: " + mReorderingAllowed);
pw.println(" sleepy: " + mSleepy);
pw.println(" fullyDozed: " + mFullyDozed);
@@ -401,4 +416,15 @@
mLockscreenShowing = isShowing;
updateAllowedStates("lockscreenShowing", isShowing);
}
+
+ private void onLockscreenInGoneTransitionChanged(boolean inGoneTransition) {
+ if (!Flags.checkLockscreenGoneTransition()) {
+ return;
+ }
+ if (inGoneTransition == mLockscreenInGoneTransition) {
+ return;
+ }
+ mLockscreenInGoneTransition = inGoneTransition;
+ updateAllowedStates("lockscreenInGoneTransition", mLockscreenInGoneTransition);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 48c974a..9166e7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -76,8 +76,6 @@
import com.android.systemui.util.Compile;
import com.android.systemui.util.DumpUtilsKt;
-import kotlinx.coroutines.DisposableHandle;
-
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
@@ -116,10 +114,6 @@
@VisibleForTesting
protected HybridNotificationView mSingleLineView;
- @Nullable public DisposableHandle mContractedBinderHandle;
- @Nullable public DisposableHandle mExpandedBinderHandle;
- @Nullable public DisposableHandle mHeadsUpBinderHandle;
-
private RemoteInputView mExpandedRemoteInput;
private RemoteInputView mHeadsUpRemoteInput;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index c342bcd..b166def 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -46,9 +46,6 @@
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
import com.android.systemui.statusbar.notification.InflationException
import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder
-import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView
-import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP
@@ -71,7 +68,6 @@
import com.android.systemui.statusbar.notification.row.shared.NewRemoteViews
import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer
@@ -95,8 +91,6 @@
private val remoteViewCache: NotifRemoteViewCache,
private val remoteInputManager: NotificationRemoteInputManager,
private val conversationProcessor: ConversationNotificationProcessor,
- private val ronExtractor: RichOngoingNotificationContentExtractor,
- private val ronInflater: RichOngoingNotificationViewInflater,
@NotifInflation private val inflationExecutor: Executor,
private val smartReplyStateInflater: SmartReplyStateInflater,
private val notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider,
@@ -144,8 +138,6 @@
remoteViewCache,
entry,
conversationProcessor,
- ronExtractor,
- ronInflater,
row,
bindParams.isMinimized,
bindParams.usesIncreasedHeight,
@@ -190,7 +182,6 @@
notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider,
headsUpStyleProvider = headsUpStyleProvider,
conversationProcessor = conversationProcessor,
- ronExtractor = ronExtractor,
logger = logger,
)
inflateSmartReplyViews(
@@ -282,22 +273,16 @@
when (inflateFlag) {
FLAG_CONTENT_VIEW_CONTRACTED ->
row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_CONTRACTED) {
- row.privateLayout.mContractedBinderHandle?.dispose()
- row.privateLayout.mContractedBinderHandle = null
row.privateLayout.setContractedChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)
}
FLAG_CONTENT_VIEW_EXPANDED ->
row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_EXPANDED) {
- row.privateLayout.mExpandedBinderHandle?.dispose()
- row.privateLayout.mExpandedBinderHandle = null
row.privateLayout.setExpandedChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
}
FLAG_CONTENT_VIEW_HEADS_UP ->
row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_HEADSUP) {
- row.privateLayout.mHeadsUpBinderHandle?.dispose()
- row.privateLayout.mHeadsUpBinderHandle = null
row.privateLayout.setHeadsUpChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
row.privateLayout.setHeadsUpInflatedSmartReplies(null)
@@ -378,8 +363,6 @@
private val remoteViewCache: NotifRemoteViewCache,
private val entry: NotificationEntry,
private val conversationProcessor: ConversationNotificationProcessor,
- private val ronExtractor: RichOngoingNotificationContentExtractor,
- private val ronInflater: RichOngoingNotificationViewInflater,
private val row: ExpandableNotificationRow,
private val isMinimized: Boolean,
private val usesIncreasedHeight: Boolean,
@@ -459,7 +442,6 @@
notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider,
headsUpStyleProvider = headsUpStyleProvider,
conversationProcessor = conversationProcessor,
- ronExtractor = ronExtractor,
logger = logger
)
logger.logAsyncTaskProgress(
@@ -506,90 +488,6 @@
}
}
- val richOngoingContentModel = inflationProgress.contentModel.richOngoingContentModel
-
- if (
- richOngoingContentModel != null &&
- reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0
- ) {
- logger.logAsyncTaskProgress(entry, "inflating RON view")
- val inflateContractedView = reInflateFlags and FLAG_CONTENT_VIEW_CONTRACTED != 0
- val inflateExpandedView = reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0
- val inflateHeadsUpView = reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0
-
- inflationProgress.contractedRichOngoingNotificationViewHolder =
- if (inflateContractedView) {
- ronInflater.inflateView(
- contentModel = richOngoingContentModel,
- existingView = row.privateLayout.contractedChild,
- entry = entry,
- systemUiContext = context,
- parentView = row.privateLayout,
- viewType = RichOngoingNotificationViewType.Contracted
- )
- } else {
- if (
- ronInflater.canKeepView(
- contentModel = richOngoingContentModel,
- existingView = row.privateLayout.contractedChild,
- viewType = RichOngoingNotificationViewType.Contracted
- )
- ) {
- KeepExistingView
- } else {
- NullContentView
- }
- }
-
- inflationProgress.expandedRichOngoingNotificationViewHolder =
- if (inflateExpandedView) {
- ronInflater.inflateView(
- contentModel = richOngoingContentModel,
- existingView = row.privateLayout.expandedChild,
- entry = entry,
- systemUiContext = context,
- parentView = row.privateLayout,
- viewType = RichOngoingNotificationViewType.Expanded
- )
- } else {
- if (
- ronInflater.canKeepView(
- contentModel = richOngoingContentModel,
- existingView = row.privateLayout.expandedChild,
- viewType = RichOngoingNotificationViewType.Expanded
- )
- ) {
- KeepExistingView
- } else {
- NullContentView
- }
- }
-
- inflationProgress.headsUpRichOngoingNotificationViewHolder =
- if (inflateHeadsUpView) {
- ronInflater.inflateView(
- contentModel = richOngoingContentModel,
- existingView = row.privateLayout.headsUpChild,
- entry = entry,
- systemUiContext = context,
- parentView = row.privateLayout,
- viewType = RichOngoingNotificationViewType.HeadsUp
- )
- } else {
- if (
- ronInflater.canKeepView(
- contentModel = richOngoingContentModel,
- existingView = row.privateLayout.headsUpChild,
- viewType = RichOngoingNotificationViewType.HeadsUp
- )
- ) {
- KeepExistingView
- } else {
- NullContentView
- }
- }
- }
-
logger.logAsyncTaskProgress(entry, "getting row image resolver (on wrong thread!)")
val imageResolver = row.imageResolver
// wait for image resolver to finish preloading
@@ -695,9 +593,6 @@
var inflatedSmartReplyState: InflatedSmartReplyState? = null
var expandedInflatedSmartReplies: InflatedSmartReplyViewHolder? = null
var headsUpInflatedSmartReplies: InflatedSmartReplyViewHolder? = null
- var contractedRichOngoingNotificationViewHolder: ContentViewInflationResult? = null
- var expandedRichOngoingNotificationViewHolder: ContentViewInflationResult? = null
- var headsUpRichOngoingNotificationViewHolder: ContentViewInflationResult? = null
// Inflated SingleLineView that lacks the UI State
var inflatedSingleLineView: HybridNotificationView? = null
@@ -734,7 +629,6 @@
val inflateHeadsUp =
(reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0 &&
result.remoteViews.headsUp != null)
-
if (inflateContracted || inflateExpanded || inflateHeadsUp) {
logger.logAsyncTaskProgress(entry, "inflating contracted smart reply state")
result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry)
@@ -776,7 +670,6 @@
notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider,
headsUpStyleProvider: HeadsUpStyleProvider,
conversationProcessor: ConversationNotificationProcessor,
- ronExtractor: RichOngoingNotificationContentExtractor,
logger: NotificationRowContentBinderLogger
): InflationProgress {
// process conversations and extract the messaging style
@@ -785,24 +678,9 @@
conversationProcessor.processNotification(entry, builder, logger)
} else null
- val richOngoingContentModel =
- if (reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0) {
- ronExtractor.extractContentModel(
- entry = entry,
- builder = builder,
- systemUIContext = systemUIContext,
- packageContext = packageContext
- )
- } else {
- // if we're not re-inflating any RON views, make sure the model doesn't change
- entry.richOngoingContentModel.value
- }
-
- val remoteViewsFlags = getRemoteViewsFlags(reInflateFlags, richOngoingContentModel)
-
val remoteViews =
createRemoteViews(
- reInflateFlags = remoteViewsFlags,
+ reInflateFlags = reInflateFlags,
builder = builder,
isMinimized = isMinimized,
usesIncreasedHeight = usesIncreasedHeight,
@@ -850,7 +728,6 @@
headsUpStatusBarModel = headsUpStatusBarModel,
singleLineViewModel = singleLineViewModel,
publicSingleLineViewModel = publicSingleLineViewModel,
- richOngoingContentModel = richOngoingContentModel,
)
return InflationProgress(
@@ -1506,31 +1383,11 @@
}
logger.logAsyncTaskProgress(entry, "finishing")
- // before updating the content model, stop existing binding if necessary
- if (result.contractedRichOngoingNotificationViewHolder.shouldDisposeViewBinder()) {
- row.privateLayout.mContractedBinderHandle?.dispose()
- row.privateLayout.mContractedBinderHandle = null
- }
-
- if (result.expandedRichOngoingNotificationViewHolder.shouldDisposeViewBinder()) {
- row.privateLayout.mExpandedBinderHandle?.dispose()
- row.privateLayout.mExpandedBinderHandle = null
- }
-
- if (result.headsUpRichOngoingNotificationViewHolder.shouldDisposeViewBinder()) {
- row.privateLayout.mHeadsUpBinderHandle?.dispose()
- row.privateLayout.mHeadsUpBinderHandle = null
- }
-
- // set the content model after disposal and before setting new rich ongoing view
entry.setContentModel(result.contentModel)
result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) }
- // set normal remote views (skipping rich ongoing states when that model exists)
- val remoteViewsFlags =
- getRemoteViewsFlags(reInflateFlags, result.contentModel.richOngoingContentModel)
setContentViewsFromRemoteViews(
- remoteViewsFlags,
+ reInflateFlags,
entry,
remoteViewCache,
result,
@@ -1538,7 +1395,6 @@
isMinimized,
)
- // set single line view
if (
AsyncHybridViewInflation.isEnabled &&
reInflateFlags and FLAG_CONTENT_VIEW_SINGLE_LINE != 0
@@ -1563,55 +1419,6 @@
}
}
- val hasRichOngoingViewHolder =
- result.contractedRichOngoingNotificationViewHolder != null ||
- result.expandedRichOngoingNotificationViewHolder != null ||
- result.headsUpRichOngoingNotificationViewHolder != null
-
- if (hasRichOngoingViewHolder) {
- // after updating the content model, set the view, then start the new binder
- result.contractedRichOngoingNotificationViewHolder?.let { contractedViewHolder ->
- if (contractedViewHolder is InflatedContentViewHolder) {
- row.privateLayout.contractedChild = contractedViewHolder.view
- row.privateLayout.mContractedBinderHandle =
- contractedViewHolder.binder.setupContentViewBinder()
- } else if (contractedViewHolder == NullContentView) {
- row.privateLayout.contractedChild = null
- }
- }
-
- result.expandedRichOngoingNotificationViewHolder?.let { expandedViewHolder ->
- if (expandedViewHolder is InflatedContentViewHolder) {
- row.privateLayout.expandedChild = expandedViewHolder.view
- row.privateLayout.mExpandedBinderHandle =
- expandedViewHolder.binder.setupContentViewBinder()
- } else if (expandedViewHolder == NullContentView) {
- row.privateLayout.expandedChild = null
- }
- }
-
- result.headsUpRichOngoingNotificationViewHolder?.let { headsUpViewHolder ->
- if (headsUpViewHolder is InflatedContentViewHolder) {
- row.privateLayout.headsUpChild = headsUpViewHolder.view
- row.privateLayout.mHeadsUpBinderHandle =
- headsUpViewHolder.binder.setupContentViewBinder()
- } else if (headsUpViewHolder == NullContentView) {
- row.privateLayout.headsUpChild = null
- }
- }
-
- // clean remoteViewCache when we don't keep existing views.
- remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)
- remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
- remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
-
- // Since RONs don't support smart reply, remove them from HUNs and Expanded.
- row.privateLayout.setExpandedInflatedSmartReplies(null)
- row.privateLayout.setHeadsUpInflatedSmartReplies(null)
-
- row.setExpandable(row.privateLayout.expandedChild != null)
- }
-
Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row))
endListener?.onAsyncInflationFinished(entry)
return true
@@ -1775,21 +1582,6 @@
!oldView.hasFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED)
}
- @InflationFlag
- private fun getRemoteViewsFlags(
- @InflationFlag reInflateFlags: Int,
- richOngoingContentModel: RichOngoingContentModel?
- ): Int =
- if (richOngoingContentModel != null) {
- reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING.inv()
- } else {
- reInflateFlags
- }
-
- @InflationFlag
- private const val CONTENT_VIEWS_TO_CREATE_RICH_ONGOING =
- FLAG_CONTENT_VIEW_CONTRACTED or FLAG_CONTENT_VIEW_EXPANDED or FLAG_CONTENT_VIEW_HEADS_UP
-
private const val ASYNC_TASK_TRACE_METHOD =
"NotificationRowContentBinderImpl.AsyncInflationTask"
private const val APPLY_TRACE_METHOD = "NotificationRowContentBinderImpl#apply"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
index c630c4d..84f2f66 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
@@ -18,8 +18,6 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor;
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag;
-import com.android.systemui.statusbar.notification.row.ui.viewmodel.RichOngoingViewModelComponent;
import dagger.Binds;
import dagger.Module;
@@ -30,7 +28,7 @@
/**
* Dagger Module containing notification row and view inflation implementations.
*/
-@Module(subcomponents = {RichOngoingViewModelComponent.class})
+@Module
public abstract class NotificationRowModule {
/**
@@ -49,25 +47,6 @@
}
}
- /** Provides ron content model extractor. */
- @Provides
- @SysUISingleton
- public static RichOngoingNotificationContentExtractor provideRonContentExtractor(
- Provider<RichOngoingNotificationContentExtractorImpl> realImpl
- ) {
- if (RichOngoingNotificationFlag.isEnabled()) {
- return realImpl.get();
- } else {
- return new NoOpRichOngoingNotificationContentExtractor();
- }
- }
-
- /** Provides ron view inflater. */
- @Binds
- @SysUISingleton
- public abstract RichOngoingNotificationViewInflater provideRonViewInflater(
- RichOngoingNotificationViewInflaterImpl impl);
-
/**
* Provides notification remote view cache instance.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
deleted file mode 100644
index ec5ebc36..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * 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.notification.row
-
-import android.app.Notification
-import android.app.PendingIntent
-import android.content.Context
-import android.util.Log
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel
-import com.android.systemui.statusbar.notification.row.shared.IconModel
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
-import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
-import java.time.Duration
-import java.time.LocalDate
-import java.time.LocalDateTime
-import java.time.LocalTime
-import java.time.ZoneId
-import javax.inject.Inject
-
-/**
- * Interface which provides a [RichOngoingContentModel] for a given [Notification] when one is
- * applicable to the given style.
- */
-interface RichOngoingNotificationContentExtractor {
- fun extractContentModel(
- entry: NotificationEntry,
- builder: Notification.Builder,
- systemUIContext: Context,
- packageContext: Context,
- ): RichOngoingContentModel?
-}
-
-class NoOpRichOngoingNotificationContentExtractor : RichOngoingNotificationContentExtractor {
- override fun extractContentModel(
- entry: NotificationEntry,
- builder: Notification.Builder,
- systemUIContext: Context,
- packageContext: Context,
- ): RichOngoingContentModel? = null
-}
-
-@SysUISingleton
-class RichOngoingNotificationContentExtractorImpl @Inject constructor() :
- RichOngoingNotificationContentExtractor {
-
- init {
- /* check if */ RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()
- }
-
- override fun extractContentModel(
- entry: NotificationEntry,
- builder: Notification.Builder,
- systemUIContext: Context,
- packageContext: Context,
- ): RichOngoingContentModel? {
- val sbn = entry.sbn
- val notification = sbn.notification
- val icon = IconModel(notification.smallIcon)
-
- try {
- return if (sbn.packageName == "com.google.android.deskclock") {
- when (notification.channelId) {
- "Timers v2" -> {
- parseTimerNotification(notification, icon)
- }
- "Stopwatch v2" -> {
- Log.i("RONs", "Can't process stopwatch yet")
- null
- }
- else -> {
- Log.i("RONs", "Can't process channel '${notification.channelId}'")
- null
- }
- }
- } else if (builder.style is Notification.ProgressStyle) {
- parseEnRouteNotification(notification, icon)
- } else null
- } catch (e: Exception) {
- Log.e("RONs", "Error parsing RON", e)
- return null
- }
- }
-
- /**
- * FOR PROTOTYPING ONLY: create a RON TimerContentModel using the time information available
- * inside the sortKey of the clock app's timer notifications.
- */
- private fun parseTimerNotification(
- notification: Notification,
- icon: IconModel,
- ): TimerContentModel {
- // sortKey=1 0|↺7|RUNNING|▶16:21:58.523|Σ0:05:00|Δ0:00:03|⏳0:04:57
- // sortKey=1 0|↺7|PAUSED|Σ0:05:00|Δ0:04:54|⏳0:00:06
- // sortKey=1 1|↺7|RUNNING|▶16:30:28.433|Σ0:04:05|Δ0:00:06|⏳0:03:59
- // sortKey=1 0|↺7|RUNNING|▶16:36:18.350|Σ0:05:00|Δ0:01:42|⏳0:03:18
- // sortKey=1 2|↺7|RUNNING|▶16:38:37.816|Σ0:02:00|Δ0:01:09|⏳0:00:51
- // ▶ = "current" time (when updated)
- // Σ = total time
- // Δ = time elapsed
- // ⏳ = time remaining
- val sortKey = notification.sortKey
- val (_, _, state, extra) = sortKey.split("|", limit = 4)
- return when (state) {
- "PAUSED" -> {
- val (total, _, remaining) = extra.split("|")
- val timeRemaining = parseTimeDelta(remaining)
- TimerContentModel(
- icon = icon,
- // TODO: b/352142761 - define and use a string resource rather than " Timer".
- // (The UX isn't final so using " Timer" for now).
- name = total.replace("Σ", "") + " Timer",
- state =
- TimerContentModel.TimerState.Paused(
- timeRemaining = timeRemaining,
- resumeIntent = notification.findStartIntent(),
- addMinuteAction = notification.findAddMinuteAction(),
- resetAction = notification.findResetAction(),
- ),
- )
- }
- "RUNNING" -> {
- val (current, total, _, remaining) = extra.split("|")
- val finishTime = parseCurrentTime(current) + parseTimeDelta(remaining).toMillis()
- TimerContentModel(
- icon = icon,
- // TODO: b/352142761 - define and use a string resource rather than " Timer".
- // (The UX isn't final so using " Timer" for now).
- name = total.replace("Σ", "") + " Timer",
- state =
- TimerContentModel.TimerState.Running(
- finishTime = finishTime,
- pauseIntent = notification.findPauseIntent(),
- addMinuteAction = notification.findAddMinuteAction(),
- resetAction = notification.findResetAction(),
- ),
- )
- }
- else -> error("unknown state ($state) in sortKey=$sortKey")
- }
- }
-
- private fun Notification.findPauseIntent(): PendingIntent? {
- return actions
- .firstOrNull { it.actionIntent.intent?.action?.endsWith(".PAUSE_TIMER") == true }
- ?.actionIntent
- }
-
- private fun Notification.findStartIntent(): PendingIntent? {
- return actions
- .firstOrNull { it.actionIntent.intent?.action?.endsWith(".START_TIMER") == true }
- ?.actionIntent
- }
-
- // TODO: b/352142761 - switch to system attributes for label and icon.
- // - We probably want a consistent look for the Reset button. (Double check with UX.)
- // - Using the custom assets now since I couldn't an existing "Reset" icon.
- private fun Notification.findResetAction(): Notification.Action? {
- return actions.firstOrNull {
- it.actionIntent.intent?.action?.endsWith(".RESET_TIMER") == true
- }
- }
-
- // TODO: b/352142761 - check with UX on whether this should be required.
- // - Alternative is to allow for optional actions in addition to main and reset.
- // - For optional actions, we should take the custom label and icon.
- private fun Notification.findAddMinuteAction(): Notification.Action? {
- return actions.firstOrNull {
- it.actionIntent.intent?.action?.endsWith(".ADD_MINUTE_TIMER") == true
- }
- }
-
- private fun parseCurrentTime(current: String): Long {
- val (hour, minute, second, millis) = current.replace("▶", "").split(":", ".")
- // NOTE: this won't work correctly at/around midnight. It's just for prototyping.
- val localDateTime =
- LocalDateTime.of(
- LocalDate.now(),
- LocalTime.of(hour.toInt(), minute.toInt(), second.toInt(), millis.toInt() * 1000000),
- )
- val offset = ZoneId.systemDefault().rules.getOffset(localDateTime)
- return localDateTime.toInstant(offset).toEpochMilli()
- }
-
- private fun parseTimeDelta(delta: String): Duration {
- val (hour, minute, second) = delta.replace("Σ", "").replace("⏳", "").split(":")
- return Duration.ofHours(hour.toLong())
- .plusMinutes(minute.toLong())
- .plusSeconds(second.toLong())
- }
-
- private fun parseEnRouteNotification(
- notification: Notification,
- icon: IconModel,
- ): EnRouteContentModel {
- return EnRouteContentModel(
- smallIcon = icon,
- title = notification.extras.getCharSequence(Notification.EXTRA_TITLE),
- text = notification.extras.getCharSequence(Notification.EXTRA_TEXT),
- )
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt
deleted file mode 100644
index 77c4130..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * 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.notification.row
-
-import android.app.Notification
-import android.content.Context
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder
-import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView
-import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView
-import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
-import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
-import com.android.systemui.statusbar.notification.row.ui.view.EnRouteView
-import com.android.systemui.statusbar.notification.row.ui.view.TimerView
-import com.android.systemui.statusbar.notification.row.ui.viewbinder.EnRouteViewBinder
-import com.android.systemui.statusbar.notification.row.ui.viewbinder.TimerViewBinder
-import com.android.systemui.statusbar.notification.row.ui.viewmodel.EnRouteViewModel
-import com.android.systemui.statusbar.notification.row.ui.viewmodel.RichOngoingViewModelComponent
-import com.android.systemui.statusbar.notification.row.ui.viewmodel.TimerViewModel
-import javax.inject.Inject
-import kotlinx.coroutines.DisposableHandle
-
-fun interface DeferredContentViewBinder {
- fun setupContentViewBinder(): DisposableHandle
-}
-
-enum class RichOngoingNotificationViewType {
- Contracted,
- Expanded,
- HeadsUp,
-}
-
-/**
- * * Supertype of the 3 different possible result types of
- * [RichOngoingNotificationViewInflater.inflateView].
- */
-sealed interface ContentViewInflationResult {
-
- /** Indicates that the content view should be removed if present. */
- data object NullContentView : ContentViewInflationResult
-
- /**
- * Indicates that the content view (which *must be* present) should be unmodified during this
- * inflation.
- */
- data object KeepExistingView : ContentViewInflationResult
-
- /**
- * Contains the new view and binder that should replace any existing content view for this slot.
- */
- data class InflatedContentViewHolder(val view: View, val binder: DeferredContentViewBinder) :
- ContentViewInflationResult
-}
-
-fun ContentViewInflationResult?.shouldDisposeViewBinder() = this !is KeepExistingView
-
-/**
- * Interface which provides a [RichOngoingContentModel] for a given [Notification] when one is
- * applicable to the given style.
- */
-interface RichOngoingNotificationViewInflater {
- fun inflateView(
- contentModel: RichOngoingContentModel,
- existingView: View?,
- entry: NotificationEntry,
- systemUiContext: Context,
- parentView: ViewGroup,
- viewType: RichOngoingNotificationViewType,
- ): ContentViewInflationResult
-
- fun canKeepView(
- contentModel: RichOngoingContentModel,
- existingView: View?,
- viewType: RichOngoingNotificationViewType
- ): Boolean
-}
-
-@SysUISingleton
-class RichOngoingNotificationViewInflaterImpl
-@Inject
-constructor(
- private val viewModelComponentFactory: RichOngoingViewModelComponent.Factory,
-) : RichOngoingNotificationViewInflater {
-
- override fun inflateView(
- contentModel: RichOngoingContentModel,
- existingView: View?,
- entry: NotificationEntry,
- systemUiContext: Context,
- parentView: ViewGroup,
- viewType: RichOngoingNotificationViewType,
- ): ContentViewInflationResult {
- if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return NullContentView
- val component = viewModelComponentFactory.create(entry)
- return when (contentModel) {
- is TimerContentModel ->
- inflateTimerView(
- existingView,
- component::createTimerViewModel,
- systemUiContext,
- parentView,
- viewType
- )
- is EnRouteContentModel ->
- inflateEnRouteView(
- existingView,
- component::createEnRouteViewModel,
- systemUiContext,
- parentView,
- viewType
- )
- else -> TODO("Not yet implemented")
- }
- }
-
- override fun canKeepView(
- contentModel: RichOngoingContentModel,
- existingView: View?,
- viewType: RichOngoingNotificationViewType
- ): Boolean {
- if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return false
- return when (contentModel) {
- is TimerContentModel -> canKeepTimerView(contentModel, existingView, viewType)
- is EnRouteContentModel -> canKeepEnRouteView(contentModel, existingView, viewType)
- else -> TODO("Not yet implemented")
- }
- }
-
- private fun inflateTimerView(
- existingView: View?,
- createViewModel: () -> TimerViewModel,
- systemUiContext: Context,
- parentView: ViewGroup,
- viewType: RichOngoingNotificationViewType,
- ): ContentViewInflationResult {
- if (existingView is TimerView && !existingView.isReinflateNeeded()) return KeepExistingView
-
- return when (viewType) {
- RichOngoingNotificationViewType.Contracted -> {
- val newView =
- LayoutInflater.from(systemUiContext)
- .inflate(
- R.layout.rich_ongoing_timer_notification,
- parentView,
- /* attachToRoot= */ false
- ) as TimerView
- InflatedContentViewHolder(newView) {
- TimerViewBinder.bindWhileAttached(newView, createViewModel())
- }
- }
- RichOngoingNotificationViewType.Expanded,
- RichOngoingNotificationViewType.HeadsUp -> NullContentView
- }
- }
-
- private fun canKeepTimerView(
- contentModel: TimerContentModel,
- existingView: View?,
- viewType: RichOngoingNotificationViewType
- ): Boolean = true
-
- private fun inflateEnRouteView(
- existingView: View?,
- createViewModel: () -> EnRouteViewModel,
- systemUiContext: Context,
- parentView: ViewGroup,
- viewType: RichOngoingNotificationViewType,
- ): ContentViewInflationResult {
- if (existingView is EnRouteView && !existingView.isReinflateNeeded())
- return KeepExistingView
- return when (viewType) {
- RichOngoingNotificationViewType.Contracted -> {
- val newView =
- LayoutInflater.from(systemUiContext)
- .inflate(
- R.layout.notification_template_en_route_contracted,
- parentView,
- /* attachToRoot= */ false
- ) as EnRouteView
- InflatedContentViewHolder(newView) {
- EnRouteViewBinder.bindWhileAttached(newView, createViewModel())
- }
- }
- RichOngoingNotificationViewType.Expanded -> {
- val newView =
- LayoutInflater.from(systemUiContext)
- .inflate(
- R.layout.notification_template_en_route_expanded,
- parentView,
- /* attachToRoot= */ false
- ) as EnRouteView
- InflatedContentViewHolder(newView) {
- EnRouteViewBinder.bindWhileAttached(newView, createViewModel())
- }
- }
- RichOngoingNotificationViewType.HeadsUp -> NullContentView
- }
- }
-
- private fun canKeepEnRouteView(
- contentModel: EnRouteContentModel,
- existingView: View?,
- viewType: RichOngoingNotificationViewType
- ): Boolean = true
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepository.kt
deleted file mode 100644
index bac887b..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepository.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.notification.row.data.repository
-
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
-import kotlinx.coroutines.flow.StateFlow
-
-/** A repository of states relating to a specific notification row. */
-interface NotificationRowRepository {
- /**
- * A flow of an immutable data class with the current state of the Rich Ongoing Notification
- * content, if applicable.
- */
- val richOngoingContentModel: StateFlow<RichOngoingContentModel?>
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt
deleted file mode 100644
index 72823a7..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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.notification.row.domain.interactor
-
-import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
-import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel
-import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filterIsInstance
-
-/** Interactor specific to a particular notification row. */
-class NotificationRowInteractor @Inject constructor(repository: NotificationRowRepository) {
- /** Content of a rich ongoing timer notification. */
- val timerContentModel: Flow<TimerContentModel> =
- repository.richOngoingContentModel.filterIsInstance<TimerContentModel>()
-
- /** Content of a rich ongoing timer notification. */
- val enRouteContentModel: Flow<EnRouteContentModel> =
- repository.richOngoingContentModel.filterIsInstance<EnRouteContentModel>()
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/EnRouteContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/EnRouteContentModel.kt
deleted file mode 100644
index 7e78cca..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/EnRouteContentModel.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.notification.row.shared
-
-/**
- * Represents something en route.
- *
- * @param smallIcon the main small icon of the EnRoute notification.
- * @param title the title of the EnRoute notification.
- * @param text the text of the EnRoute notification.
- */
-data class EnRouteContentModel(
- val smallIcon: IconModel,
- val title: CharSequence?,
- val text: CharSequence?,
-) : RichOngoingContentModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/IconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/IconModel.kt
deleted file mode 100644
index e611938..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/IconModel.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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.notification.row.shared
-
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.Icon
-
-// TODO: figure out how to support lazy resolution of the drawable, e.g. on unrelated text change
-class IconModel(val icon: Icon) {
- var drawable: Drawable? = null
-
- override fun equals(other: Any?): Boolean =
- when (other) {
- null -> false
- (other === this) -> true
- !is IconModel -> false
- else -> other.icon.sameAs(icon)
- }
-
- override fun toString(): String = "IconModel(icon=$icon, drawable=$drawable)"
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/NotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/NotificationContentModel.kt
index 0f9a5a3..004c66b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/NotificationContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/NotificationContentModel.kt
@@ -22,7 +22,4 @@
val headsUpStatusBarModel: HeadsUpStatusBarModel,
val singleLineViewModel: SingleLineViewModel? = null,
val publicSingleLineViewModel: SingleLineViewModel? = null,
- val richOngoingContentModel: RichOngoingContentModel? = null,
)
-
-sealed interface RichOngoingContentModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
deleted file mode 100644
index 33b2564..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * 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.notification.row.shared
-
-import android.app.Notification
-import android.app.PendingIntent
-import java.time.Duration
-
-/**
- * Represents a simple timer that counts down to a time.
- *
- * @param name the label for the timer
- * @param state state of the timer, including time and whether it is paused or running
- */
-data class TimerContentModel(
- val icon: IconModel,
- val name: String,
- val state: TimerState,
-) : RichOngoingContentModel {
- /** The state (paused or running) of the timer, and relevant time */
- sealed interface TimerState {
- val addMinuteAction: Notification.Action?
- val resetAction: Notification.Action?
-
- /**
- * Indicates a running timer
- *
- * @param finishTime the time in ms since epoch that the timer will finish
- * @param pauseIntent the action for pausing the timer
- */
- data class Running(
- val finishTime: Long,
- val pauseIntent: PendingIntent?,
- override val addMinuteAction: Notification.Action?,
- override val resetAction: Notification.Action?,
- ) : TimerState
-
- /**
- * Indicates a paused timer
- *
- * @param timeRemaining the time in ms remaining on the paused timer
- * @param resumeIntent the action for resuming the timer
- */
- data class Paused(
- val timeRemaining: Duration,
- val resumeIntent: PendingIntent?,
- override val addMinuteAction: Notification.Action?,
- override val resetAction: Notification.Action?,
- ) : TimerState
- }
-}
-
-/**
- * Represents a simple stopwatch that counts up and allows tracking laps.
- *
- * @param state state of the stopwatch, including time and whether it is paused or running
- * @param lapDurations a list of durations of each completed lap
- */
-data class StopwatchContentModel(
- val icon: IconModel,
- val state: StopwatchState,
- val lapDurations: List<Long>,
-) : RichOngoingContentModel {
- /** The state (paused or running) of the stopwatch, and relevant time */
- sealed interface StopwatchState {
- /**
- * Indicates a running stopwatch
- *
- * @param startTime the time in ms since epoch that the stopwatch started, plus any
- * accumulated pause time
- * @param pauseIntent the action for pausing the stopwatch
- */
- data class Running(
- val startTime: Long,
- val pauseIntent: PendingIntent,
- ) : StopwatchState
-
- /**
- * Indicates a paused stopwatch
- *
- * @param timeElapsed the time in ms elapsed on the stopwatch
- * @param resumeIntent the action for resuming the stopwatch
- */
- data class Paused(
- val timeElapsed: Duration,
- val resumeIntent: PendingIntent,
- ) : StopwatchState
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingNotificationFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingNotificationFlag.kt
deleted file mode 100644
index 4a7f7cd..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingNotificationFlag.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.notification.row.shared
-
-import android.app.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the api rich ongoing flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object RichOngoingNotificationFlag {
- /** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_API_RICH_ONGOING
-
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
-
- /** Is the refactor enabled */
- @JvmStatic
- inline val isEnabled
- get() = Flags.apiRichOngoing()
-
- /**
- * Called to ensure code is only run when the flag is enabled. This protects users from the
- * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
- * build to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun isUnexpectedlyInLegacyMode() =
- RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
- /**
- * Called to ensure code is only run when the flag is disabled. This will throw an exception if
- * the flag is enabled to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/ConfigurationTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/ConfigurationTracker.kt
deleted file mode 100644
index 95c507c..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/ConfigurationTracker.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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.notification.row.ui.view
-
-import android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS
-import android.content.pm.ActivityInfo.CONFIG_DENSITY
-import android.content.pm.ActivityInfo.CONFIG_FONT_SCALE
-import android.content.pm.ActivityInfo.CONFIG_LAYOUT_DIRECTION
-import android.content.pm.ActivityInfo.CONFIG_LOCALE
-import android.content.pm.ActivityInfo.CONFIG_UI_MODE
-import android.content.res.Configuration
-import android.content.res.Resources
-
-/**
- * Tracks the active configuration when constructed and returns (when queried) whether the
- * configuration has unhandled changes.
- */
-class ConfigurationTracker(
- private val resources: Resources,
- private val unhandledConfigChanges: Int
-) {
- private val initialConfig = Configuration(resources.configuration)
-
- constructor(
- resources: Resources,
- handlesDensityFontScale: Boolean = false,
- handlesTheme: Boolean = false,
- handlesLocaleAndLayout: Boolean = true,
- ) : this(
- resources,
- unhandledConfigChanges =
- (if (handlesDensityFontScale) 0 else CONFIG_DENSITY or CONFIG_FONT_SCALE) or
- (if (handlesTheme) 0 else CONFIG_ASSETS_PATHS or CONFIG_UI_MODE) or
- (if (handlesLocaleAndLayout) 0 else CONFIG_LOCALE or CONFIG_LAYOUT_DIRECTION)
- )
-
- /**
- * Whether the current configuration has unhandled changes relative to the initial configuration
- */
- fun hasUnhandledConfigChange(): Boolean =
- initialConfig.diff(resources.configuration) and unhandledConfigChanges != 0
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/EnRouteView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/EnRouteView.kt
deleted file mode 100644
index e5c2b5f..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/EnRouteView.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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.notification.row.ui.view
-
-import android.content.Context
-import android.graphics.drawable.Icon
-import android.util.AttributeSet
-import android.widget.FrameLayout
-import android.widget.ImageView
-import android.widget.TextView
-import com.android.internal.R
-import com.android.internal.widget.NotificationExpandButton
-
-class EnRouteView
-@JvmOverloads
-constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0,
- defStyleRes: Int = 0,
-) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
-
- private val configTracker = ConfigurationTracker(resources)
-
- private lateinit var icon: ImageView
- private lateinit var title: TextView
- private lateinit var text: TextView
- private lateinit var expandButton: NotificationExpandButton
-
- override fun onFinishInflate() {
- super.onFinishInflate()
- icon = requireViewById(R.id.icon)
- title = requireViewById(R.id.title)
- text = requireViewById(R.id.text)
-
- expandButton = requireViewById(R.id.expand_button)
- expandButton.setExpanded(false)
- }
-
- /** the resources configuration has changed such that the view needs to be reinflated */
- fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange()
-
- fun setIcon(icon: Icon?) {
- this.icon.setImageIcon(icon)
- }
-
- fun setTitle(title: CharSequence?) {
- this.title.text = title
- }
-
- fun setText(text: CharSequence?) {
- this.text.text = text
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
deleted file mode 100644
index 8c95187..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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.notification.row.ui.view
-
-import android.annotation.DrawableRes
-import android.content.Context
-import android.graphics.BlendMode
-import android.util.AttributeSet
-import com.android.internal.widget.EmphasizedNotificationButton
-
-class TimerButtonView
-@JvmOverloads
-constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0,
- defStyleRes: Int = 0,
-) : EmphasizedNotificationButton(context, attrs, defStyleAttr, defStyleRes) {
-
- private val Int.dp: Int
- get() = (this * context.resources.displayMetrics.density).toInt()
-
- fun setIcon(@DrawableRes icon: Int) {
- val drawable = context.getDrawable(icon)
-
- drawable?.mutate()
- drawable?.setTintList(textColors)
- drawable?.setTintBlendMode(BlendMode.SRC_IN)
- drawable?.setBounds(0, 0, 24.dp, 24.dp)
-
- setCompoundDrawablesRelative(drawable, null, null, null)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
deleted file mode 100644
index d481b50..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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.notification.row.ui.view
-
-import android.content.Context
-import android.graphics.drawable.Icon
-import android.os.SystemClock
-import android.util.AttributeSet
-import android.widget.Chronometer
-import android.widget.ImageView
-import android.widget.TextView
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.core.view.isVisible
-import com.android.systemui.res.R
-
-class TimerView
-@JvmOverloads
-constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0,
- defStyleRes: Int = 0,
-) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
-
- private val configTracker = ConfigurationTracker(resources)
-
- private lateinit var icon: ImageView
- private lateinit var label: TextView
- private lateinit var chronometer: Chronometer
- private lateinit var pausedTimeRemaining: TextView
- lateinit var mainButton: TimerButtonView
- private set
-
- lateinit var altButton: TimerButtonView
- private set
-
- lateinit var resetButton: TimerButtonView
- private set
-
- override fun onFinishInflate() {
- super.onFinishInflate()
- icon = requireViewById(R.id.icon)
- label = requireViewById(R.id.label)
- chronometer = requireViewById(R.id.chronoRemaining)
- pausedTimeRemaining = requireViewById(R.id.pausedTimeRemaining)
- mainButton = requireViewById(R.id.mainButton)
- altButton = requireViewById(R.id.altButton)
- resetButton = requireViewById(R.id.resetButton)
- }
-
- /** the resources configuration has changed such that the view needs to be reinflated */
- fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange()
-
- fun setIcon(icon: Icon?) {
- this.icon.setImageIcon(icon)
- }
-
- fun setLabel(label: String) {
- this.label.text = label
- }
-
- fun setPausedTime(pausedTime: String?) {
- if (pausedTime != null) {
- pausedTimeRemaining.text = pausedTime
- pausedTimeRemaining.isVisible = true
- } else {
- pausedTimeRemaining.isVisible = false
- }
- }
-
- fun setCountdownTime(countdownTimeMs: Long?) {
- if (countdownTimeMs != null) {
- chronometer.base =
- countdownTimeMs - System.currentTimeMillis() + SystemClock.elapsedRealtime()
- chronometer.isVisible = true
- chronometer.start()
- } else {
- chronometer.isVisible = false
- chronometer.stop()
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/EnRouteViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/EnRouteViewBinder.kt
deleted file mode 100644
index 3b8957c..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/EnRouteViewBinder.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.notification.row.ui.viewbinder
-
-import androidx.lifecycle.lifecycleScope
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.statusbar.notification.row.ui.view.EnRouteView
-import com.android.systemui.statusbar.notification.row.ui.viewmodel.EnRouteViewModel
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-
-/** Binds a [EnRouteView] to its [view model][EnRouteViewModel]. */
-object EnRouteViewBinder {
- fun bindWhileAttached(
- view: EnRouteView,
- viewModel: EnRouteViewModel,
- ): DisposableHandle {
- return view.repeatWhenAttached { lifecycleScope.launch { bind(view, viewModel) } }
- }
-
- suspend fun bind(
- view: EnRouteView,
- viewModel: EnRouteViewModel,
- ) = coroutineScope {
- launch { viewModel.icon.collect { view.setIcon(it) } }
- launch { viewModel.title.collect { view.setTitle(it) } }
- launch { viewModel.text.collect { view.setText(it) } }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
deleted file mode 100644
index 042d1bc..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.notification.row.ui.viewbinder
-
-import android.content.res.ColorStateList
-import android.graphics.drawable.Icon
-import android.view.View
-import androidx.core.view.isGone
-import androidx.lifecycle.lifecycleScope
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
-import com.android.systemui.statusbar.notification.row.ui.view.TimerView
-import com.android.systemui.statusbar.notification.row.ui.viewmodel.TimerViewModel
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-
-/** Binds a [TimerView] to its [view model][TimerViewModel]. */
-object TimerViewBinder {
- fun bindWhileAttached(
- view: TimerView,
- viewModel: TimerViewModel,
- ): DisposableHandle {
- return view.repeatWhenAttached { lifecycleScope.launch { bind(view, viewModel) } }
- }
-
- suspend fun bind(
- view: TimerView,
- viewModel: TimerViewModel,
- ) = coroutineScope {
- launch { viewModel.icon.collect { view.setIcon(it) } }
- launch { viewModel.label.collect { view.setLabel(it) } }
- launch { viewModel.pausedTime.collect { view.setPausedTime(it) } }
- launch { viewModel.countdownTime.collect { view.setCountdownTime(it) } }
- launch { viewModel.mainButtonModel.collect { bind(view.mainButton, it) } }
- launch { viewModel.altButtonModel.collect { bind(view.altButton, it) } }
- launch { viewModel.resetButtonModel.collect { bind(view.resetButton, it) } }
- }
-
- fun bind(buttonView: TimerButtonView, model: TimerViewModel.ButtonViewModel?) {
- if (model != null) {
- buttonView.setButtonBackground(
- ColorStateList.valueOf(
- buttonView.context.getColor(com.android.internal.R.color.system_accent2_100)
- )
- )
- buttonView.setTextColor(
- buttonView.context.getColor(
- com.android.internal.R.color.notification_primary_text_color_light
- )
- )
-
- when (model) {
- is TimerViewModel.ButtonViewModel.WithSystemAttrs -> {
- buttonView.setIcon(model.iconRes)
- buttonView.setText(model.labelRes)
- }
- is TimerViewModel.ButtonViewModel.WithCustomAttrs -> {
- // TODO: b/352142761 - is there a better way to deal with TYPE_RESOURCE icons
- // with empty resPackage? RemoteViews handles this by using a different
- // `contextForResources` for inflation.
- val icon =
- if (model.icon.type == Icon.TYPE_RESOURCE && model.icon.resPackage == "")
- Icon.createWithResource(
- "com.google.android.deskclock",
- model.icon.resId
- )
- else model.icon
- buttonView.setImageIcon(icon)
- buttonView.text = model.label
- }
- }
-
- buttonView.setOnClickListener(
- model.pendingIntent?.let { pendingIntent ->
- View.OnClickListener { pendingIntent.send() }
- }
- )
- buttonView.isEnabled = model.pendingIntent != null
- }
- buttonView.isGone = model == null
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModel.kt
deleted file mode 100644
index 307a983..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModel.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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.notification.row.ui.viewmodel
-
-import android.graphics.drawable.Icon
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
-import com.android.systemui.util.kotlin.FlowDumperImpl
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
-
-/** A view model for EnRoute notifications. */
-class EnRouteViewModel
-@Inject
-constructor(
- dumpManager: DumpManager,
- rowInteractor: NotificationRowInteractor,
-) : FlowDumperImpl(dumpManager) {
- init {
- /* check if */ RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()
- }
-
- val icon: Flow<Icon?> = rowInteractor.enRouteContentModel.mapNotNull { it.smallIcon.icon }
-
- val title: Flow<CharSequence?> = rowInteractor.enRouteContentModel.map { it.title }
-
- val text: Flow<CharSequence?> = rowInteractor.enRouteContentModel.map { it.text }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt
deleted file mode 100644
index 5552d89..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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.notification.row.ui.viewmodel
-
-// noinspection CleanArchitectureDependencyViolation
-import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
-import dagger.BindsInstance
-import dagger.Subcomponent
-
-@Subcomponent
-interface RichOngoingViewModelComponent {
-
- @Subcomponent.Factory
- interface Factory {
- /** Creates an instance of [RichOngoingViewModelComponent]. */
- fun create(
- @BindsInstance repository: NotificationRowRepository
- ): RichOngoingViewModelComponent
- }
-
- fun createTimerViewModel(): TimerViewModel
-
- fun createEnRouteViewModel(): EnRouteViewModel
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
deleted file mode 100644
index 768a093..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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.notification.row.ui.viewmodel
-
-import android.annotation.DrawableRes
-import android.annotation.StringRes
-import android.app.PendingIntent
-import android.graphics.drawable.Icon
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
-import com.android.systemui.statusbar.notification.row.shared.TimerContentModel.TimerState
-import com.android.systemui.util.kotlin.FlowDumperImpl
-import java.time.Duration
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
-
-/** A view model for Timer notifications. */
-class TimerViewModel
-@Inject
-constructor(
- dumpManager: DumpManager,
- rowInteractor: NotificationRowInteractor,
-) : FlowDumperImpl(dumpManager) {
- init {
- /* check if */ RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()
- }
-
- private val state: Flow<TimerState> = rowInteractor.timerContentModel.mapNotNull { it.state }
-
- val icon: Flow<Icon?> = rowInteractor.timerContentModel.mapNotNull { it.icon.icon }
-
- val label: Flow<String> = rowInteractor.timerContentModel.mapNotNull { it.name }
-
- val countdownTime: Flow<Long?> = state.map { (it as? TimerState.Running)?.finishTime }
-
- val pausedTime: Flow<String?> =
- state.map { (it as? TimerState.Paused)?.timeRemaining?.format() }
-
- val mainButtonModel: Flow<ButtonViewModel> =
- state.map {
- when (it) {
- is TimerState.Paused ->
- ButtonViewModel.WithSystemAttrs(
- it.resumeIntent,
- com.android.systemui.res.R.string.controls_media_resume, // "Resume",
- com.android.systemui.res.R.drawable.ic_media_play
- )
- is TimerState.Running ->
- ButtonViewModel.WithSystemAttrs(
- it.pauseIntent,
- com.android.systemui.res.R.string.controls_media_button_pause, // "Pause",
- com.android.systemui.res.R.drawable.ic_media_pause
- )
- }
- }
-
- val altButtonModel: Flow<ButtonViewModel?> =
- state.map {
- it.addMinuteAction?.let { action ->
- ButtonViewModel.WithCustomAttrs(
- action.actionIntent,
- action.title, // "1:00",
- action.getIcon()
- )
- }
- }
-
- val resetButtonModel: Flow<ButtonViewModel?> =
- state.map {
- it.resetAction?.let { action ->
- ButtonViewModel.WithCustomAttrs(
- action.actionIntent,
- action.title, // "Reset",
- action.getIcon()
- )
- }
- }
-
- sealed interface ButtonViewModel {
- val pendingIntent: PendingIntent?
-
- data class WithSystemAttrs(
- override val pendingIntent: PendingIntent?,
- @StringRes val labelRes: Int,
- @DrawableRes val iconRes: Int,
- ) : ButtonViewModel
-
- data class WithCustomAttrs(
- override val pendingIntent: PendingIntent?,
- val label: CharSequence,
- val icon: Icon,
- ) : ButtonViewModel
- }
-}
-
-private fun Duration.format(): String {
- val hours = this.toHours()
- return if (hours > 0) {
- String.format("%d:%02d:%02d", hours, toMinutesPart(), toSecondsPart())
- } else {
- String.format("%d:%02d", toMinutes(), toSecondsPart())
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 5ae24f7..479ffb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -55,6 +55,7 @@
import com.android.keyguard.KeyguardViewController;
import com.android.keyguard.TrustGrantFlags;
import com.android.keyguard.ViewMediatorCallback;
+import com.android.systemui.DejankUtils;
import com.android.systemui.Flags;
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
@@ -1203,6 +1204,11 @@
@Override
public void hide(long startTime, long fadeoutDuration) {
Trace.beginSection("StatusBarKeyguardViewManager#hide");
+ if (Flags.checkLockscreenGoneTransition()) {
+ DejankUtils.notifyRendererOfExpensiveFrame(
+ mNotificationShadeWindowController.getWindowRootView(),
+ "StatusBarKeyguardViewManager#hide");
+ }
mKeyguardStateController.notifyKeyguardState(false,
mKeyguardStateController.isOccluded());
launchPendingWakeupAction();
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 eea4c21..9c168be 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
@@ -95,7 +95,8 @@
primaryChipView.show(shouldAnimateChange = true)
is OngoingActivityChipModel.Hidden ->
primaryChipView.hide(
- shouldAnimateChange = primaryChipModel.shouldAnimate
+ state = View.GONE,
+ shouldAnimateChange = primaryChipModel.shouldAnimate,
)
}
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt
new file mode 100644
index 0000000..441cbb3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.policy
+
+import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onStart
+
+/** [DevicePostureController.getDevicePosture] as a [Flow]. */
+@DevicePostureInt
+fun DevicePostureController.devicePosture(): Flow<Int> =
+ conflatedCallbackFlow {
+ val callback = DevicePostureController.Callback { posture -> trySend(posture) }
+ addCallback(callback)
+ awaitClose { removeCallback(callback) }
+ }
+ .onStart { emit(devicePosture) }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
index 6acc891..94e19de 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
@@ -20,19 +20,27 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.android.systemui.inputdevice.tutorial.ui.composable.DoneButton
import com.android.systemui.res.R
@@ -47,20 +55,17 @@
Column(
verticalArrangement = Arrangement.Center,
modifier =
- Modifier.background(
- color = MaterialTheme.colorScheme.surfaceContainer,
- )
- .fillMaxSize()
+ Modifier.background(color = MaterialTheme.colorScheme.surfaceContainer).fillMaxSize(),
) {
TutorialSelectionButtons(
onBackTutorialClicked = onBackTutorialClicked,
onHomeTutorialClicked = onHomeTutorialClicked,
onRecentAppsTutorialClicked = onRecentAppsTutorialClicked,
- modifier = Modifier.padding(60.dp)
+ modifier = Modifier.padding(60.dp),
)
DoneButton(
onDoneButtonClicked = onDoneButtonClicked,
- modifier = Modifier.padding(horizontal = 60.dp)
+ modifier = Modifier.padding(horizontal = 60.dp),
)
}
}
@@ -70,30 +75,36 @@
onBackTutorialClicked: () -> Unit,
onHomeTutorialClicked: () -> Unit,
onRecentAppsTutorialClicked: () -> Unit,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(20.dp),
verticalAlignment = Alignment.CenterVertically,
- modifier = modifier
+ modifier = modifier,
) {
TutorialButton(
text = stringResource(R.string.touchpad_tutorial_home_gesture_button),
+ icon = Icons.AutoMirrored.Outlined.ArrowBack,
+ iconColor = MaterialTheme.colorScheme.onPrimary,
onClick = onHomeTutorialClicked,
- color = MaterialTheme.colorScheme.primary,
- modifier = Modifier.weight(1f)
+ backgroundColor = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.weight(1f),
)
TutorialButton(
text = stringResource(R.string.touchpad_tutorial_back_gesture_button),
+ icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon),
+ iconColor = MaterialTheme.colorScheme.onTertiary,
onClick = onBackTutorialClicked,
- color = MaterialTheme.colorScheme.tertiary,
- modifier = Modifier.weight(1f)
+ backgroundColor = MaterialTheme.colorScheme.tertiary,
+ modifier = Modifier.weight(1f),
)
TutorialButton(
text = stringResource(R.string.touchpad_tutorial_recent_apps_gesture_button),
+ icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_recents_icon),
+ iconColor = MaterialTheme.colorScheme.onSecondary,
onClick = onRecentAppsTutorialClicked,
- color = MaterialTheme.colorScheme.secondary,
- modifier = Modifier.weight(1f)
+ backgroundColor = MaterialTheme.colorScheme.secondary,
+ modifier = Modifier.weight(1f),
)
}
}
@@ -101,16 +112,30 @@
@Composable
private fun TutorialButton(
text: String,
+ icon: ImageVector,
+ iconColor: Color,
onClick: () -> Unit,
- color: Color,
- modifier: Modifier = Modifier
+ backgroundColor: Color,
+ modifier: Modifier = Modifier,
) {
Button(
onClick = onClick,
shape = RoundedCornerShape(16.dp),
- colors = ButtonDefaults.buttonColors(containerColor = color),
- modifier = modifier.aspectRatio(0.66f)
+ colors = ButtonDefaults.buttonColors(containerColor = backgroundColor),
+ modifier = modifier.aspectRatio(0.66f),
) {
- Text(text = text, style = MaterialTheme.typography.headlineLarge)
+ Column(
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = text,
+ modifier = Modifier.width(30.dp).height(30.dp),
+ tint = iconColor,
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(text = text, style = MaterialTheme.typography.headlineLarge)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt
deleted file mode 100644
index 869b3c6..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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.volume.dialog
-
-import android.app.Dialog
-import android.content.Context
-import android.os.Bundle
-import android.view.ContextThemeWrapper
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.res.R
-import javax.inject.Inject
-
-class NewVolumeDialog @Inject constructor(@Application context: Context) :
- Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.volume_dialog)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt
deleted file mode 100644
index b93714a..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.volume.dialog
-
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.plugins.VolumeDialog
-import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
-import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-
-class NewVolumeDialogPlugin
-@Inject
-constructor(
- @Application private val applicationCoroutineScope: CoroutineScope,
- private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory,
-) : VolumeDialog {
-
- private var volumeDialogPluginComponent: VolumeDialogPluginComponent? = null
- private var job: Job? = null
-
- override fun init(windowType: Int, callback: VolumeDialog.Callback?) {
- job =
- applicationCoroutineScope.launch {
- coroutineScope {
- volumeDialogPluginComponent = volumeDialogPluginComponentFactory.create(this)
- }
- }
- }
-
- private fun showDialog() {
- val volumeDialogPluginComponent =
- volumeDialogPluginComponent ?: error("Creating dialog before init was called")
- volumeDialogPluginComponent.coroutineScope().launch {
- coroutineScope {
- val volumeDialogComponent: VolumeDialogComponent =
- volumeDialogPluginComponent.volumeDialogComponentFactory().create(this)
- with(volumeDialogComponent.volumeDialog()) {
- setOnDismissListener { volumeDialogComponent.coroutineScope().cancel() }
- show()
- }
- }
- }
- }
-
- override fun destroy() {
- job?.cancel()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
index 74e823e..7476c6a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
@@ -20,15 +20,39 @@
import android.content.Context
import android.os.Bundle
import android.view.ContextThemeWrapper
+import android.view.MotionEvent
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.res.R
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.ui.binder.VolumeDialogBinder
import javax.inject.Inject
-class VolumeDialog @Inject constructor(@Application context: Context) :
- Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
+class VolumeDialog
+@Inject
+constructor(
+ @Application context: Context,
+ private val dialogBinder: VolumeDialogBinder,
+ private val visibilityInteractor: VolumeDialogVisibilityInteractor,
+) : Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.volume_dialog)
+ dialogBinder.bind(this)
+ }
+
+ /**
+ * NOTE: This will be called with ACTION_OUTSIDE MotionEvents for touches that occur outside of
+ * the touchable region of the volume dialog (as returned by [.onComputeInternalInsets]) even if
+ * those touches occurred within the bounds of the volume dialog.
+ */
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ if (isShowing) {
+ if (event.action == MotionEvent.ACTION_OUTSIDE) {
+ visibilityInteractor.dismissDialog(Events.DISMISS_REASON_TOUCH_OUTSIDE)
+ return true
+ }
+ }
+ return false
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
index a2e81d9..4b7a978 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
@@ -18,12 +18,10 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.plugins.VolumeDialog
-import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@@ -34,31 +32,17 @@
private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory,
) : VolumeDialog {
- private var volumeDialogPluginComponent: VolumeDialogPluginComponent? = null
private var job: Job? = null
override fun init(windowType: Int, callback: VolumeDialog.Callback?) {
job =
applicationCoroutineScope.launch {
coroutineScope {
- volumeDialogPluginComponent = volumeDialogPluginComponentFactory.create(this)
- }
- }
- }
+ val component = volumeDialogPluginComponentFactory.create(this)
- private fun showDialog() {
- val volumeDialogPluginComponent =
- volumeDialogPluginComponent ?: error("Creating dialog before init was called")
- volumeDialogPluginComponent.coroutineScope().launch {
- coroutineScope {
- val volumeDialogComponent: VolumeDialogComponent =
- volumeDialogPluginComponent.volumeDialogComponentFactory().create(this)
- with(volumeDialogComponent.volumeDialog()) {
- setOnDismissListener { volumeDialogComponent.coroutineScope().cancel() }
- show()
+ component.viewModel().activate()
}
}
- }
}
override fun destroy() {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
index 82612a7..4e0098c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
@@ -19,6 +19,7 @@
import com.android.systemui.volume.dialog.dagger.module.VolumeDialogPluginModule
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogPluginViewModel
import dagger.BindsInstance
import dagger.Subcomponent
import kotlinx.coroutines.CoroutineScope
@@ -31,15 +32,7 @@
@Subcomponent(modules = [VolumeDialogPluginModule::class])
interface VolumeDialogPluginComponent {
- /**
- * Provides a coroutine scope to use inside [VolumeDialogPluginScope].
- * [com.android.systemui.volume.dialog.VolumeDialogPlugin] manages the lifecycle of this scope.
- * It's cancelled when the dialog is disposed. This helps to free occupied resources when volume
- * dialog is not shown.
- */
- @VolumeDialogPlugin fun coroutineScope(): CoroutineScope
-
- fun volumeDialogComponentFactory(): VolumeDialogComponent.Factory
+ fun viewModel(): VolumeDialogPluginViewModel
@Subcomponent.Factory
interface Factory {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
index ec7c6ce..2e26fd6 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
@@ -20,7 +20,8 @@
import android.os.Handler
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.plugins.VolumeDialogController
-import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
import com.android.systemui.volume.dialog.domain.model.VolumeDialogStateModel
import javax.inject.Inject
@@ -40,12 +41,12 @@
*
* @see VolumeDialogController.Callbacks
*/
-@VolumeDialog
+@VolumeDialogPluginScope
class VolumeDialogCallbacksInteractor
@Inject
constructor(
private val volumeDialogController: VolumeDialogController,
- @VolumeDialog private val coroutineScope: CoroutineScope,
+ @VolumeDialogPlugin private val coroutineScope: CoroutineScope,
@Background private val bgHandler: Handler,
) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
index dd51108..4a709a44b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
@@ -17,7 +17,8 @@
package com.android.systemui.volume.dialog.domain.interactor
import com.android.systemui.plugins.VolumeDialogController
-import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
import com.android.systemui.volume.dialog.domain.model.VolumeDialogStateModel
import javax.inject.Inject
@@ -35,13 +36,13 @@
*
* @see [VolumeDialogController]
*/
-@VolumeDialog
+@VolumeDialogPluginScope
class VolumeDialogStateInteractor
@Inject
constructor(
volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor,
private val volumeDialogController: VolumeDialogController,
- @VolumeDialog private val coroutineScope: CoroutineScope,
+ @VolumeDialogPlugin private val coroutineScope: CoroutineScope,
) {
val volumeDialogState: Flow<VolumeDialogStateModel> =
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
new file mode 100644
index 0000000..6c92754
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import android.annotation.SuppressLint
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+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.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.update
+
+private val maxDialogShowTime: Duration = 3.seconds
+
+/**
+ * Handles Volume Dialog visibility state. It might change from several sources:
+ * - [com.android.systemui.plugins.VolumeDialogController] requests visibility change;
+ * - it might be dismissed by the inactivity timeout;
+ * - it can be dismissed by the user;
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumeDialogPluginScope
+class VolumeDialogVisibilityInteractor
+@Inject
+constructor(
+ @VolumeDialogPlugin coroutineScope: CoroutineScope,
+ callbacksInteractor: VolumeDialogCallbacksInteractor,
+) {
+
+ @SuppressLint("SharedFlowCreation")
+ private val mutableDismissDialogEvents = MutableSharedFlow<Unit>()
+ private val mutableDialogVisibility =
+ MutableStateFlow<VolumeDialogVisibilityModel>(VolumeDialogVisibilityModel.Invisible)
+
+ val dialogVisibility: Flow<VolumeDialogVisibilityModel> = mutableDialogVisibility.asStateFlow()
+
+ init {
+ merge(
+ mutableDismissDialogEvents.mapLatest {
+ delay(maxDialogShowTime)
+ VolumeDialogEventModel.DismissRequested(Events.DISMISS_REASON_TIMEOUT)
+ },
+ callbacksInteractor.event,
+ )
+ .onEach { event ->
+ VolumeDialogVisibilityModel.fromEvent(event)?.let { model ->
+ mutableDialogVisibility.value = model
+ if (model is VolumeDialogVisibilityModel.Visible) {
+ resetDismissTimeout()
+ }
+ }
+ }
+ .launchIn(coroutineScope)
+ }
+
+ /**
+ * Dismisses the dialog with a given [reason]. The new state will be emitted in the
+ * [dialogVisibility].
+ */
+ fun dismissDialog(reason: Int) {
+ mutableDialogVisibility.update {
+ if (it is VolumeDialogVisibilityModel.Dismissed) {
+ it
+ } else {
+ VolumeDialogVisibilityModel.Dismissed(reason)
+ }
+ }
+ }
+
+ /** Resets current dialog timeout. */
+ suspend fun resetDismissTimeout() {
+ mutableDismissDialogEvents.emit(Unit)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt
new file mode 100644
index 0000000..646445d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.volume.dialog.domain.model
+
+/** Models current Volume Dialog visibility state. */
+sealed interface VolumeDialogVisibilityModel {
+
+ /** Dialog is currently visible. */
+ data class Visible(val reason: Int, val keyguardLocked: Boolean, val lockTaskModeState: Int) :
+ VolumeDialogVisibilityModel
+
+ /** Dialog has never been shown. So it's just invisible. */
+ interface Invisible : VolumeDialogVisibilityModel {
+ companion object : Invisible
+ }
+
+ /** Dialog has been shown and then dismissed. */
+ data class Dismissed(val reason: Int) : Invisible
+
+ companion object {
+
+ /**
+ * Creates [VolumeDialogVisibilityModel] from appropriate events and returns null otherwise.
+ */
+ fun fromEvent(event: VolumeDialogEventModel): VolumeDialogVisibilityModel? {
+ return when (event) {
+ is VolumeDialogEventModel.DismissRequested -> Dismissed(event.reason)
+ is VolumeDialogEventModel.ShowRequested ->
+ Visible(event.reason, event.keyguardLocked, event.lockTaskModeState)
+ else -> null
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt
new file mode 100644
index 0000000..59c38c0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.volume.dialog.shared
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.VolumeLog
+import com.android.systemui.volume.Events
+import javax.inject.Inject
+
+private const val TAG = "SysUI_VolumeDialog"
+
+/** Logs events related to the Volume Panel. */
+class VolumeDialogLogger @Inject constructor(@VolumeLog private val logBuffer: LogBuffer) {
+
+ fun onShow(reason: Int) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { int1 = reason },
+ { "Show: ${Events.SHOW_REASONS[int1]}" },
+ )
+ }
+
+ fun onDismiss(reason: Int) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { int1 = reason },
+ { "Dismiss: ${Events.DISMISS_REASONS[int1]}" },
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
new file mode 100644
index 0000000..3f2c39b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.volume.dialog.ui.binder
+
+import android.app.Dialog
+import android.graphics.Color
+import android.graphics.PixelFormat
+import android.graphics.drawable.ColorDrawable
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogGravityViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+@VolumeDialogScope
+class VolumeDialogBinder
+@Inject
+constructor(
+ @VolumeDialog private val coroutineScope: CoroutineScope,
+ private val volumeDialogViewBinder: VolumeDialogViewBinder,
+ private val gravityViewModel: VolumeDialogGravityViewModel,
+) {
+
+ fun bind(dialog: Dialog) {
+ with(dialog) {
+ setupWindow(window!!)
+ dialog.setContentView(R.layout.volume_dialog)
+
+ val volumeDialogView: View = dialog.requireViewById(R.id.volume_dialog_container)
+ volumeDialogView.repeatWhenAttached {
+ lifecycleScope.launch { volumeDialogViewBinder.bind(volumeDialogView) }
+ }
+ }
+ }
+
+ /** Configures [Window] for the [Dialog]. */
+ private fun setupWindow(window: Window) =
+ with(window) {
+ clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+ addFlags(
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+ WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
+ WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
+ )
+ addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
+
+ requestFeature(Window.FEATURE_NO_TITLE)
+ setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
+ setWindowAnimations(-1)
+ setFormat(PixelFormat.TRANSLUCENT)
+
+ attributes =
+ attributes.apply {
+ title = "VolumeDialog" // Not the same as Window#setTitle
+ }
+ setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+
+ gravityViewModel.dialogGravity.onEach { window.setGravity(it) }.launchIn(coroutineScope)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt
new file mode 100644
index 0000000..df6523c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.volume.dialog.ui.viewmodel
+
+import android.content.Context
+import android.content.res.Configuration
+import android.view.Gravity
+import androidx.annotation.GravityInt
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.UiBackground
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.DevicePostureController
+import com.android.systemui.statusbar.policy.devicePosture
+import com.android.systemui.statusbar.policy.onConfigChanged
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+@VolumeDialogScope
+class VolumeDialogGravityViewModel
+@Inject
+constructor(
+ @Application private val context: Context,
+ @VolumeDialog private val coroutineScope: CoroutineScope,
+ @UiBackground private val uiBackgroundCoroutineContext: CoroutineContext,
+ configurationController: ConfigurationController,
+ private val devicePostureController: DevicePostureController,
+) {
+
+ @GravityInt private var originalGravity: Int = context.getAbsoluteGravity()
+
+ val dialogGravity: Flow<Int> =
+ combine(
+ devicePostureController.devicePosture(),
+ configurationController.onConfigChanged.onEach { onConfigurationChanged() },
+ ) { devicePosture, configuration ->
+ context.calculateGravity(devicePosture, configuration)
+ }
+ .stateIn(
+ scope = coroutineScope,
+ started = SharingStarted.Eagerly,
+ context.calculateGravity(),
+ )
+
+ private suspend fun onConfigurationChanged() {
+ withContext(uiBackgroundCoroutineContext) { originalGravity = context.getAbsoluteGravity() }
+ }
+
+ @GravityInt
+ private fun Context.calculateGravity(
+ devicePosture: Int = devicePostureController.devicePosture,
+ config: Configuration = resources.configuration,
+ ): Int {
+ val isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE
+ val isHalfOpen = devicePosture == DevicePostureController.DEVICE_POSTURE_HALF_OPENED
+ val gravity =
+ if (isLandscape && isHalfOpen) {
+ originalGravity or Gravity.TOP
+ } else {
+ originalGravity
+ }
+ return getAbsoluteGravity(gravity)
+ }
+}
+
+@GravityInt
+private fun Context.getAbsoluteGravity(
+ gravity: Int = resources.getInteger(R.integer.volume_dialog_gravity)
+): Int = with(resources) { Gravity.getAbsoluteGravity(gravity, configuration.layoutDirection) }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
new file mode 100644
index 0000000..329a947
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.volume.dialog.ui.viewmodel
+
+import android.app.Dialog
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import com.android.systemui.volume.dialog.shared.VolumeDialogLogger
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumeDialogPluginScope
+class VolumeDialogPluginViewModel
+@Inject
+constructor(
+ private val componentFactory: VolumeDialogComponent.Factory,
+ private val dialogVisibilityInteractor: VolumeDialogVisibilityInteractor,
+ private val controller: VolumeDialogController,
+ private val logger: VolumeDialogLogger,
+) : ExclusiveActivatable() {
+
+ override suspend fun onActivated(): Nothing {
+ coroutineScope {
+ dialogVisibilityInteractor.dialogVisibility
+ .mapLatest { visibilityModel ->
+ with(visibilityModel) {
+ if (this is VolumeDialogVisibilityModel.Visible) {
+ showDialog(reason, keyguardLocked, lockTaskModeState)
+ }
+ if (this is VolumeDialogVisibilityModel.Dismissed) {
+ Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason)
+ logger.onDismiss(reason)
+ }
+ }
+ }
+ .launchIn(this)
+ }
+ awaitCancellation()
+ }
+
+ suspend fun showDialog(reason: Int, keyguardLocked: Boolean, lockTaskModeState: Int): Unit =
+ coroutineScope {
+ logger.onShow(reason)
+
+ controller.notifyVisible(true)
+
+ val volumeDialogComponent: VolumeDialogComponent = componentFactory.create(this)
+ val dialog =
+ volumeDialogComponent.volumeDialog().apply {
+ setOnDismissListener {
+ volumeDialogComponent.coroutineScope().cancel()
+ dialogVisibilityInteractor.dismissDialog(Events.DISMISS_REASON_UNKNOWN)
+ }
+ }
+ launch { dialog.awaitShow() }
+
+ Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, keyguardLocked)
+ }
+}
+
+/** Shows [Dialog] until suspend function is cancelled. */
+private suspend fun Dialog.awaitShow() =
+ suspendCancellableCoroutine<Unit> {
+ show()
+ it.invokeOnCancellation { dismiss() }
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
index f9e91ae..30c8c15 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
@@ -19,11 +19,12 @@
import com.android.systemui.lifecycle.ExclusiveActivatable
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
class VolumeDialogViewModel @AssistedInject constructor() : ExclusiveActivatable() {
override suspend fun onActivated(): Nothing {
- TODO("Not yet implemented")
+ awaitCancellation()
}
@AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 5f6ad92..02d0b57 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -61,7 +61,7 @@
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.wm.shell.desktopmode.DesktopMode;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedEventCallback;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
@@ -393,7 +393,7 @@
void initDesktopMode(DesktopMode desktopMode) {
desktopMode.addVisibleTasksListener(
- new DesktopModeTaskRepository.VisibleTasksListener() {
+ new DesktopRepository.VisibleTasksListener() {
@Override
public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) {
if (displayId == Display.DEFAULT_DISPLAY) {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
index 9cd5215..8206c21 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
@@ -85,8 +85,9 @@
`when`(telephonyManager.createForSubscriptionId(anyInt())).thenReturn(telephonyManager)
`when`(telephonyManager.supplyIccLockPin(anyString())).thenReturn(mock())
simPinView =
- LayoutInflater.from(context).inflate(R.layout.keyguard_sim_pin_view, null)
- as KeyguardSimPinView
+ LayoutInflater.from(context)
+ .inflate(R.layout.keyguard_sim_pin_view, null)
+ .requireViewById(R.id.keyguard_sim_pin_view) as KeyguardSimPinView
val fakeFeatureFlags = FakeFeatureFlags()
val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index 8075d11..6061063 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -19,6 +19,7 @@
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static com.android.systemui.Flags.FLAG_CLIPBOARD_SHARED_TRANSITIONS;
+import static com.android.systemui.Flags.FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EXPANDED_FROM_MINIMIZED;
@@ -166,6 +167,7 @@
}
@Test
+ @DisableFlags(FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE)
public void test_setClipData_invalidImageData_legacy() {
initController();
@@ -238,6 +240,7 @@
}
@Test
+ @DisableFlags(FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE)
public void test_setClipData_invalidImageData() {
initController();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
index 3ed0977..1d74e8b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
@@ -17,12 +17,12 @@
package com.android.systemui.screenshot
import android.content.ComponentName
-import androidx.test.ext.junit.runners.AndroidJUnit4
import android.graphics.Insets
import android.graphics.Rect
import android.os.UserHandle
import android.view.Display
import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.internal.util.ScreenshotRequest
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -71,15 +71,6 @@
}
@Test
- fun testNegativeUserId() {
- val request = ScreenshotRequest.Builder(type, source).setUserId(-1).build()
-
- val data = ScreenshotData.fromRequest(request)
-
- assertThat(data.userHandle).isNull()
- }
-
- @Test
fun testPackageNameAsString() {
val request = ScreenshotRequest.Builder(type, source).setTopComponent(component).build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index 0bea560..27e9f07 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -20,6 +20,7 @@
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.FakeDisplayRepository
+import com.android.systemui.display.data.repository.FakeFocusedDisplayRepository
import com.android.systemui.display.data.repository.display
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
@@ -58,6 +59,7 @@
private val testScope = TestScope(UnconfinedTestDispatcher())
private val eventLogger = UiEventLoggerFake()
private val headlessHandler = mock<HeadlessScreenshotHandler>()
+ private val focusedDisplayRepository = FakeFocusedDisplayRepository()
private val screenshotExecutor =
TakeScreenshotExecutorImpl(
@@ -68,6 +70,7 @@
eventLogger,
notificationControllerFactory,
headlessHandler,
+ focusedDisplayRepository,
)
@Before
@@ -309,6 +312,59 @@
}
@Test
+ @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_keyOther_usesFocusedDisplay() =
+ testScope.runTest {
+ val displayId = 1
+ setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = displayId))
+ val onSaved = { _: Uri? -> }
+ focusedDisplayRepository.emit(displayId)
+
+ screenshotExecutor.executeScreenshots(
+ createScreenshotRequest(
+ source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER
+ ),
+ onSaved,
+ callback,
+ )
+
+ val dataCaptor = ArgumentCaptor<ScreenshotData>()
+
+ verify(controller).handleScreenshot(dataCaptor.capture(), any(), any())
+
+ assertThat(dataCaptor.value.displayId).isEqualTo(displayId)
+
+ screenshotExecutor.onDestroy()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_keyOtherInvalidDisplay_usesDefault() =
+ testScope.runTest {
+ setDisplays(
+ display(TYPE_INTERNAL, id = Display.DEFAULT_DISPLAY),
+ display(TYPE_EXTERNAL, id = 1),
+ )
+ focusedDisplayRepository.emit(5) // invalid display
+ val onSaved = { _: Uri? -> }
+ screenshotExecutor.executeScreenshots(
+ createScreenshotRequest(
+ source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER
+ ),
+ onSaved,
+ callback,
+ )
+
+ val dataCaptor = ArgumentCaptor<ScreenshotData>()
+
+ verify(controller).handleScreenshot(dataCaptor.capture(), any(), any())
+
+ assertThat(dataCaptor.value.displayId).isEqualTo(Display.DEFAULT_DISPLAY)
+
+ screenshotExecutor.onDestroy()
+ }
+
+ @Test
fun onDestroy_propagatedToControllers() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt
index bab9bbb..2fcacb9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt
@@ -58,13 +58,13 @@
ScreenshotData(
TAKE_SCREENSHOT_FULLSCREEN,
SCREENSHOT_KEY_CHORD,
- null,
+ UserHandle.CURRENT,
topComponent = null,
screenBounds = Rect(0, 0, 1, 1),
taskId = -1,
insets = Insets.NONE,
bitmap = null,
- displayId = DEFAULT_DISPLAY
+ displayId = DEFAULT_DISPLAY,
)
/* Create a policy request processor with no capture policies */
@@ -75,7 +75,7 @@
policies = emptyList(),
defaultOwner = UserHandle.of(PERSONAL),
defaultComponent = ComponentName("default", "Component"),
- displayTasks = fullScreenWork
+ displayTasks = fullScreenWork,
)
val result = runBlocking { requestProcessor.process(request) }
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 f5a90196..0e9ef06 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -556,11 +556,13 @@
return null;
}).when(mView).setOnTouchListener(any(NotificationPanelViewController.TouchHandler.class));
- // Dreaming->Lockscreen
+ // Any edge transition
when(mKeyguardTransitionInteractor.transition(any()))
.thenReturn(emptyFlow());
when(mKeyguardTransitionInteractor.transition(any(), any()))
.thenReturn(emptyFlow());
+
+ // Dreaming->Lockscreen
when(mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha())
.thenReturn(emptyFlow());
when(mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY(anyInt()))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
index a099c9d..48608eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
@@ -35,9 +35,6 @@
import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder
-import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView
-import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
@@ -51,33 +48,23 @@
import com.android.systemui.statusbar.notification.row.shared.NewRemoteViews
import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
-import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
import com.android.systemui.statusbar.policy.InflatedSmartReplyState
import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder
import com.android.systemui.statusbar.policy.SmartReplyStateInflater
-import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
-import kotlinx.coroutines.DisposableHandle
import org.junit.Assert
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
-import org.mockito.kotlin.argThat
-import org.mockito.kotlin.clearInvocations
-import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
-import org.mockito.kotlin.inOrder
import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@SmallTest
@@ -118,45 +105,6 @@
}
}
- private var fakeRonContentModel: RichOngoingContentModel? = null
- private val fakeRonExtractor =
- object : RichOngoingNotificationContentExtractor {
- override fun extractContentModel(
- entry: NotificationEntry,
- builder: Notification.Builder,
- systemUIContext: Context,
- packageContext: Context
- ): RichOngoingContentModel? = fakeRonContentModel
- }
-
- private var fakeContractedRonViewHolder: ContentViewInflationResult = NullContentView
- private var fakeExpandedRonViewHolder: ContentViewInflationResult = NullContentView
- private var fakeHeadsUpRonViewHolder: ContentViewInflationResult = NullContentView
- private var fakeRonViewInflater =
- spy(
- object : RichOngoingNotificationViewInflater {
- override fun inflateView(
- contentModel: RichOngoingContentModel,
- existingView: View?,
- entry: NotificationEntry,
- systemUiContext: Context,
- parentView: ViewGroup,
- viewType: RichOngoingNotificationViewType
- ): ContentViewInflationResult =
- when (viewType) {
- RichOngoingNotificationViewType.Contracted -> fakeContractedRonViewHolder
- RichOngoingNotificationViewType.Expanded -> fakeExpandedRonViewHolder
- RichOngoingNotificationViewType.HeadsUp -> fakeHeadsUpRonViewHolder
- }
-
- override fun canKeepView(
- contentModel: RichOngoingContentModel,
- existingView: View?,
- viewType: RichOngoingNotificationViewType
- ): Boolean = false
- }
- )
-
@Before
fun setUp() {
allowTestableLooperAsMainThread()
@@ -167,15 +115,12 @@
.setContentText("Text")
.setStyle(Notification.BigTextStyle().bigText("big text"))
testHelper = NotificationTestHelper(mContext, mDependency)
- testHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL)
row = spy(testHelper.createRow(builder.build()))
notificationInflater =
NotificationRowContentBinderImpl(
cache,
mock(),
mock<ConversationNotificationProcessor>(),
- fakeRonExtractor,
- fakeRonViewInflater,
mock(),
smartReplyStateInflater,
layoutInflaterFactoryProvider,
@@ -405,496 +350,6 @@
}
@Test
- fun testRonModelRequiredForRonView() {
- fakeRonContentModel = null
- val contractedRonView = View(context)
- val expandedRonView = View(context)
- val headsUpRonView = View(context)
- fakeContractedRonViewHolder =
- InflatedContentViewHolder(view = contractedRonView, binder = mock())
- fakeExpandedRonViewHolder =
- InflatedContentViewHolder(view = expandedRonView, binder = mock())
- fakeHeadsUpRonViewHolder = InflatedContentViewHolder(view = headsUpRonView, binder = mock())
-
- // WHEN inflater inflates
- val contentToInflate =
- FLAG_CONTENT_VIEW_CONTRACTED or FLAG_CONTENT_VIEW_EXPANDED or FLAG_CONTENT_VIEW_HEADS_UP
- inflateAndWait(notificationInflater, contentToInflate, row)
- verifyNoMoreInteractions(fakeRonViewInflater)
- }
-
- @Test
- fun testRonModelCleansUpRemoteViews() {
- val ronView = View(context)
-
- val entry = row.entry
-
- fakeRonContentModel = mock<TimerContentModel>()
- fakeContractedRonViewHolder =
- InflatedContentViewHolder(view = ronView, binder = mock<DeferredContentViewBinder>())
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
-
- // VERIFY
- verify(cache).removeCachedView(eq(entry), eq(FLAG_CONTENT_VIEW_CONTRACTED))
- verify(cache).removeCachedView(eq(entry), eq(FLAG_CONTENT_VIEW_EXPANDED))
- verify(cache).removeCachedView(eq(entry), eq(FLAG_CONTENT_VIEW_HEADS_UP))
- }
-
- @Test
- fun testRonModelCleansUpSmartReplies() {
- val ronView = View(context)
-
- val privateLayout = spy(row.privateLayout)
-
- row.privateLayout = privateLayout
-
- fakeRonContentModel = mock<TimerContentModel>()
- fakeContractedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mock())
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
-
- // VERIFY
- verify(privateLayout).setExpandedInflatedSmartReplies(eq(null))
- verify(privateLayout).setHeadsUpInflatedSmartReplies(eq(null))
- }
-
- @Test
- fun testRonModelTriggersInflationOfContractedRonView() {
- val mockRonModel = mock<TimerContentModel>()
- val ronView = View(context)
- val mockBinder = mock<DeferredContentViewBinder>()
-
- val entry = row.entry
- val privateLayout = row.privateLayout
-
- fakeRonContentModel = mockRonModel
- fakeContractedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
-
- // VERIFY that the inflater is invoked
- verify(fakeRonViewInflater)
- .inflateView(
- eq(mockRonModel),
- any(),
- eq(entry),
- any(),
- eq(privateLayout),
- eq(RichOngoingNotificationViewType.Contracted)
- )
- assertThat(row.privateLayout.contractedChild).isSameInstanceAs(ronView)
- verify(mockBinder).setupContentViewBinder()
- }
-
- @Test
- fun testRonModelTriggersInflationOfExpandedRonView() {
- val mockRonModel = mock<TimerContentModel>()
- val ronView = View(context)
- val mockBinder = mock<DeferredContentViewBinder>()
-
- val entry = row.entry
- val privateLayout = row.privateLayout
-
- fakeRonContentModel = mockRonModel
- fakeExpandedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row)
-
- // VERIFY that the inflater is invoked
- verify(fakeRonViewInflater)
- .inflateView(
- eq(mockRonModel),
- any(),
- eq(entry),
- any(),
- eq(privateLayout),
- eq(RichOngoingNotificationViewType.Expanded)
- )
- assertThat(row.privateLayout.expandedChild).isSameInstanceAs(ronView)
- verify(mockBinder).setupContentViewBinder()
- }
-
- @Test
- fun testRonModelTriggersInflationOfHeadsUpRonView() {
- val mockRonModel = mock<TimerContentModel>()
- val ronView = View(context)
- val mockBinder = mock<DeferredContentViewBinder>()
-
- val entry = row.entry
- val privateLayout = row.privateLayout
-
- fakeRonContentModel = mockRonModel
- fakeHeadsUpRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row)
-
- // VERIFY that the inflater is invoked
- verify(fakeRonViewInflater)
- .inflateView(
- eq(mockRonModel),
- any(),
- eq(entry),
- any(),
- eq(privateLayout),
- eq(RichOngoingNotificationViewType.HeadsUp)
- )
- assertThat(row.privateLayout.headsUpChild).isSameInstanceAs(ronView)
- verify(mockBinder).setupContentViewBinder()
- }
-
- @Test
- fun keepExistingViewForContractedRonNotChangingContractedChild() {
- val oldHandle = mock<DisposableHandle>()
- val mockRonModel = mock<TimerContentModel>()
-
- row.privateLayout.mContractedBinderHandle = oldHandle
- val entry = spy(row.entry)
- row.entry = entry
- val privateLayout = spy(row.privateLayout)
- row.privateLayout = privateLayout
-
- fakeRonContentModel = mockRonModel
- fakeContractedRonViewHolder = KeepExistingView
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
-
- // THEN do not dispose old contracted binder handle and change contracted child
- verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
- verifyNoMoreInteractions(oldHandle)
- verify(privateLayout, never()).setContractedChild(any())
- }
-
- @Test
- fun keepExistingViewForExpandedRonNotChangingExpandedChild() {
- val oldHandle = mock<DisposableHandle>()
- val mockRonModel = mock<TimerContentModel>()
-
- row.privateLayout.mExpandedBinderHandle = oldHandle
- val entry = spy(row.entry)
- row.entry = entry
- val privateLayout = spy(row.privateLayout)
- row.privateLayout = privateLayout
-
- fakeRonContentModel = mockRonModel
- fakeExpandedRonViewHolder = KeepExistingView
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row)
-
- // THEN do not dispose old expanded binder handle and change expanded child
- verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
- verifyNoMoreInteractions(oldHandle)
- verify(privateLayout, never()).setExpandedChild(any())
- }
-
- @Test
- fun keepExistingViewForHeadsUpRonNotChangingHeadsUpChild() {
- val oldHandle = mock<DisposableHandle>()
- val mockRonModel = mock<TimerContentModel>()
-
- row.privateLayout.mHeadsUpBinderHandle = oldHandle
- val entry = spy(row.entry)
- row.entry = entry
- val privateLayout = spy(row.privateLayout)
- row.privateLayout = privateLayout
-
- fakeRonContentModel = mockRonModel
- fakeHeadsUpRonViewHolder = KeepExistingView
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row)
-
- // THEN - do not dispose old heads up binder handle and change heads up child
- verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
- verifyNoMoreInteractions(oldHandle)
- verify(privateLayout, never()).setHeadsUpChild(any())
- }
-
- @Test
- fun nullContentViewForContractedRonAppliesElementsInOrder() {
- val oldHandle = mock<DisposableHandle>()
- val mockRonModel = mock<TimerContentModel>()
-
- row.privateLayout.mContractedBinderHandle = oldHandle
- val entry = spy(row.entry)
- row.entry = entry
- val privateLayout = spy(row.privateLayout)
- row.privateLayout = privateLayout
-
- fakeRonContentModel = mockRonModel
- fakeContractedRonViewHolder = NullContentView
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
-
- // Validate that these 4 steps happen in this precise order
- inOrder(oldHandle, entry, privateLayout, cache) {
- verify(oldHandle).dispose()
- verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
- verify(privateLayout).setContractedChild(eq(null))
- }
- }
-
- @Test
- fun nullContentViewForExpandedRonAppliesElementsInOrder() {
- val oldHandle = mock<DisposableHandle>()
- val mockRonModel = mock<TimerContentModel>()
-
- row.privateLayout.mExpandedBinderHandle = oldHandle
- val entry = spy(row.entry)
- row.entry = entry
- val privateLayout = spy(row.privateLayout)
- row.privateLayout = privateLayout
-
- fakeRonContentModel = mockRonModel
- fakeExpandedRonViewHolder = NullContentView
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row)
-
- // Validate that these 4 steps happen in this precise order
- inOrder(oldHandle, entry, privateLayout, cache) {
- verify(oldHandle).dispose()
- verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
- verify(privateLayout).setExpandedChild(eq(null))
- }
- }
-
- @Test
- fun nullContentViewForHeadsUpRonAppliesElementsInOrder() {
- val oldHandle = mock<DisposableHandle>()
- val mockRonModel = mock<TimerContentModel>()
-
- row.privateLayout.mHeadsUpBinderHandle = oldHandle
- val entry = spy(row.entry)
- row.entry = entry
- val privateLayout = spy(row.privateLayout)
- row.privateLayout = privateLayout
-
- fakeRonContentModel = mockRonModel
- fakeHeadsUpRonViewHolder = NullContentView
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row)
-
- // Validate that these 4 steps happen in this precise order
- inOrder(oldHandle, entry, privateLayout, cache) {
- verify(oldHandle).dispose()
- verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
- verify(privateLayout).setHeadsUpChild(eq(null))
- }
- }
-
- @Test
- fun contractedRonViewAppliesElementsInOrder() {
- val oldHandle = mock<DisposableHandle>()
- val mockRonModel = mock<TimerContentModel>()
- val ronView = View(context)
- val mockBinder = mock<DeferredContentViewBinder>()
-
- row.privateLayout.mContractedBinderHandle = oldHandle
- val entry = spy(row.entry)
- row.entry = entry
- val privateLayout = spy(row.privateLayout)
- row.privateLayout = privateLayout
-
- fakeRonContentModel = mockRonModel
- fakeContractedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
-
- // Validate that these 4 steps happen in this precise order
- inOrder(oldHandle, entry, privateLayout, mockBinder) {
- verify(oldHandle).dispose()
- verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
- verify(privateLayout).setContractedChild(eq(ronView))
- verify(mockBinder).setupContentViewBinder()
- }
- }
-
- @Test
- fun expandedRonViewAppliesElementsInOrder() {
- val oldHandle = mock<DisposableHandle>()
- val mockRonModel = mock<TimerContentModel>()
- val ronView = View(context)
- val mockBinder = mock<DeferredContentViewBinder>()
-
- row.privateLayout.mExpandedBinderHandle = oldHandle
- val entry = spy(row.entry)
- row.entry = entry
- val privateLayout = spy(row.privateLayout)
- row.privateLayout = privateLayout
-
- fakeRonContentModel = mockRonModel
- fakeExpandedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row)
-
- // Validate that these 4 steps happen in this precise order
- inOrder(oldHandle, entry, privateLayout, mockBinder) {
- verify(oldHandle).dispose()
- verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
- verify(privateLayout).setExpandedChild(eq(ronView))
- verify(mockBinder).setupContentViewBinder()
- }
- }
-
- @Test
- fun headsUpRonViewAppliesElementsInOrder() {
- val oldHandle = mock<DisposableHandle>()
- val mockRonModel = mock<TimerContentModel>()
- val ronView = View(context)
- val mockBinder = mock<DeferredContentViewBinder>()
-
- row.privateLayout.mHeadsUpBinderHandle = oldHandle
- val entry = spy(row.entry)
- row.entry = entry
- val privateLayout = spy(row.privateLayout)
- row.privateLayout = privateLayout
-
- fakeRonContentModel = mockRonModel
- fakeHeadsUpRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
-
- // WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row)
-
- // Validate that these 4 steps happen in this precise order
- inOrder(oldHandle, entry, privateLayout, mockBinder) {
- verify(oldHandle).dispose()
- verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
- verify(privateLayout).setHeadsUpChild(eq(ronView))
- verify(mockBinder).setupContentViewBinder()
- }
- }
-
- @Test
- fun testRonNotReinflating() {
- val oldContractedBinderHandle = mock<DisposableHandle>()
- val oldExpandedBinderHandle = mock<DisposableHandle>()
- val oldHeadsUpBinderHandle = mock<DisposableHandle>()
-
- val contractedBinderHandle = mock<DisposableHandle>()
- val expandedBinderHandle = mock<DisposableHandle>()
- val headsUpBinderHandle = mock<DisposableHandle>()
-
- val contractedRonView = View(context)
- val expandedRonView = View(context)
- val headsUpRonView = View(context)
-
- val mockRonModel1 = mock<TimerContentModel>()
- val mockRonModel2 = mock<TimerContentModel>()
-
- val mockContractedViewBinder = mock<DeferredContentViewBinder>()
- val mockExpandedViewBinder = mock<DeferredContentViewBinder>()
- val mockHeadsUpViewBinder = mock<DeferredContentViewBinder>()
-
- doReturn(contractedBinderHandle).whenever(mockContractedViewBinder).setupContentViewBinder()
- doReturn(expandedBinderHandle).whenever(mockExpandedViewBinder).setupContentViewBinder()
- doReturn(headsUpBinderHandle).whenever(mockHeadsUpViewBinder).setupContentViewBinder()
-
- row.privateLayout.mContractedBinderHandle = oldContractedBinderHandle
- row.privateLayout.mExpandedBinderHandle = oldExpandedBinderHandle
- row.privateLayout.mHeadsUpBinderHandle = oldHeadsUpBinderHandle
- val entry = spy(row.entry)
- row.entry = entry
- val privateLayout = spy(row.privateLayout)
- row.privateLayout = privateLayout
-
- // WHEN inflater inflates both a model and a view
- fakeRonContentModel = mockRonModel1
- fakeContractedRonViewHolder =
- InflatedContentViewHolder(view = contractedRonView, binder = mockContractedViewBinder)
- fakeExpandedRonViewHolder =
- InflatedContentViewHolder(view = expandedRonView, binder = mockExpandedViewBinder)
- fakeHeadsUpRonViewHolder =
- InflatedContentViewHolder(view = headsUpRonView, binder = mockHeadsUpViewBinder)
-
- val contentToInflate =
- FLAG_CONTENT_VIEW_CONTRACTED or FLAG_CONTENT_VIEW_EXPANDED or FLAG_CONTENT_VIEW_HEADS_UP
- inflateAndWait(notificationInflater, contentToInflate, row)
-
- // Validate that these 4 steps happen in this precise order
- inOrder(
- oldContractedBinderHandle,
- oldExpandedBinderHandle,
- oldHeadsUpBinderHandle,
- entry,
- privateLayout,
- mockContractedViewBinder,
- mockExpandedViewBinder,
- mockHeadsUpViewBinder,
- contractedBinderHandle,
- expandedBinderHandle,
- headsUpBinderHandle
- ) {
- verify(oldContractedBinderHandle).dispose()
- verify(oldExpandedBinderHandle).dispose()
- verify(oldHeadsUpBinderHandle).dispose()
-
- verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel1 })
-
- verify(privateLayout).setContractedChild(eq(contractedRonView))
- verify(mockContractedViewBinder).setupContentViewBinder()
-
- verify(privateLayout).setExpandedChild(eq(expandedRonView))
- verify(mockExpandedViewBinder).setupContentViewBinder()
-
- verify(privateLayout).setHeadsUpChild(eq(headsUpRonView))
- verify(mockHeadsUpViewBinder).setupContentViewBinder()
-
- verify(contractedBinderHandle, never()).dispose()
- verify(expandedBinderHandle, never()).dispose()
- verify(headsUpBinderHandle, never()).dispose()
- }
-
- clearInvocations(
- oldContractedBinderHandle,
- oldExpandedBinderHandle,
- oldHeadsUpBinderHandle,
- entry,
- privateLayout,
- mockContractedViewBinder,
- mockExpandedViewBinder,
- mockHeadsUpViewBinder,
- contractedBinderHandle,
- expandedBinderHandle,
- headsUpBinderHandle
- )
-
- // THEN when the inflater inflates just a model
- fakeRonContentModel = mockRonModel2
- fakeContractedRonViewHolder = KeepExistingView
- fakeExpandedRonViewHolder = KeepExistingView
- fakeHeadsUpRonViewHolder = KeepExistingView
-
- inflateAndWait(notificationInflater, contentToInflate, row)
-
- // Validate that for reinflation, the only thing we do us update the model
- verify(contractedBinderHandle, never()).dispose()
- verify(expandedBinderHandle, never()).dispose()
- verify(headsUpBinderHandle, never()).dispose()
- verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel2 })
- verify(privateLayout, never()).setContractedChild(any())
- verify(privateLayout, never()).setExpandedChild(any())
- verify(privateLayout, never()).setHeadsUpChild(any())
- verify(mockContractedViewBinder, never()).setupContentViewBinder()
- verify(mockExpandedViewBinder, never()).setupContentViewBinder()
- verify(mockHeadsUpViewBinder, never()).setupContentViewBinder()
- verify(contractedBinderHandle, never()).dispose()
- verify(expandedBinderHandle, never()).dispose()
- verify(headsUpBinderHandle, never()).dispose()
- }
-
- @Test
fun testNotificationViewHeightTooSmallFailsValidation() {
val validationError =
getValidationError(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 75376e6..2340d02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -200,8 +200,6 @@
mock(NotifRemoteViewCache.class),
mock(NotificationRemoteInputManager.class),
mock(ConversationNotificationProcessor.class),
- mock(RichOngoingNotificationContentExtractor.class),
- mock(RichOngoingNotificationViewInflater.class),
mock(Executor.class),
new MockSmartReplyInflater(),
mock(NotifLayoutInflaterFactory.Provider.class),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelKosmos.kt
similarity index 67%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelKosmos.kt
index 3a7d7ba..8422942 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelKosmos.kt
@@ -14,10 +14,8 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.notification.row.domain.interactor
+package com.android.systemui.communal.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
-fun Kosmos.getNotificationRowInteractor(repository: NotificationRowRepository) =
- NotificationRowInteractor(repository = repository)
+val Kosmos.resizeableItemFrameViewModel by Kosmos.Fixture { ResizeableItemFrameViewModel() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt
new file mode 100644
index 0000000..83df5d8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.display.data.repository
+
+import android.view.Display
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+@SysUISingleton
+/** Fake [FocusedDisplayRepository] for testing. */
+class FakeFocusedDisplayRepository @Inject constructor() : FocusedDisplayRepository {
+ private val flow = MutableStateFlow<Int>(Display.DEFAULT_DISPLAY)
+
+ override val focusedDisplayId: StateFlow<Int>
+ get() = flow.asStateFlow()
+
+ suspend fun emit(focusedDisplay: Int) = flow.emit(focusedDisplay)
+}
+
+@Module
+interface FakeFocusedDisplayRepositoryModule {
+ @Binds fun bindFake(fake: FakeFocusedDisplayRepository): FocusedDisplayRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt
new file mode 100644
index 0000000..e4a2a87
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.plugins
+
+import android.media.AudioManager
+import android.media.AudioManager.CsdWarning
+import android.os.Handler
+import android.os.VibrationEffect
+import androidx.core.util.getOrElse
+import java.util.concurrent.CopyOnWriteArraySet
+
+class FakeVolumeDialogController(private val audioManager: AudioManager) : VolumeDialogController {
+
+ var isVisible: Boolean = false
+ private set
+
+ var hasScheduledTouchFeedback: Boolean = false
+ private set
+
+ var vibrationEffect: VibrationEffect? = null
+ private set
+
+ var hasUserActivity: Boolean = false
+ private set
+
+ private var hasVibrator: Boolean = true
+
+ private val state = VolumeDialogController.State()
+ private val callbacks = CopyOnWriteArraySet<VolumeDialogController.Callbacks>()
+
+ override fun setActiveStream(stream: Int) {
+ // ensure streamState existence for the active stream
+ state.states.getOrElse(stream) {
+ VolumeDialogController.StreamState().also { streamState ->
+ state.states.put(stream, streamState)
+ }
+ }
+ state.activeStream = stream
+ }
+
+ override fun setStreamVolume(stream: Int, userLevel: Int) {
+ val streamState =
+ state.states.getOrElse(stream) {
+ VolumeDialogController.StreamState().also { streamState ->
+ state.states.put(stream, streamState)
+ }
+ }
+ streamState.level = userLevel.coerceIn(streamState.levelMin, streamState.levelMax)
+ }
+
+ override fun setRingerMode(ringerModeNormal: Int, external: Boolean) {
+ if (external) {
+ state.ringerModeExternal = ringerModeNormal
+ } else {
+ state.ringerModeInternal = ringerModeNormal
+ }
+ }
+
+ fun setHasVibrator(hasVibrator: Boolean) {
+ this.hasVibrator = hasVibrator
+ }
+
+ override fun hasVibrator(): Boolean = hasVibrator
+
+ override fun vibrate(effect: VibrationEffect) {
+ vibrationEffect = effect
+ }
+
+ override fun scheduleTouchFeedback() {
+ hasScheduledTouchFeedback = true
+ }
+
+ fun resetScheduledTouchFeedback() {
+ hasScheduledTouchFeedback = false
+ }
+
+ override fun getAudioManager(): AudioManager = audioManager
+
+ override fun notifyVisible(visible: Boolean) {
+ isVisible = visible
+ }
+
+ override fun addCallback(callbacks: VolumeDialogController.Callbacks?, handler: Handler?) {
+ this.callbacks.add(callbacks)
+ }
+
+ override fun removeCallback(callbacks: VolumeDialogController.Callbacks?) {
+ this.callbacks.remove(callbacks)
+ }
+
+ override fun userActivity() {
+ hasUserActivity = true
+ }
+
+ fun resetUserActivity() {
+ hasUserActivity = false
+ }
+
+ override fun getState() {
+ callbacks.sendEvent { it.onStateChanged(state) }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowRequested */
+ fun onShowRequested(reason: Int, keyguardLocked: Boolean, lockTaskModeState: Int) {
+ callbacks.sendEvent { it.onShowRequested(reason, keyguardLocked, lockTaskModeState) }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onDismissRequested */
+ fun onDismissRequested(reason: Int) {
+ callbacks.sendEvent { it.onDismissRequested(reason) }
+ }
+
+ /**
+ * @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onLayoutDirectionChanged
+ */
+ fun onLayoutDirectionChanged(layoutDirection: Int) {
+ callbacks.sendEvent { it.onLayoutDirectionChanged(layoutDirection) }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onConfigurationChanged */
+ fun onConfigurationChanged() {
+ callbacks.sendEvent { it.onConfigurationChanged() }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowVibrateHint */
+ fun onShowVibrateHint() {
+ callbacks.sendEvent { it.onShowVibrateHint() }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowSilentHint */
+ fun onShowSilentHint() {
+ callbacks.sendEvent { it.onShowSilentHint() }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onScreenOff */
+ fun onScreenOff() {
+ callbacks.sendEvent { it.onScreenOff() }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowSafetyWarning */
+ fun onShowSafetyWarning(flags: Int) {
+ callbacks.sendEvent { it.onShowSafetyWarning(flags) }
+ }
+
+ /**
+ * @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onAccessibilityModeChanged
+ */
+ fun onAccessibilityModeChanged(showA11yStream: Boolean?) {
+ callbacks.sendEvent { it.onAccessibilityModeChanged(showA11yStream) }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowCsdWarning */
+ fun onShowCsdWarning(@CsdWarning csdWarning: Int, durationMs: Int) {
+ callbacks.sendEvent { it.onShowCsdWarning(csdWarning, durationMs) }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onVolumeChangedFromKey */
+ fun onVolumeChangedFromKey() {
+ callbacks.sendEvent { it.onVolumeChangedFromKey() }
+ }
+
+ override fun getCaptionsEnabledState(checkForSwitchState: Boolean) {
+ error("Unsupported for the new Volume Dialog")
+ }
+
+ override fun setCaptionsEnabledState(enabled: Boolean) {
+ error("Unsupported for the new Volume Dialog")
+ }
+
+ override fun getCaptionsComponentState(fromTooltip: Boolean) {
+ error("Unsupported for the new Volume Dialog")
+ }
+}
+
+private inline fun CopyOnWriteArraySet<VolumeDialogController.Callbacks>.sendEvent(
+ event: (callback: VolumeDialogController.Callbacks) -> Unit
+) {
+ for (callback in this) {
+ event(callback)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/VolumeDialogControllerKosmos.kt
similarity index 68%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/plugins/VolumeDialogControllerKosmos.kt
index 3a7d7ba..2f6d4fa 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/VolumeDialogControllerKosmos.kt
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.notification.row.domain.interactor
+package com.android.systemui.plugins
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import org.mockito.kotlin.mock
-fun Kosmos.getNotificationRowInteractor(repository: NotificationRowRepository) =
- NotificationRowInteractor(repository = repository)
+val Kosmos.fakeVolumeDialogController by Kosmos.Fixture { FakeVolumeDialogController(mock {}) }
+var Kosmos.volumeDialogController: VolumeDialogController by
+ Kosmos.Fixture { fakeVolumeDialogController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
index dbb3e38..c218ff6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
@@ -24,6 +24,7 @@
import com.android.systemui.qs.footerActionsController
import com.android.systemui.qs.footerActionsViewModelFactory
import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor
+import com.android.systemui.qs.panels.ui.viewmodel.paginatedGridViewModel
import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModel
import com.android.systemui.shade.largeScreenHeaderHelper
import com.android.systemui.shade.transition.largeScreenShadeInterpolator
@@ -48,6 +49,7 @@
configurationInteractor,
largeScreenHeaderHelper,
tileSquishinessInteractor,
+ paginatedGridViewModel,
lifecycleScope,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 020f0a6..4f414d9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -28,6 +28,7 @@
Scenes.Bouncer,
Scenes.Gone,
Scenes.Communal,
+ Scenes.Dream,
)
}
@@ -49,9 +50,10 @@
Scenes.Gone to 0,
Scenes.Lockscreen to 0,
Scenes.Communal to 1,
- Scenes.Shade to 2,
- Scenes.QuickSettings to 3,
- Scenes.Bouncer to 4,
+ Scenes.Dream to 2,
+ Scenes.Shade to 3,
+ Scenes.QuickSettings to 4,
+ Scenes.Bouncer to 5,
)
SceneContainerConfig(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
index 2eb1573..fc4f05d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
@@ -222,8 +222,6 @@
Mockito.mock(NotifRemoteViewCache::class.java, STUB_ONLY),
remoteInputManager,
conversationProcessor,
- Mockito.mock(RichOngoingNotificationContentExtractor::class.java, STUB_ONLY),
- Mockito.mock(RichOngoingNotificationViewInflater::class.java, STUB_ONLY),
Mockito.mock(Executor::class.java, STUB_ONLY),
smartReplyStateInflater,
notifLayoutInflaterFactoryProvider,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepositoryKosmos.kt
deleted file mode 100644
index 84ef4b5..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepositoryKosmos.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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.notification.row.data.repository
-
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
-import kotlinx.coroutines.flow.MutableStateFlow
-
-val Kosmos.fakeNotificationRowRepository by Fixture { FakeNotificationRowRepository() }
-
-class FakeNotificationRowRepository : NotificationRowRepository {
- override val richOngoingContentModel = MutableStateFlow<RichOngoingContentModel?>(null)
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelKosmos.kt
deleted file mode 100644
index 7e51135..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelKosmos.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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.notification.row.ui.viewmodel
-
-import com.android.systemui.dump.dumpManager
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
-import com.android.systemui.statusbar.notification.row.domain.interactor.getNotificationRowInteractor
-
-fun Kosmos.getEnRouteViewModel(repository: NotificationRowRepository) =
- EnRouteViewModel(
- dumpManager = dumpManager,
- rowInteractor = getNotificationRowInteractor(repository),
- )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelKosmos.kt
deleted file mode 100644
index 00f45b2..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelKosmos.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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.notification.row.ui.viewmodel
-
-import com.android.systemui.dump.dumpManager
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
-import com.android.systemui.statusbar.notification.row.domain.interactor.getNotificationRowInteractor
-
-fun Kosmos.getTimerViewModel(repository: NotificationRowRepository) =
- TimerViewModel(
- dumpManager = dumpManager,
- rowInteractor = getNotificationRowInteractor(repository),
- )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
new file mode 100644
index 0000000..db9c48d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import android.os.Handler
+import android.os.looper
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.plugins.volumeDialogController
+
+val Kosmos.volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor by
+ Kosmos.Fixture {
+ VolumeDialogCallbacksInteractor(
+ volumeDialogController = volumeDialogController,
+ coroutineScope = applicationCoroutineScope,
+ bgHandler = Handler(looper),
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
similarity index 68%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
index 3a7d7ba..e73539e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
@@ -14,10 +14,12 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.notification.row.domain.interactor
+package com.android.systemui.volume.dialog.domain.interactor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import com.android.systemui.kosmos.applicationCoroutineScope
-fun Kosmos.getNotificationRowInteractor(repository: NotificationRowRepository) =
- NotificationRowInteractor(repository = repository)
+val Kosmos.volumeDialogVisibilityInteractor by
+ Kosmos.Fixture {
+ VolumeDialogVisibilityInteractor(applicationCoroutineScope, volumeDialogCallbacksInteractor)
+ }
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index 86246e2..72f62c5 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -5,7 +5,8 @@
{ "name": "hoststubgen-test-tiny-test" },
{ "name": "hoststubgen-invoke-test" },
{ "name": "RavenwoodMockitoTest_device" },
- { "name": "RavenwoodBivalentTest_device" },
+ // TODO(b/371215487): Re-enable when the test is fixed.
+ // { "name": "RavenwoodBivalentTest_device" },
{ "name": "RavenwoodBivalentInstTest_nonself_inst" },
{ "name": "RavenwoodBivalentInstTest_self_inst_device" },
diff --git a/ravenwood/tests/coretest/Android.bp b/ravenwood/tests/coretest/Android.bp
index d94475c..85f1baf 100644
--- a/ravenwood/tests/coretest/Android.bp
+++ b/ravenwood/tests/coretest/Android.bp
@@ -17,9 +17,15 @@
"junit-params",
"platform-parametric-runner-lib",
"truth",
+
+ // This library should be removed by Ravenizer
+ "mockito-target-minus-junit4",
],
srcs: [
"test/**/*.java",
],
+ ravenizer: {
+ strip_mockito: true,
+ },
auto_gen_config: true,
}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMockitoTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMockitoTest.java
new file mode 100644
index 0000000..dd6d259
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMockitoTest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.ravenwoodtest.coretest;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+public class RavenwoodMockitoTest {
+
+ @Test
+ public void checkMockitoClasses() {
+ // DexMaker should not exist
+ assertThrows(
+ ClassNotFoundException.class,
+ () -> Class.forName("com.android.dx.DexMaker"));
+ // Mockito 2 should not exist
+ assertThrows(
+ ClassNotFoundException.class,
+ () -> Class.forName("org.mockito.Matchers"));
+ }
+}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
index f7f9a85..49f0b59 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
@@ -85,18 +85,17 @@
/**
* Main class.
*/
-class Ravenizer(val options: RavenizerOptions) {
- fun run() {
+class Ravenizer {
+ fun run(options: RavenizerOptions) {
val stats = RavenizerStats()
- val fatalValidation = options.fatalValidation.get
-
stats.totalTime = log.nTime {
process(
options.inJar.get,
options.outJar.get,
options.enableValidation.get,
- fatalValidation,
+ options.fatalValidation.get,
+ options.stripMockito.get,
stats,
)
}
@@ -108,6 +107,7 @@
outJar: String,
enableValidation: Boolean,
fatalValidation: Boolean,
+ stripMockito: Boolean,
stats: RavenizerStats,
) {
var allClasses = ClassNodes.loadClassStructures(inJar) {
@@ -126,6 +126,9 @@
}
}
}
+ if (includeUnsupportedMockito(allClasses)) {
+ log.w("Unsupported Mockito detected in $inJar}!")
+ }
stats.totalProcessTime = log.vTime("$executableName processing $inJar") {
ZipFile(inJar).use { inZip ->
@@ -145,6 +148,11 @@
)
}
+ if (stripMockito && entry.name.isMockitoFile()) {
+ // Skip this entry
+ continue
+ }
+
val className = zipEntryNameToClassName(entry.name)
if (className != null) {
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
index ff41818..aee4530 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
@@ -36,6 +36,6 @@
log.v("Options: $options")
// Run.
- Ravenizer(options).run()
+ Ravenizer().run(options)
}
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
index 10fe0a3..32dcbe5 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
@@ -47,6 +47,9 @@
/** Whether the validation failure is fatal or not. */
var fatalValidation: SetOnce<Boolean> = SetOnce(false),
+
+ /** Whether to remove mockito and dexmaker classes. */
+ var stripMockito: SetOnce<Boolean> = SetOnce(false),
) {
companion object {
@@ -85,6 +88,9 @@
"--fatal-validation" -> ret.fatalValidation.set(true)
"--no-fatal-validation" -> ret.fatalValidation.set(false)
+ "--strip-mockito" -> ret.stripMockito.set(true)
+ "--no-strip-mockito" -> ret.stripMockito.set(false)
+
else -> throw ArgumentsException("Unknown option: $arg")
}
} catch (e: SetOnce.SetMoreThanOnceException) {
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
index 1aa70c08..37a7975 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
@@ -100,3 +100,19 @@
// TODO -- anything else?
)
}
+
+/**
+ * Files that should be removed when "--strip-mockito" is set.
+ */
+fun String.isMockitoFile(): Boolean {
+ return this.startsWithAny(
+ "org/mockito/", // Mockito
+ "com/android/dx/", // DexMaker
+ "mockito-extensions/", // DexMaker overrides
+ )
+}
+
+fun includeUnsupportedMockito(classes: ClassNodes): Boolean {
+ return classes.findClass("com/android/dx/DexMaker") != null
+ || classes.findClass("org/mockito/Matchers") != null
+}
diff --git a/services/appfunctions/TEST_MAPPING b/services/appfunctions/TEST_MAPPING
index 1a9bdb6..851d754 100644
--- a/services/appfunctions/TEST_MAPPING
+++ b/services/appfunctions/TEST_MAPPING
@@ -6,13 +6,5 @@
{
"name": "CtsAppFunctionTestCases"
}
- ],
- "postsubmit": [
- {
- "name": "FrameworksAppFunctionsTests"
- },
- {
- "name": "CtsAppFunctionTestCases"
- }
]
}
\ No newline at end of file
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index d0c3daf..d31ced3 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -16,8 +16,6 @@
package com.android.server.appfunctions;
-import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DISABLED;
-import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED;
import static android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB;
import static android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE;
@@ -363,26 +361,14 @@
callingPackage,
functionIdentifier,
runtimeMetadataSearchSession));
- AppFunctionRuntimeMetadata.Builder newMetadata =
- new AppFunctionRuntimeMetadata.Builder(existingMetadata);
- switch (enabledState) {
- case AppFunctionManager.APP_FUNCTION_STATE_DEFAULT -> {
- newMetadata.setEnabled(null);
- }
- case APP_FUNCTION_STATE_ENABLED -> {
- newMetadata.setEnabled(true);
- }
- case APP_FUNCTION_STATE_DISABLED -> {
- newMetadata.setEnabled(false);
- }
- default ->
- throw new IllegalArgumentException("Value of EnabledState is unsupported.");
- }
+ AppFunctionRuntimeMetadata newMetadata =
+ new AppFunctionRuntimeMetadata.Builder(existingMetadata)
+ .setEnabled(enabledState).build();
AppSearchBatchResult<String, Void> putDocumentBatchResult =
runtimeMetadataSearchSession
.put(
new PutDocumentsRequest.Builder()
- .addGenericDocuments(newMetadata.build())
+ .addGenericDocuments(newMetadata)
.build())
.get();
if (!putDocumentBatchResult.isSuccess()) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 6fd281e..f5a297b 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -685,11 +685,6 @@
// default. Controlled by Settings.Global.FORCE_ENABLE_PSS_PROFILING
volatile boolean mForceEnablePssProfiling = false;
- // Indicates whether to use ApplicationInfo to determine launched state instead of PM user state
- // This is a temporary workaround until the trunk-stable flag is pushed to nextfood.
- // TODO: b/365979852 - remove this workaround when redundant
- volatile boolean mFlagUseAppInfoNotLaunched = false;
-
/**
* Indicates whether the foreground service background start restriction is enabled for
* caller app that is targeting S+.
@@ -1022,9 +1017,6 @@
private static final Uri FORCE_ENABLE_PSS_PROFILING_URI =
Settings.Global.getUriFor(Settings.Global.FORCE_ENABLE_PSS_PROFILING);
- private static final Uri ENABLE_USE_APP_INFO_NOT_LAUNCHED_URI =
- Settings.Global.getUriFor(Settings.Global.ENABLE_USE_APP_INFO_NOT_LAUNCHED);
-
/**
* The threshold to decide if a given association should be dumped into metrics.
*/
@@ -1487,7 +1479,6 @@
false, this);
}
mResolver.registerContentObserver(FORCE_ENABLE_PSS_PROFILING_URI, false, this);
- mResolver.registerContentObserver(ENABLE_USE_APP_INFO_NOT_LAUNCHED_URI, false, this);
updateConstants();
if (mSystemServerAutomaticHeapDumpEnabled) {
updateEnableAutomaticSystemServerHeapDumps();
@@ -1504,7 +1495,6 @@
updateActivityStartsLoggingEnabled();
updateForegroundServiceStartsLoggingEnabled();
updateForceEnablePssProfiling();
- updateEnableUseAppInfoNotLaunched();
// Read DropboxRateLimiter params from flags.
mService.initDropboxRateLimiter();
}
@@ -1550,8 +1540,6 @@
updateEnableAutomaticSystemServerHeapDumps();
} else if (FORCE_ENABLE_PSS_PROFILING_URI.equals(uri)) {
updateForceEnablePssProfiling();
- } else if (ENABLE_USE_APP_INFO_NOT_LAUNCHED_URI.equals(uri)) {
- updateEnableUseAppInfoNotLaunched();
}
}
@@ -1671,11 +1659,6 @@
Settings.Global.FORCE_ENABLE_PSS_PROFILING, 0) == 1;
}
- private void updateEnableUseAppInfoNotLaunched() {
- mFlagUseAppInfoNotLaunched = Settings.Global.getInt(mResolver,
- Settings.Global.ENABLE_USE_APP_INFO_NOT_LAUNCHED, 0) == 1;
- }
-
private void updateBackgroundActivityStarts() {
mFlagBackgroundActivityStartsEnabled = DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -2555,8 +2538,6 @@
pw.print(" OOMADJ_UPDATE_QUICK="); pw.println(OOMADJ_UPDATE_QUICK);
pw.print(" ENABLE_WAIT_FOR_FINISH_ATTACH_APPLICATION=");
pw.println(mEnableWaitForFinishAttachApplication);
- pw.print(" FLAG_USE_APP_INFO_NOT_LAUNCHED=");
- pw.println(mFlagUseAppInfoNotLaunched);
pw.print(" "); pw.print(KEY_FOLLOW_UP_OOMADJ_UPDATE_WAIT_DURATION);
pw.print("="); pw.println(FOLLOW_UP_OOMADJ_UPDATE_WAIT_DURATION);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 83bc75e..9219cc12 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -18398,25 +18398,34 @@
"Cannot kill the dependents of a package without its name.");
}
+ userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
+ userId, true, ALLOW_FULL_ONLY, "killPackageDependents", null);
+ final int[] userIds = mUserController.expandUserId(userId);
+
final long callingId = Binder.clearCallingIdentity();
IPackageManager pm = AppGlobals.getPackageManager();
- int pkgUid = -1;
try {
- pkgUid = pm.getPackageUid(packageName, MATCH_DEBUG_TRIAGED_MISSING, userId);
- } catch (RemoteException e) {
- }
- if (userId != UserHandle.USER_ALL && pkgUid == -1) {
- throw new IllegalArgumentException(
- "Cannot kill dependents of non-existing package " + packageName);
- }
- try {
- synchronized(this) {
- synchronized (mProcLock) {
- mProcessList.killPackageProcessesLSP(packageName, UserHandle.getAppId(pkgUid),
- userId, ProcessList.FOREGROUND_APP_ADJ,
- ApplicationExitInfo.REASON_DEPENDENCY_DIED,
- ApplicationExitInfo.SUBREASON_UNKNOWN,
- "dep: " + packageName);
+ for (int targetUserId : userIds) {
+ int pkgUid = -1;
+ try {
+ pkgUid = pm.getPackageUid(packageName, MATCH_DEBUG_TRIAGED_MISSING,
+ targetUserId);
+ } catch (RemoteException e) {
+ }
+ if (userId != UserHandle.USER_ALL && pkgUid == -1) {
+ throw new IllegalArgumentException(
+ "Cannot kill dependents of non-existing package " + packageName);
+ }
+ synchronized (this) {
+ synchronized (mProcLock) {
+ mProcessList.killPackageProcessesLSP(packageName,
+ UserHandle.getAppId(pkgUid),
+ targetUserId,
+ ProcessList.FOREGROUND_APP_ADJ,
+ ApplicationExitInfo.REASON_DEPENDENCY_DIED,
+ ApplicationExitInfo.SUBREASON_UNKNOWN,
+ "dep: " + packageName);
+ }
}
}
} finally {
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index a93ae72..57922d5 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -3401,8 +3401,7 @@
// Check if we should mark the processrecord for first launch after force-stopping
if (wasStopped) {
boolean wasEverLaunched = false;
- if (android.app.Flags.useAppInfoNotLaunched()
- || mService.mConstants.mFlagUseAppInfoNotLaunched) {
+ if (android.app.Flags.useAppInfoNotLaunched()) {
wasEverLaunched = !info.isNotLaunched();
} else {
try {
@@ -3423,8 +3422,7 @@
: STOPPED_STATE_FIRST_LAUNCH;
r.getWindowProcessController().setStoppedState(stoppedState);
} else {
- if (android.app.Flags.useAppInfoNotLaunched()
- || mService.mConstants.mFlagUseAppInfoNotLaunched) {
+ if (android.app.Flags.useAppInfoNotLaunched()) {
// If it was launched before, then it must be a force-stop
r.setWasForceStopped(wasEverLaunched);
} else {
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index cc66378..7873d34 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -198,13 +198,10 @@
flag {
name: "logcat_longer_timeout"
- namespace: "backstage_power"
+ namespace: "stability"
description: "Wait longer during the logcat gathering operation"
bug: "292533246"
is_fixed_read_only: true
- metadata {
- purpose: PURPOSE_BUGFIX
- }
}
flag {
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index e0cf96f..596e375 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -72,6 +72,9 @@
import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP;
import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_PROXY_OPERATION;
import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;
import android.Manifest;
@@ -160,6 +163,7 @@
import com.android.internal.pm.pkg.component.ParsedAttribution;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils;
import com.android.internal.util.function.pooled.PooledLambda;
@@ -2829,12 +2833,26 @@
@Override
public int checkOperation(int code, int uid, String packageName) {
+ if (Flags.appopAccessTrackingLoggingEnabled()) {
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED,
+ uid, code,
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION,
+ false);
+ }
return mCheckOpsDelegateDispatcher.checkOperation(code, uid, packageName, null,
Context.DEVICE_ID_DEFAULT, false /*raw*/);
}
@Override
public int checkOperationForDevice(int code, int uid, String packageName, int virtualDeviceId) {
+ if (Flags.appopAccessTrackingLoggingEnabled()) {
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED,
+ uid, code,
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION,
+ false);
+ }
return mCheckOpsDelegateDispatcher.checkOperation(code, uid, packageName, null,
virtualDeviceId, false /*raw*/);
}
@@ -3015,6 +3033,14 @@
public SyncNotedAppOp noteProxyOperationWithState(int code,
AttributionSourceState attributionSourceState, boolean shouldCollectAsyncNotedOp,
String message, boolean shouldCollectMessage, boolean skipProxyOperation) {
+ if (Flags.appopAccessTrackingLoggingEnabled()) {
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED,
+ attributionSourceState.uid, code,
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_PROXY_OPERATION,
+ attributionSourceState.attributionTag != null);
+ }
+
AttributionSource attributionSource = new AttributionSource(attributionSourceState);
return mCheckOpsDelegateDispatcher.noteProxyOperation(code, attributionSource,
shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation);
@@ -3096,6 +3122,14 @@
public SyncNotedAppOp noteOperation(int code, int uid, String packageName,
String attributionTag, boolean shouldCollectAsyncNotedOp, String message,
boolean shouldCollectMessage) {
+ if (Flags.appopAccessTrackingLoggingEnabled()) {
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED,
+ uid, code,
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION,
+ attributionTag != null);
+ }
+
return mCheckOpsDelegateDispatcher.noteOperation(code, uid, packageName,
attributionTag, Context.DEVICE_ID_DEFAULT, shouldCollectAsyncNotedOp, message,
shouldCollectMessage);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index c37d471..1563a62 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -286,6 +286,7 @@
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CancellationException;
+import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -4030,7 +4031,6 @@
&& isFullVolumeDevice(device);
boolean tvConditions = mHdmiTvClient != null
&& mHdmiSystemAudioSupported
- && isFullVolumeDevice(device)
&& !isAbsoluteVolumeDevice(device)
&& !isA2dpAbsoluteVolumeDevice(device);
diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcController.java b/services/core/java/com/android/server/hdmi/HdmiEarcController.java
index 46a8f03..1c947e9 100644
--- a/services/core/java/com/android/server/hdmi/HdmiEarcController.java
+++ b/services/core/java/com/android/server/hdmi/HdmiEarcController.java
@@ -87,8 +87,8 @@
} catch (ServiceSpecificException sse) {
HdmiLogger.error(
"Could not set eARC enabled to " + enabled + ". Error: ", sse.errorCode);
- } catch (RemoteException re) {
- HdmiLogger.error("Could not set eARC enabled to " + enabled + ":. Exception: ", re);
+ } catch (RemoteException | NullPointerException e) {
+ HdmiLogger.error("Could not set eARC enabled to " + enabled + ":. Exception: ", e);
}
}
@@ -96,8 +96,8 @@
public boolean nativeIsEarcEnabled() {
try {
return mEarc.isEArcEnabled();
- } catch (RemoteException re) {
- HdmiLogger.error("Could not read if eARC is enabled. Exception: ", re);
+ } catch (RemoteException | NullPointerException e) {
+ HdmiLogger.error("Could not read if eARC is enabled. Exception: ", e);
return false;
}
}
@@ -107,8 +107,8 @@
mEarcCallback = callback;
try {
mEarc.setCallback(callback);
- } catch (RemoteException re) {
- HdmiLogger.error("Could not set callback. Exception: ", re);
+ } catch (RemoteException | NullPointerException e) {
+ HdmiLogger.error("Could not set callback. Exception: ", e);
}
}
@@ -116,8 +116,8 @@
public byte nativeGetState(int portId) {
try {
return mEarc.getState(portId);
- } catch (RemoteException re) {
- HdmiLogger.error("Could not get eARC state. Exception: ", re);
+ } catch (RemoteException | NullPointerException e) {
+ HdmiLogger.error("Could not get eARC state. Exception: ", e);
return -1;
}
}
@@ -126,9 +126,9 @@
public byte[] nativeGetLastReportedAudioCapabilities(int portId) {
try {
return mEarc.getLastReportedAudioCapabilities(portId);
- } catch (RemoteException re) {
+ } catch (RemoteException | NullPointerException e) {
HdmiLogger.error(
- "Could not read last reported audio capabilities. Exception: ", re);
+ "Could not read last reported audio capabilities. Exception: ", e);
return null;
}
}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 3780fbd..bbdac56 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -99,6 +99,7 @@
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
+import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
@@ -126,6 +127,7 @@
import android.util.ArraySet;
import android.util.Log;
import android.util.LongSparseArray;
+import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
@@ -253,6 +255,8 @@
private static final String MIGRATED_KEYSTORE_NS = "migrated_keystore_namespace";
private static final String MIGRATED_SP_CE_ONLY = "migrated_all_users_to_sp_and_bound_ce";
private static final String MIGRATED_SP_FULL = "migrated_all_users_to_sp_and_bound_keys";
+ private static final String MIGRATED_WEAVER_DISABLED_ON_UNSECURED_USERS =
+ "migrated_weaver_disabled_on_unsecured_users";
// Duration that LockSettingsService will store the gatekeeper password for. This allows
// multiple biometric enrollments without prompting the user to enter their password via
@@ -309,6 +313,10 @@
@GuardedBy("mUserCreationAndRemovalLock")
private boolean mThirdPartyAppsStarted;
+ // This list contains the (protectorId, userId) of any protectors that were by replaced by a
+ // migration and should be destroyed once rollback to the old build is no longer possible.
+ private ArrayList<Pair<Long, Integer>> mProtectorsToDestroyOnBootCompleted = new ArrayList<>();
+
// Current password metrics for all secured users on the device. Updated when user unlocks the
// device or changes password. Removed if user is stopped with its CE key evicted.
@GuardedBy("this")
@@ -363,6 +371,10 @@
mLockSettingsService.migrateOldDataAfterSystemReady();
mLockSettingsService.deleteRepairModePersistentDataIfNeeded();
} else if (phase == PHASE_BOOT_COMPLETED) {
+ // In the case of an upgrade, PHASE_BOOT_COMPLETED means that a rollback to the old
+ // build can no longer occur. This is the time to destroy any migrated protectors.
+ mLockSettingsService.destroyMigratedProtectors();
+
mLockSettingsService.loadEscrowData();
}
}
@@ -1076,6 +1088,11 @@
mStorage.deleteRepairModePersistentData();
}
+ private boolean isWeaverDisabledOnUnsecuredUsers() {
+ return mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_disableWeaverOnUnsecuredUsers);
+ }
+
// This is called when Weaver is guaranteed to be available (if the device supports Weaver).
// It does any synthetic password related work that was delayed from earlier in the boot.
private void onThirdPartyAppsStarted() {
@@ -1114,13 +1131,20 @@
//
// - Upgrading from Android 14, where unsecured users didn't have Keystore super keys.
//
+ // - Upgrading from a build with config_disableWeaverOnUnsecuredUsers=false to one with
+ // config_disableWeaverOnUnsecuredUsers=true. (We don't bother to proactively add
+ // Weaver for the reverse update to false, as it's too late to help in that case.)
+ //
// The end result is that all users, regardless of whether they are secured or not, have
- // a synthetic password with all keys initialized and protected by it.
+ // a synthetic password with all keys initialized and protected by it, and honoring
+ // config_disableWeaverOnUnsecuredUsers=true when applicable.
//
// Note: if this migration gets interrupted (e.g. by the device powering off), there
// shouldn't be a problem since this will run again on the next boot, and
// setCeStorageProtection() and initKeystoreSuperKeys(..., true) are idempotent.
- if (!getBoolean(MIGRATED_SP_FULL, false, 0)) {
+ if (!getBoolean(MIGRATED_SP_FULL, false, 0)
+ || (isWeaverDisabledOnUnsecuredUsers()
+ && !getBoolean(MIGRATED_WEAVER_DISABLED_ON_UNSECURED_USERS, false, 0))) {
for (UserInfo user : mUserManager.getAliveUsers()) {
removeStateForReusedUserIdIfNecessary(user.id, user.serialNumber);
synchronized (mSpManager) {
@@ -1128,6 +1152,9 @@
}
}
setBoolean(MIGRATED_SP_FULL, true, 0);
+ if (isWeaverDisabledOnUnsecuredUsers()) {
+ setBoolean(MIGRATED_WEAVER_DISABLED_ON_UNSECURED_USERS, true, 0);
+ }
}
mThirdPartyAppsStarted = true;
@@ -1151,13 +1178,61 @@
getGateKeeperService(), protectorId, LockscreenCredential.createNone(), userId,
null);
SyntheticPassword sp = result.syntheticPassword;
- if (sp == null) {
+ if (isWeaverDisabledOnUnsecuredUsers()) {
+ Slog.i(TAG, "config_disableWeaverOnUnsecuredUsers=true");
+
+ // If config_disableWeaverOnUnsecuredUsers=true, then the Weaver HAL may be buggy and
+ // need multiple retries before it works here to unwrap the SP, if the SP was already
+ // protected by Weaver. Note that the problematic HAL can also deadlock if called with
+ // the ActivityManagerService lock held, but that should not be a problem here since
+ // that lock isn't held here, unlike unlockUserKeyIfUnsecured() where it is.
+ for (int i = 0; i < 12 && sp == null; i++) {
+ Slog.e(TAG, "Failed to unwrap synthetic password. Waiting 5 seconds to retry.");
+ SystemClock.sleep(5000);
+ result = mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId,
+ LockscreenCredential.createNone(), userId, null);
+ sp = result.syntheticPassword;
+ }
+ if (sp == null) {
+ throw new IllegalStateException(
+ "Failed to unwrap synthetic password for unsecured user");
+ }
+ // If the SP is protected by Weaver, then remove the Weaver protection in order to make
+ // config_disableWeaverOnUnsecuredUsers=true take effect.
+ if (result.usedWeaver) {
+ Slog.i(TAG, "Removing Weaver protection from the synthetic password");
+ // Create a new protector, which will not use Weaver.
+ long newProtectorId = mSpManager.createLskfBasedProtector(
+ getGateKeeperService(), LockscreenCredential.createNone(), sp, userId);
+
+ // Out of paranoia, make sure the new protector really works.
+ result = mSpManager.unlockLskfBasedProtector(getGateKeeperService(),
+ newProtectorId, LockscreenCredential.createNone(), userId, null);
+ sp = result.syntheticPassword;
+ if (sp == null) {
+ throw new IllegalStateException("New SP protector does not work");
+ }
+
+ // Replace the protector. Wait until PHASE_BOOT_COMPLETED to destroy the old
+ // protector, since the Weaver slot erasure and freeing cannot be rolled back.
+ setCurrentLskfBasedProtectorId(newProtectorId, userId);
+ mProtectorsToDestroyOnBootCompleted.add(new Pair(protectorId, userId));
+ } else {
+ Slog.i(TAG, "Synthetic password is already not protected by Weaver");
+ }
+ } else if (sp == null) {
Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId);
return;
}
- // While setCeStorageProtection() is idempotent, it does log some error messages when called
- // again. Skip it if we know it was already handled by an earlier upgrade to Android 14.
- if (getString(MIGRATED_SP_CE_ONLY, null, 0) == null) {
+
+ // Call setCeStorageProtection(), to re-encrypt the CE key with the SP if it's currently
+ // encrypted by an empty secret. Skip this if it was definitely already done as part of the
+ // upgrade to Android 14, since while setCeStorageProtection() is idempotent it does log
+ // some error messages when called again. Do not skip this if
+ // config_disableWeaverOnUnsecuredUsers=true, since in that case we'd like to recover from
+ // the case where an earlier upgrade to Android 14 incorrectly skipped this step.
+ if (getString(MIGRATED_SP_CE_ONLY, null, 0) == null
+ || isWeaverDisabledOnUnsecuredUsers()) {
Slogf.i(TAG, "Encrypting CE key of user %d with synthetic password", userId);
setCeStorageProtection(userId, sp);
}
@@ -1165,6 +1240,17 @@
initKeystoreSuperKeys(userId, sp, /* allowExisting= */ true);
}
+ private void destroyMigratedProtectors() {
+ if (!mProtectorsToDestroyOnBootCompleted.isEmpty()) {
+ synchronized (mSpManager) {
+ for (Pair<Long, Integer> pair : mProtectorsToDestroyOnBootCompleted) {
+ mSpManager.destroyLskfBasedProtector(pair.first, pair.second);
+ }
+ }
+ }
+ mProtectorsToDestroyOnBootCompleted = null; // The list is no longer needed.
+ }
+
/**
* Returns the lowest password quality that still presents the same UI for entering it.
*
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 3a429b0..47788f2 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -195,6 +195,8 @@
// ERROR: password / token fails verification
// RETRY: password / token verification is throttled at the moment.
@Nullable public VerifyCredentialResponse gkResponse;
+ // For unlockLskfBasedProtector() this is set to true if the protector uses Weaver.
+ public boolean usedWeaver;
}
/**
@@ -532,6 +534,11 @@
Settings.Global.DEVICE_PROVISIONED, 0) != 0;
}
+ private boolean isWeaverDisabledOnUnsecuredUsers() {
+ return mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_disableWeaverOnUnsecuredUsers);
+ }
+
@VisibleForTesting
protected android.hardware.weaver.V1_0.IWeaver getWeaverHidlService() throws RemoteException {
try {
@@ -1011,7 +1018,13 @@
Slogf.i(TAG, "Creating LSKF-based protector %016x for user %d", protectorId, userId);
- final IWeaver weaver = getWeaverService();
+ final IWeaver weaver;
+ if (credential.isNone() && isWeaverDisabledOnUnsecuredUsers()) {
+ weaver = null;
+ Slog.w(TAG, "Not using Weaver for unsecured user (disabled by config)");
+ } else {
+ weaver = getWeaverService();
+ }
if (weaver != null) {
// Weaver is available, so make the protector use it to verify the LSKF. Do this even
// if the LSKF is empty, as that gives us support for securely deleting the protector.
@@ -1404,6 +1417,7 @@
int weaverSlot = loadWeaverSlot(protectorId, userId);
if (weaverSlot != INVALID_WEAVER_SLOT) {
// Protector uses Weaver to verify the LSKF
+ result.usedWeaver = true;
final IWeaver weaver = getWeaverService();
if (weaver == null) {
Slog.e(TAG, "Protector uses Weaver, but Weaver is unavailable");
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index e8d14cb..9b9be4c 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -832,8 +832,9 @@
FullyQualifiedGroupKey newGroup) { }
/**
- * Called when a notification channel is updated, so that this helper can adjust
- * the aggregate groups by moving children if their section has changed.
+ * Called when a notification channel is updated (channel attributes have changed),
+ * so that this helper can adjust the aggregate groups by moving children
+ * if their section has changed.
* see {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)}
* @param userId the userId of the channel
* @param pkgName the channel's package
@@ -853,24 +854,48 @@
}
}
- // The list of notification operations required after the channel update
- final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>();
+ regroupNotifications(userId, pkgName, notificationsToCheck);
+ }
+ }
- // Check any already auto-grouped notifications that may need to be re-grouped
- // after the channel update
- notificationsToMove.addAll(
- getAutogroupedNotificationsMoveOps(userId, pkgName,
- notificationsToCheck));
+ /**
+ * Called when an individuial notification's channel is updated (moved to a new channel),
+ * so that this helper can adjust the aggregate groups by moving children
+ * if their section has changed.
+ * see {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)}
+ *
+ * @param record the notification which had its channel updated
+ */
+ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void onChannelUpdated(final NotificationRecord record) {
+ synchronized (mAggregatedNotifications) {
+ ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>();
+ notificationsToCheck.put(record.getKey(), record);
+ regroupNotifications(record.getUserId(), record.getSbn().getPackageName(),
+ notificationsToCheck);
+ }
+ }
- // Check any ungrouped notifications that may need to be auto-grouped
- // after the channel update
- notificationsToMove.addAll(
- getUngroupedNotificationsMoveOps(userId, pkgName, notificationsToCheck));
+ @GuardedBy("mAggregatedNotifications")
+ private void regroupNotifications(int userId, String pkgName,
+ ArrayMap<String, NotificationRecord> notificationsToCheck) {
+ // The list of notification operations required after the channel update
+ final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>();
- // Batch move to new section
- if (!notificationsToMove.isEmpty()) {
- moveNotificationsToNewSection(userId, pkgName, notificationsToMove);
- }
+ // Check any already auto-grouped notifications that may need to be re-grouped
+ // after the channel update
+ notificationsToMove.addAll(
+ getAutogroupedNotificationsMoveOps(userId, pkgName,
+ notificationsToCheck));
+
+ // Check any ungrouped notifications that may need to be auto-grouped
+ // after the channel update
+ notificationsToMove.addAll(
+ getUngroupedNotificationsMoveOps(userId, pkgName, notificationsToCheck));
+
+ // Batch move to new section
+ if (!notificationsToMove.isEmpty()) {
+ moveNotificationsToNewSection(userId, pkgName, notificationsToMove);
}
}
diff --git a/services/core/java/com/android/server/notification/NotificationAdjustmentExtractor.java b/services/core/java/com/android/server/notification/NotificationAdjustmentExtractor.java
index 97bbc23..2dd4f83 100644
--- a/services/core/java/com/android/server/notification/NotificationAdjustmentExtractor.java
+++ b/services/core/java/com/android/server/notification/NotificationAdjustmentExtractor.java
@@ -15,6 +15,9 @@
*/
package com.android.server.notification;
+import static android.service.notification.Adjustment.KEY_TYPE;
+import static android.service.notification.Flags.notificationForceGrouping;
+
import android.content.Context;
import android.util.Slog;
@@ -24,6 +27,7 @@
public class NotificationAdjustmentExtractor implements NotificationSignalExtractor {
private static final String TAG = "AdjustmentExtractor";
private static final boolean DBG = false;
+ private GroupHelper mGroupHelper;
public void initialize(Context ctx, NotificationUsageStats usageStats) {
@@ -35,8 +39,27 @@
if (DBG) Slog.d(TAG, "skipping empty notification");
return null;
}
+
+ final boolean hasAdjustedClassification = record.hasAdjustment(KEY_TYPE);
record.applyAdjustments();
+ if (notificationForceGrouping()
+ && android.service.notification.Flags.notificationClassification()) {
+ // Classification adjustments trigger regrouping
+ if (mGroupHelper != null && hasAdjustedClassification) {
+ return new RankingReconsideration(record.getKey(), 0) {
+ @Override
+ public void work() {
+ }
+
+ @Override
+ public void applyChangesLocked(NotificationRecord record) {
+ mGroupHelper.onChannelUpdated(record);
+ }
+ };
+ }
+ }
+
return null;
}
@@ -49,4 +72,9 @@
public void setZenHelper(ZenModeHelper helper) {
}
+
+ @Override
+ public void setGroupHelper(GroupHelper groupHelper) {
+ mGroupHelper = groupHelper;
+ }
}
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index 06f419a..ea4a6db 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -17,6 +17,7 @@
package com.android.server.notification;
import static android.app.Flags.sortSectionByTime;
+import static android.app.Notification.CATEGORY_MESSAGE;
import static android.app.Notification.FLAG_INSISTENT;
import static android.app.Notification.FLAG_ONLY_ALERT_ONCE;
import static android.app.NotificationManager.IMPORTANCE_MIN;
@@ -42,6 +43,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.database.ContentObserver;
@@ -1641,7 +1643,7 @@
}
// recent conversation
- if (record.isConversation()
+ if ((record.isConversation() || isConversationMessage(record))
&& record.getNotification().getWhen() > mLastAvalancheTriggerTimestamp) {
return true;
}
@@ -1656,6 +1658,21 @@
return false;
}
+
+ // Relaxed signals for conversations messages
+ private boolean isConversationMessage(final NotificationRecord record) {
+ if (!CATEGORY_MESSAGE.equals(record.getSbn().getNotification().category)) {
+ return false;
+ }
+ if (record.getChannel().isDemoted()) {
+ return false;
+ }
+ final ShortcutInfo shortcut = record.getShortcutInfo();
+ if (shortcut == null) {
+ return false;
+ }
+ return true;
+ }
}
//====================== Observers =============================
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 56e0a89..6c2d4f7 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -519,6 +519,7 @@
private static final long DELAY_FORCE_REGROUP_TIME = 3000;
+
private static final String ACTION_NOTIFICATION_TIMEOUT =
NotificationManagerService.class.getSimpleName() + ".TIMEOUT";
private static final int REQUEST_CODE_TIMEOUT = 1;
@@ -2583,7 +2584,7 @@
mShowReviewPermissionsNotification,
Clock.systemUTC());
mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper,
- mZenModeHelper, mUsageStats, extractorNames, mPlatformCompat);
+ mZenModeHelper, mUsageStats, extractorNames, mPlatformCompat, groupHelper);
mSnoozeHelper = snoozeHelper;
mGroupHelper = groupHelper;
mHistoryManager = historyManager;
@@ -6871,22 +6872,9 @@
}
if (android.service.notification.Flags.notificationClassification()
&& adjustments.containsKey(KEY_TYPE)) {
- NotificationChannel newChannel = null;
- int type = adjustments.getInt(KEY_TYPE);
- if (TYPE_NEWS == type) {
- newChannel = mPreferencesHelper.getNotificationChannel(
- r.getSbn().getPackageName(), r.getUid(), NEWS_ID, false);
- } else if (TYPE_PROMOTION == type) {
- newChannel = mPreferencesHelper.getNotificationChannel(
- r.getSbn().getPackageName(), r.getUid(), PROMOTIONS_ID, false);
- } else if (TYPE_SOCIAL_MEDIA == type) {
- newChannel = mPreferencesHelper.getNotificationChannel(
- r.getSbn().getPackageName(), r.getUid(), SOCIAL_MEDIA_ID, false);
- } else if (TYPE_CONTENT_RECOMMENDATION == type) {
- newChannel = mPreferencesHelper.getNotificationChannel(
- r.getSbn().getPackageName(), r.getUid(), RECS_ID, false);
- }
- if (newChannel == null) {
+ final NotificationChannel newChannel = getClassificationChannelLocked(r,
+ adjustments);
+ if (newChannel == null || newChannel.getId().equals(r.getChannel().getId())) {
adjustments.remove(KEY_TYPE);
} else {
// swap app provided type with the real thing
@@ -6902,6 +6890,27 @@
}
}
+ @GuardedBy("mNotificationLock")
+ @Nullable
+ private NotificationChannel getClassificationChannelLocked(NotificationRecord r,
+ Bundle adjustments) {
+ int type = adjustments.getInt(KEY_TYPE);
+ if (TYPE_NEWS == type) {
+ return mPreferencesHelper.getNotificationChannel(
+ r.getSbn().getPackageName(), r.getUid(), NEWS_ID, false);
+ } else if (TYPE_PROMOTION == type) {
+ return mPreferencesHelper.getNotificationChannel(
+ r.getSbn().getPackageName(), r.getUid(), PROMOTIONS_ID, false);
+ } else if (TYPE_SOCIAL_MEDIA == type) {
+ return mPreferencesHelper.getNotificationChannel(
+ r.getSbn().getPackageName(), r.getUid(), SOCIAL_MEDIA_ID, false);
+ } else if (TYPE_CONTENT_RECOMMENDATION == type) {
+ return mPreferencesHelper.getNotificationChannel(
+ r.getSbn().getPackageName(), r.getUid(), RECS_ID, false);
+ }
+ return null;
+ }
+
@SuppressWarnings("GuardedBy")
@GuardedBy("mNotificationLock")
void addAutogroupKeyLocked(String key, String groupName, boolean requestSort) {
@@ -12009,6 +12018,10 @@
if (record != null && (record.getSbn().getNotification().flags
& FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0
&& !record.isCanceledAfterLifetimeExtension()) {
+ // Mark that the notification is being updated due to cancelation, so it won't
+ // be updated again if the app cancels multiple times.
+ record.setCanceledAfterLifetimeExtension(true);
+
boolean isAppForeground = pkg != null && packageImportance == IMPORTANCE_FOREGROUND;
// Save the original Record's post silently value, so we can restore it after we send
@@ -12024,9 +12037,6 @@
PostNotificationTracker tracker = mPostNotificationTrackerFactory.newTracker(null);
tracker.addCleanupRunnable(() -> {
synchronized (mNotificationLock) {
- // Mark that the notification has been updated due to cancelation, so it won't
- // be updated again if the app cancels multiple times.
- record.setCanceledAfterLifetimeExtension(true);
// Set the post silently status to the record's previous value.
record.setPostSilently(savedPostSilentlyState);
// Remove FLAG_ONLY_ALERT_ONCE if the notification did not previously have it.
diff --git a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
index f0358d1..be34bee 100644
--- a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
+++ b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
@@ -55,4 +55,9 @@
void setZenHelper(ZenModeHelper helper);
default void setCompatChangeLogger(IPlatformCompat platformCompat){};
+
+ /**
+ * @param groupHelper Helper for auto-grouping notifications
+ */
+ default void setGroupHelper(GroupHelper groupHelper){};
}
diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java
index 03dd935..f06d6405 100644
--- a/services/core/java/com/android/server/notification/RankingHelper.java
+++ b/services/core/java/com/android/server/notification/RankingHelper.java
@@ -23,7 +23,6 @@
import static android.text.TextUtils.formatSimple;
import android.annotation.NonNull;
-import android.app.NotificationManager;
import android.content.Context;
import android.service.notification.RankingHelperProto;
import android.util.ArrayMap;
@@ -61,7 +60,7 @@
})
public RankingHelper(Context context, RankingHandler rankingHandler, RankingConfig config,
ZenModeHelper zenHelper, NotificationUsageStats usageStats, String[] extractorNames,
- IPlatformCompat platformCompat) {
+ IPlatformCompat platformCompat, GroupHelper groupHelper) {
mContext = context;
mRankingHandler = rankingHandler;
if (sortSectionByTime()) {
@@ -80,6 +79,7 @@
extractor.initialize(mContext, usageStats);
extractor.setConfig(config);
extractor.setZenHelper(zenHelper);
+ extractor.setGroupHelper(groupHelper);
if (restrictAudioAttributesAlarm() || restrictAudioAttributesMedia()
|| restrictAudioAttributesCall()) {
extractor.setCompatChangeLogger(platformCompat);
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 68e781f..e47b4c2 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1141,9 +1141,9 @@
+ mShortPressOnPowerBehavior);
if (count == 2) {
- powerMultiPressAction(displayId, eventTime, interactive, mDoublePressOnPowerBehavior);
+ powerMultiPressAction(eventTime, interactive, mDoublePressOnPowerBehavior);
} else if (count == 3) {
- powerMultiPressAction(displayId, eventTime, interactive, mTriplePressOnPowerBehavior);
+ powerMultiPressAction(eventTime, interactive, mTriplePressOnPowerBehavior);
} else if (count > 3 && count <= getMaxMultiPressPowerCount()) {
Slog.d(TAG, "No behavior defined for power press count " + count);
} else if (count == 1 && shouldHandleShortPressPowerAction(interactive, eventTime)) {
@@ -1307,8 +1307,7 @@
}
}
- private void powerMultiPressAction(int displayId, long eventTime, boolean interactive,
- int behavior) {
+ private void powerMultiPressAction(long eventTime, boolean interactive, int behavior) {
switch (behavior) {
case MULTI_PRESS_POWER_NOTHING:
break;
@@ -1323,7 +1322,7 @@
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.THEATER_MODE_ON, 0);
if (!interactive) {
- wakeUpFromWakeKey(displayId, eventTime, KEYCODE_POWER, /* isDown= */ false);
+ wakeUpFromWakeKey(eventTime, KEYCODE_POWER, /* isDown= */ false);
}
} else {
Slog.i(TAG, "Toggling theater mode on.");
@@ -1339,7 +1338,7 @@
case MULTI_PRESS_POWER_BRIGHTNESS_BOOST:
Slog.i(TAG, "Starting brightness boost.");
if (!interactive) {
- wakeUpFromWakeKey(displayId, eventTime, KEYCODE_POWER, /* isDown= */ false);
+ wakeUpFromWakeKey(eventTime, KEYCODE_POWER, /* isDown= */ false);
}
mPowerManager.boostScreenBrightness(eventTime);
break;
@@ -5547,7 +5546,7 @@
if (mRequestedOrSleepingDefaultDisplay) {
mCameraGestureTriggeredDuringGoingToSleep = true;
// Wake device up early to prevent display doing redundant turning off/on stuff.
- mWindowWakeUpPolicy.wakeUpFromPowerKeyCameraGesture(event.getDisplayId());
+ mWindowWakeUpPolicy.wakeUpFromPowerKeyCameraGesture();
}
return true;
}
@@ -5645,8 +5644,8 @@
public int interceptMotionBeforeQueueingNonInteractive(int displayId, int source, int action,
long whenNanos, int policyFlags) {
if ((policyFlags & FLAG_WAKE) != 0) {
- if (mWindowWakeUpPolicy.wakeUpFromMotion(displayId, whenNanos / 1000000, source,
- action == MotionEvent.ACTION_DOWN)) {
+ if (mWindowWakeUpPolicy.wakeUpFromMotion(
+ whenNanos / 1000000, source, action == MotionEvent.ACTION_DOWN)) {
// Woke up. Pass motion events to user.
return ACTION_PASS_TO_USER;
}
@@ -5660,8 +5659,8 @@
// there will be no dream to intercept the touch and wake into ambient. The device should
// wake up in this case.
if (isTheaterModeEnabled() && (policyFlags & FLAG_WAKE) != 0) {
- if (mWindowWakeUpPolicy.wakeUpFromMotion(displayId, whenNanos / 1000000, source,
- action == MotionEvent.ACTION_DOWN)) {
+ if (mWindowWakeUpPolicy.wakeUpFromMotion(
+ whenNanos / 1000000, source, action == MotionEvent.ACTION_DOWN)) {
// Woke up. Pass motion events to user.
return ACTION_PASS_TO_USER;
}
@@ -6003,14 +6002,13 @@
return;
}
wakeUpFromWakeKey(
- event.getDisplayId(),
event.getEventTime(),
event.getKeyCode(),
event.getAction() == KeyEvent.ACTION_DOWN);
}
- private void wakeUpFromWakeKey(int displayId, long eventTime, int keyCode, boolean isDown) {
- if (mWindowWakeUpPolicy.wakeUpFromKey(displayId, eventTime, keyCode, isDown)) {
+ private void wakeUpFromWakeKey(long eventTime, int keyCode, boolean isDown) {
+ if (mWindowWakeUpPolicy.wakeUpFromKey(eventTime, keyCode, isDown)) {
final boolean keyCanLaunchHome = keyCode == KEYCODE_HOME || keyCode == KEYCODE_POWER;
// Start HOME with "reason" extra if sleeping for more than mWakeUpToLastStateTimeout
if (shouldWakeUpWithHomeIntent() && keyCanLaunchHome) {
diff --git a/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java b/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java
index 966d84f4..af1ad13 100644
--- a/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java
@@ -25,7 +25,6 @@
import static android.view.KeyEvent.KEYCODE_POWER;
import static com.android.server.policy.Flags.supportInputWakeupDelegate;
-import static com.android.server.power.feature.flags.Flags.perDisplayWakeByTouch;
import android.annotation.Nullable;
import android.content.Context;
@@ -108,14 +107,13 @@
/**
* Wakes up from a key event.
*
- * @param displayId the id of the display to wake.
* @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}.
* @param keyCode the {@link android.view.KeyEvent} key code of the key event.
* @param isDown {@code true} if the event's action is {@link KeyEvent#ACTION_DOWN}.
* @return {@code true} if the policy allows the requested wake up and the request has been
* executed; {@code false} otherwise.
*/
- boolean wakeUpFromKey(int displayId, long eventTime, int keyCode, boolean isDown) {
+ boolean wakeUpFromKey(long eventTime, int keyCode, boolean isDown) {
final boolean wakeAllowedDuringTheaterMode =
keyCode == KEYCODE_POWER
? mAllowTheaterModeWakeFromPowerKey
@@ -129,7 +127,6 @@
return true;
}
wakeUp(
- displayId,
eventTime,
keyCode == KEYCODE_POWER ? WAKE_REASON_POWER_BUTTON : WAKE_REASON_WAKE_KEY,
keyCode == KEYCODE_POWER ? "POWER" : "KEY");
@@ -139,13 +136,12 @@
/**
* Wakes up from a motion event.
*
- * @param displayId the id of the display to wake.
* @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}.
* @param isDown {@code true} if the event's action is {@link MotionEvent#ACTION_DOWN}.
* @return {@code true} if the policy allows the requested wake up and the request has been
* executed; {@code false} otherwise.
*/
- boolean wakeUpFromMotion(int displayId, long eventTime, int source, boolean isDown) {
+ boolean wakeUpFromMotion(long eventTime, int source, boolean isDown) {
if (!canWakeUp(mAllowTheaterModeWakeFromMotion)) {
if (DEBUG) Slog.d(TAG, "Unable to wake up from motion.");
return false;
@@ -154,7 +150,7 @@
&& mInputWakeUpDelegate.wakeUpFromMotion(eventTime, source, isDown)) {
return true;
}
- wakeUp(displayId, eventTime, WAKE_REASON_WAKE_MOTION, "MOTION");
+ wakeUp(eventTime, WAKE_REASON_WAKE_MOTION, "MOTION");
return true;
}
@@ -170,7 +166,7 @@
if (DEBUG) Slog.d(TAG, "Unable to wake up from camera cover.");
return false;
}
- wakeUp(Display.DEFAULT_DISPLAY, eventTime, WAKE_REASON_CAMERA_LAUNCH, "CAMERA_COVER");
+ wakeUp(eventTime, WAKE_REASON_CAMERA_LAUNCH, "CAMERA_COVER");
return true;
}
@@ -185,24 +181,22 @@
if (DEBUG) Slog.d(TAG, "Unable to wake up from lid.");
return false;
}
- wakeUp(Display.DEFAULT_DISPLAY, mClock.uptimeMillis(), WAKE_REASON_LID, "LID");
+ wakeUp(mClock.uptimeMillis(), WAKE_REASON_LID, "LID");
return true;
}
/**
* Wakes up to prevent sleeping when opening camera through power button.
*
- * @param displayId the id of the display to wake.
* @return {@code true} if the policy allows the requested wake up and the request has been
* executed; {@code false} otherwise.
*/
- boolean wakeUpFromPowerKeyCameraGesture(int displayId) {
+ boolean wakeUpFromPowerKeyCameraGesture() {
if (!canWakeUp(mAllowTheaterModeWakeFromPowerKey)) {
if (DEBUG) Slog.d(TAG, "Unable to wake up from power key camera gesture.");
return false;
}
- wakeUp(displayId, mClock.uptimeMillis(), WAKE_REASON_CAMERA_LAUNCH,
- "CAMERA_GESTURE_PREVENT_LOCK");
+ wakeUp(mClock.uptimeMillis(), WAKE_REASON_CAMERA_LAUNCH, "CAMERA_GESTURE_PREVENT_LOCK");
return true;
}
@@ -217,7 +211,7 @@
if (DEBUG) Slog.d(TAG, "Unable to wake up from gesture.");
return false;
}
- wakeUp(Display.DEFAULT_DISPLAY, mClock.uptimeMillis(), WAKE_REASON_GESTURE, "GESTURE");
+ wakeUp(mClock.uptimeMillis(), WAKE_REASON_GESTURE, "GESTURE");
return true;
}
@@ -240,11 +234,7 @@
}
/** Wakes up {@link PowerManager}. */
- private void wakeUp(int displayId, long wakeTime, @WakeReason int reason, String details) {
- if (perDisplayWakeByTouch()) {
- mPowerManager.wakeUp(wakeTime, reason, "android.policy:" + details, displayId);
- } else {
- mPowerManager.wakeUp(wakeTime, reason, "android.policy:" + details);
- }
+ private void wakeUp(long wakeTime, @WakeReason int reason, String details) {
+ mPowerManager.wakeUp(wakeTime, reason, "android.policy:" + details);
}
}
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 4fae798..eb62b56 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -709,7 +709,7 @@
SparseBooleanArray newDisplayInteractivities = new SparseBooleanArray();
for (int i = 0; i < displaysByGroupId.size(); i++) {
final int groupId = displaysByGroupId.keyAt(i);
- for (int displayId : displaysByGroupId.get(i)) {
+ for (int displayId : displaysByGroupId.get(groupId)) {
// If we already know display interactivity, use that
if (mDisplayInteractivities.indexOfKey(displayId) > 0) {
newDisplayInteractivities.put(
diff --git a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
index b5a7fcb..e650c52 100644
--- a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
@@ -56,24 +56,26 @@
}
@Override
+ public long getCreateUptimeMillis() {
+ return stats.getCreateUptimeMillis();
+ }
+
+ @Override
public CallerInfo getCallerInfo() {
return callerInfo;
}
@Override
- public VibrationSession.DebugInfo getDebugInfo() {
- return new Vibration.DebugInfoImpl(getStatus(), stats, /* playedEffect= */ null,
- /* originalEffect= */ null, mScale.scaleLevel, mScale.adaptiveHapticsScale,
- callerInfo);
+ public IBinder getCallerToken() {
+ return mExternalVibration.getToken();
}
@Override
- public VibrationStats.StatsInfo getStatsInfo(long completionUptimeMillis) {
- return new VibrationStats.StatsInfo(
- mExternalVibration.getUid(),
- FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__EXTERNAL,
- mExternalVibration.getVibrationAttributes().getUsage(), getStatus(), stats,
- completionUptimeMillis);
+ public VibrationSession.DebugInfo getDebugInfo() {
+ return new Vibration.DebugInfoImpl(getStatus(), callerInfo,
+ FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__EXTERNAL, stats,
+ /* playedEffect= */ null, /* originalEffect= */ null, mScale.scaleLevel,
+ mScale.adaptiveHapticsScale);
}
@Override
@@ -86,6 +88,12 @@
}
@Override
+ public boolean wasEndRequested() {
+ // End request is immediate, so just check if vibration has already ended.
+ return hasEnded();
+ }
+
+ @Override
public boolean linkToDeath(Runnable callback) {
synchronized (mLock) {
mBinderDeathCallback = callback;
@@ -104,10 +112,12 @@
@Override
public void binderDied() {
+ Runnable callback;
synchronized (mLock) {
- if (mBinderDeathCallback != null) {
- mBinderDeathCallback.run();
- }
+ callback = mBinderDeathCallback;
+ }
+ if (callback != null) {
+ callback.run();
}
}
@@ -131,6 +141,16 @@
end(new EndInfo(status, endedBy));
}
+ @Override
+ public void notifyVibratorCallback(int vibratorId, long vibrationId) {
+ // ignored, external control does not expect callbacks from the vibrator
+ }
+
+ @Override
+ public void notifySyncedVibratorsCallback(long vibrationId) {
+ // ignored, external control does not expect callbacks from the vibrator manager
+ }
+
boolean isHoldingSameVibration(ExternalVibration vib) {
return mExternalVibration.equals(vib);
}
diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java
index ce9c47b..fbcc856 100644
--- a/services/core/java/com/android/server/vibrator/HalVibration.java
+++ b/services/core/java/com/android/server/vibrator/HalVibration.java
@@ -19,15 +19,16 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.CombinedVibration;
-import android.os.IBinder;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
+import android.os.vibrator.PrebakedSegment;
+import android.os.vibrator.VibrationEffectSegment;
import android.util.SparseArray;
-import com.android.internal.util.FrameworkStatsLog;
-
+import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
+import java.util.function.IntFunction;
/**
* Represents a vibration defined by a {@link CombinedVibration} that will be performed by
@@ -36,7 +37,6 @@
final class HalVibration extends Vibration {
public final SparseArray<VibrationEffect> mFallbacks = new SparseArray<>();
- public final IBinder callerToken;
/** A {@link CountDownLatch} to enable waiting for completion. */
private final CountDownLatch mCompletionLatch = new CountDownLatch(1);
@@ -56,10 +56,9 @@
private int mScaleLevel;
private float mAdaptiveScale;
- HalVibration(@NonNull IBinder callerToken, @NonNull CombinedVibration effect,
- @NonNull VibrationSession.CallerInfo callerInfo) {
+ HalVibration(@NonNull VibrationSession.CallerInfo callerInfo,
+ @NonNull CombinedVibration effect) {
super(callerInfo);
- this.callerToken = callerToken;
mOriginalEffect = effect;
mEffectToPlay = effect;
mScaleLevel = VibrationScaler.SCALE_NONE;
@@ -87,11 +86,11 @@
}
/**
- * Add a fallback {@link VibrationEffect} to be played when given effect id is not supported,
- * which might be necessary for replacement in realtime.
+ * Add a fallback {@link VibrationEffect} to be played for each predefined effect id, which
+ * might be necessary for replacement in realtime.
*/
- public void addFallback(int effectId, VibrationEffect effect) {
- mFallbacks.put(effectId, effect);
+ public void fillFallbacks(IntFunction<VibrationEffect> fallbackProvider) {
+ fillFallbacksForEffect(mEffectToPlay, fallbackProvider);
}
/**
@@ -131,11 +130,6 @@
// No need to update fallback effects, they are already configured per device.
}
- @Override
- public boolean isRepeating() {
- return mOriginalEffect.getDuration() == Long.MAX_VALUE;
- }
-
/** Return the effect that should be played by this vibration. */
public CombinedVibration getEffectToPlay() {
return mEffectToPlay;
@@ -146,20 +140,9 @@
// Clear the original effect if it's the same as the effect that was played, for simplicity
CombinedVibration originalEffect =
Objects.equals(mOriginalEffect, mEffectToPlay) ? null : mOriginalEffect;
- return new Vibration.DebugInfoImpl(getStatus(), stats, mEffectToPlay, originalEffect,
- mScaleLevel, mAdaptiveScale, callerInfo);
- }
-
- @Override
- public VibrationStats.StatsInfo getStatsInfo(long completionUptimeMillis) {
- int vibrationType = mEffectToPlay.hasVendorEffects()
- ? FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__VENDOR
- : isRepeating()
- ? FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__REPEATED
- : FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE;
- return new VibrationStats.StatsInfo(
- callerInfo.uid, vibrationType, callerInfo.attrs.getUsage(), getStatus(),
- stats, completionUptimeMillis);
+ return new Vibration.DebugInfoImpl(getStatus(), callerInfo,
+ VibrationStats.StatsInfo.findVibrationType(mEffectToPlay), stats, mEffectToPlay,
+ originalEffect, mScaleLevel, mAdaptiveScale);
}
/**
@@ -174,6 +157,42 @@
return callerInfo.uid == vib.callerInfo.uid && callerInfo.attrs.isFlagSet(
VibrationAttributes.FLAG_PIPELINED_EFFECT)
&& vib.callerInfo.attrs.isFlagSet(VibrationAttributes.FLAG_PIPELINED_EFFECT)
- && !isRepeating();
+ && (mOriginalEffect.getDuration() != Long.MAX_VALUE);
+ }
+
+ private void fillFallbacksForEffect(CombinedVibration effect,
+ IntFunction<VibrationEffect> fallbackProvider) {
+ if (effect instanceof CombinedVibration.Mono) {
+ fillFallbacksForEffect(((CombinedVibration.Mono) effect).getEffect(), fallbackProvider);
+ } else if (effect instanceof CombinedVibration.Stereo) {
+ SparseArray<VibrationEffect> effects =
+ ((CombinedVibration.Stereo) effect).getEffects();
+ for (int i = 0; i < effects.size(); i++) {
+ fillFallbacksForEffect(effects.valueAt(i), fallbackProvider);
+ }
+ } else if (effect instanceof CombinedVibration.Sequential) {
+ List<CombinedVibration> effects =
+ ((CombinedVibration.Sequential) effect).getEffects();
+ for (int i = 0; i < effects.size(); i++) {
+ fillFallbacksForEffect(effects.get(i), fallbackProvider);
+ }
+ }
+ }
+
+ private void fillFallbacksForEffect(VibrationEffect effect,
+ IntFunction<VibrationEffect> fallbackProvider) {
+ if (!(effect instanceof VibrationEffect.Composed composed)) {
+ return;
+ }
+ int segmentCount = composed.getSegments().size();
+ for (int i = 0; i < segmentCount; i++) {
+ VibrationEffectSegment segment = composed.getSegments().get(i);
+ if ((segment instanceof PrebakedSegment prebaked) && prebaked.shouldFallback()) {
+ VibrationEffect fallback = fallbackProvider.apply(prebaked.getEffectId());
+ if (fallback != null) {
+ mFallbacks.put(prebaked.getEffectId(), fallback);
+ }
+ }
+ }
}
}
diff --git a/services/core/java/com/android/server/vibrator/SingleVibrationSession.java b/services/core/java/com/android/server/vibrator/SingleVibrationSession.java
new file mode 100644
index 0000000..f80407d
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/SingleVibrationSession.java
@@ -0,0 +1,173 @@
+/*
+ * 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.vibrator;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.CombinedVibration;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.NoSuchElementException;
+
+/**
+ * A vibration session holding a single {@link CombinedVibration} request, performed by a
+ * {@link VibrationStepConductor}.
+ */
+final class SingleVibrationSession implements VibrationSession, IBinder.DeathRecipient {
+ private static final String TAG = "SingleVibrationSession";
+
+ private final Object mLock = new Object();
+ private final IBinder mCallerToken;
+ private final HalVibration mVibration;
+
+ @GuardedBy("mLock")
+ private VibrationStepConductor mConductor;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private Runnable mBinderDeathCallback;
+
+ SingleVibrationSession(@NonNull IBinder callerToken, @NonNull CallerInfo callerInfo,
+ @NonNull CombinedVibration vibration) {
+ mCallerToken = callerToken;
+ mVibration = new HalVibration(callerInfo, vibration);
+ }
+
+ public void setVibrationConductor(@Nullable VibrationStepConductor conductor) {
+ synchronized (mLock) {
+ mConductor = conductor;
+ }
+ }
+
+ public HalVibration getVibration() {
+ return mVibration;
+ }
+
+ @Override
+ public long getCreateUptimeMillis() {
+ return mVibration.stats.getCreateUptimeMillis();
+ }
+
+ @Override
+ public boolean isRepeating() {
+ return mVibration.getEffectToPlay().getDuration() == Long.MAX_VALUE;
+ }
+
+ @Override
+ public CallerInfo getCallerInfo() {
+ return mVibration.callerInfo;
+ }
+
+ @Override
+ public IBinder getCallerToken() {
+ return mCallerToken;
+ }
+
+ @Override
+ public DebugInfo getDebugInfo() {
+ return mVibration.getDebugInfo();
+ }
+
+ @Override
+ public boolean wasEndRequested() {
+ if (mVibration.hasEnded()) {
+ return true;
+ }
+ synchronized (mLock) {
+ return mConductor != null && mConductor.wasNotifiedToCancel();
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ Slog.d(TAG, "Binder died, cancelling vibration...");
+ requestEnd(Status.CANCELLED_BINDER_DIED, /* endedBy= */ null, /* immediate= */ false);
+ Runnable callback;
+ synchronized (mLock) {
+ callback = mBinderDeathCallback;
+ }
+ if (callback != null) {
+ callback.run();
+ }
+ }
+
+ @Override
+ public boolean linkToDeath(@Nullable Runnable callback) {
+ synchronized (mLock) {
+ mBinderDeathCallback = callback;
+ }
+ try {
+ mCallerToken.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error linking vibration to token death", e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void unlinkToDeath() {
+ try {
+ mCallerToken.unlinkToDeath(this, 0);
+ } catch (NoSuchElementException e) {
+ Slog.wtf(TAG, "Failed to unlink vibration to token death", e);
+ }
+ synchronized (mLock) {
+ mBinderDeathCallback = null;
+ }
+ }
+
+ @Override
+ public void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy,
+ boolean immediate) {
+ synchronized (mLock) {
+ if (mConductor != null) {
+ mConductor.notifyCancelled(new Vibration.EndInfo(status, endedBy), immediate);
+ } else {
+ mVibration.end(new Vibration.EndInfo(status, endedBy));
+ }
+ }
+ }
+
+ @Override
+ public void notifyVibratorCallback(int vibratorId, long vibrationId) {
+ if (vibrationId != mVibration.id) {
+ return;
+ }
+ synchronized (mLock) {
+ if (mConductor != null) {
+ mConductor.notifyVibratorComplete(vibratorId);
+ }
+ }
+ }
+
+ @Override
+ public void notifySyncedVibratorsCallback(long vibrationId) {
+ if (vibrationId != mVibration.id) {
+ return;
+ }
+ synchronized (mLock) {
+ if (mConductor != null) {
+ mConductor.notifySyncedVibrationComplete();
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index 21fd4ce..bb2a17c 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -88,15 +88,9 @@
stats.reportEnded(endInfo.endedBy);
}
- /** Return true if vibration is a repeating vibration. */
- abstract boolean isRepeating();
-
/** Return {@link VibrationSession.DebugInfo} with read-only debug data about this vibration. */
abstract VibrationSession.DebugInfo getDebugInfo();
- /** Return {@link VibrationStats.StatsInfo} with read-only metrics about this vibration. */
- abstract VibrationStats.StatsInfo getStatsInfo(long completionUptimeMillis);
-
/** Immutable info passed as a signal to end a vibration. */
static final class EndInfo {
/** The vibration status to be set when it ends with this info. */
@@ -146,35 +140,41 @@
* potentially expensive or resource-linked objects, such as {@link IBinder}.
*/
static final class DebugInfoImpl implements VibrationSession.DebugInfo {
- final VibrationSession.Status mStatus;
- final long mCreateTime;
- final VibrationSession.CallerInfo mCallerInfo;
+ private final VibrationSession.Status mStatus;
+ private final VibrationStats.StatsInfo mStatsInfo;
+ private final VibrationSession.CallerInfo mCallerInfo;
@Nullable
- final CombinedVibration mPlayedEffect;
-
- private final long mStartTime;
- private final long mEndTime;
- private final long mDurationMs;
+ private final CombinedVibration mPlayedEffect;
@Nullable
private final CombinedVibration mOriginalEffect;
private final int mScaleLevel;
private final float mAdaptiveScale;
- DebugInfoImpl(VibrationSession.Status status, VibrationStats stats,
- @Nullable CombinedVibration playedEffect,
- @Nullable CombinedVibration originalEffect, int scaleLevel,
- float adaptiveScale, @NonNull VibrationSession.CallerInfo callerInfo) {
+ private final long mCreateUptime;
+ private final long mCreateTime;
+ private final long mStartTime;
+ private final long mEndTime;
+ private final long mDurationMs;
+
+ DebugInfoImpl(VibrationSession.Status status,
+ @NonNull VibrationSession.CallerInfo callerInfo, int vibrationType,
+ VibrationStats stats, @Nullable CombinedVibration playedEffect,
+ @Nullable CombinedVibration originalEffect, int scaleLevel, float adaptiveScale) {
Objects.requireNonNull(callerInfo);
- mCreateTime = stats.getCreateTimeDebug();
- mStartTime = stats.getStartTimeDebug();
- mEndTime = stats.getEndTimeDebug();
- mDurationMs = stats.getDurationDebug();
+ mCallerInfo = callerInfo;
+ mStatsInfo = stats.toStatsInfo(callerInfo.uid, vibrationType,
+ callerInfo.attrs.getUsage(), status);
mPlayedEffect = playedEffect;
mOriginalEffect = originalEffect;
mScaleLevel = scaleLevel;
mAdaptiveScale = adaptiveScale;
- mCallerInfo = callerInfo;
mStatus = status;
+
+ mCreateUptime = stats.getCreateUptimeMillis();
+ mCreateTime = stats.getCreateTimeDebug();
+ mStartTime = stats.getStartTimeDebug();
+ mEndTime = stats.getEndTimeDebug();
+ mDurationMs = stats.getDurationDebug();
}
@Override
@@ -184,7 +184,7 @@
@Override
public long getCreateUptimeMillis() {
- return mCreateTime;
+ return mCreateUptime;
}
@Override
@@ -216,6 +216,7 @@
@Override
public void logMetrics(VibratorFrameworkStatsLogger statsLogger) {
statsLogger.logVibrationAdaptiveHapticScale(mCallerInfo.uid, mAdaptiveScale);
+ statsLogger.writeVibrationReportedAsync(mStatsInfo);
}
@Override
diff --git a/services/core/java/com/android/server/vibrator/VibrationSession.java b/services/core/java/com/android/server/vibrator/VibrationSession.java
index 70477a2..4de8f78 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSession.java
@@ -39,9 +39,18 @@
*/
interface VibrationSession {
+ /** Returns the session creation time from {@link android.os.SystemClock#uptimeMillis()}. */
+ long getCreateUptimeMillis();
+
+ /** Return true if vibration session plays a repeating vibration. */
+ boolean isRepeating();
+
/** Returns data about the client app that triggered this vibration session. */
CallerInfo getCallerInfo();
+ /** Returns the binder token from the client app attached to this vibration session. */
+ IBinder getCallerToken();
+
/** Returns debug data for logging and metric reports. */
DebugInfo getDebugInfo();
@@ -58,6 +67,19 @@
/** Removes link to the app process death. */
void unlinkToDeath();
+ /** Returns true if this session was requested to end by {@link #requestEnd}. */
+ boolean wasEndRequested();
+
+ /**
+ * Request the end of this session, which might be acted upon asynchronously.
+ *
+ * <p>This is the same as {@link #requestEnd(Status, CallerInfo, boolean)}, with no
+ * {@link CallerInfo} and with {@code immediate} flag set to false.
+ */
+ default void requestEnd(@NonNull Status status) {
+ requestEnd(status, /* endedBy= */ null, /* immediate= */ false);
+ }
+
/**
* Notify the session end was requested, which might be acted upon asynchronously.
*
@@ -71,6 +93,25 @@
void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy, boolean immediate);
/**
+ * Notify a vibrator has completed the last command during the playback of given vibration.
+ *
+ * <p>This will be called by the vibrator hardware callback indicating the last vibrate call is
+ * complete (e.g. on(), perform(), compose()). This does not mean the vibration is complete,
+ * since its playback might have one or more interactions with the vibrator hardware.
+ */
+ void notifyVibratorCallback(int vibratorId, long vibrationId);
+
+ /**
+ * Notify all synced vibrators have completed the last synchronized command during the playback
+ * of given vibration.
+ *
+ * <p>This will be called by the vibrator manager hardware callback indicating the last
+ * synchronized vibrate call is complete. This does not mean the vibration is complete, since
+ * its playback might have one or more interactions with the vibrator hardware.
+ */
+ void notifySyncedVibratorsCallback(long vibrationId);
+
+ /**
* Session status with reference to values from vibratormanagerservice.proto for logging.
*/
enum Status {
diff --git a/services/core/java/com/android/server/vibrator/VibrationStats.java b/services/core/java/com/android/server/vibrator/VibrationStats.java
index fc0c6e7..637a5a1 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStats.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStats.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.CombinedVibration;
import android.os.SystemClock;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
@@ -37,11 +38,11 @@
// vibrate request.
// - Start: time a vibration started to play, which is closer to the time that the
// VibrationEffect started playing the very first segment.
- // - End: time a vibration ended, even if it never started to play. This can be as soon as the
- // vibrator HAL reports it has finished the last command, or before it has even started
- // when the vibration is ignored or cancelled.
- // Create and end times set by VibratorManagerService only, guarded by its lock.
- // Start times set by VibrationThread only (single-threaded).
+ // - End: time a vibration ended with a status, even if it never started to play. This can be as
+ // soon as the vibrator HAL reports it has finished the last command, or before it has
+ // even started when the vibration is ignored or cancelled.
+ // Created and ended times set by VibratorManagerService only, guarded by its lock.
+ // Start time set by VibrationThread only (single-threaded).
private long mCreateUptimeMillis;
private long mStartUptimeMillis;
private long mEndUptimeMillis;
@@ -97,6 +98,10 @@
mInterruptedUsage = -1;
}
+ StatsInfo toStatsInfo(int uid, int vibrationType, int usage, VibrationSession.Status status) {
+ return new VibrationStats.StatsInfo(uid, vibrationType, usage, status, this);
+ }
+
long getCreateUptimeMillis() {
return mCreateUptimeMillis;
}
@@ -300,7 +305,7 @@
* {@link com.android.internal.util.FrameworkStatsLog} as a
* {@link com.android.internal.util.FrameworkStatsLog#VIBRATION_REPORTED}.
*/
- static final class StatsInfo {
+ public static final class StatsInfo {
public final int uid;
public final int vibrationType;
public final int usage;
@@ -331,7 +336,7 @@
private boolean mIsWritten;
StatsInfo(int uid, int vibrationType, int usage, VibrationSession.Status status,
- VibrationStats stats, long completionUptimeMillis) {
+ VibrationStats stats) {
this.uid = uid;
this.vibrationType = vibrationType;
this.usage = usage;
@@ -342,6 +347,9 @@
interruptedUsage = stats.mInterruptedUsage;
repeatCount = stats.mRepeatCount;
+ // Consider this vibration is being completed now.
+ long completionUptimeMillis = SystemClock.uptimeMillis();
+
// This duration goes from the time this object was created until the time it was
// completed. We can use latencies to detect the times between first and last
// interaction with vibrator.
@@ -419,5 +427,25 @@
}
return res;
}
+
+ /**
+ * Returns the vibration type value from {@code ReportedVibration} that best represents this
+ * {@link CombinedVibration}.
+ *
+ * <p>This does not include external vibrations, as those are not represented by a single
+ * vibration effect.
+ */
+ public static int findVibrationType(@Nullable CombinedVibration effect) {
+ if (effect == null) {
+ return FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE;
+ }
+ if (effect.hasVendorEffects()) {
+ return FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__VENDOR;
+ }
+ if (effect.getDuration() == Long.MAX_VALUE) {
+ return FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__REPEATED;
+ }
+ return FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE;
+ }
}
}
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 1d52e3c..4bb0c16 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -20,8 +20,6 @@
import android.annotation.Nullable;
import android.os.Build;
import android.os.CombinedVibration;
-import android.os.IBinder;
-import android.os.RemoteException;
import android.os.VibrationEffect;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
@@ -39,7 +37,6 @@
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
-import java.util.NoSuchElementException;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.concurrent.CancellationException;
@@ -55,7 +52,7 @@
* VibrationThread. The only thread-safe methods for calling from other threads are the "notify"
* methods (which should never be used from the VibrationThread thread).
*/
-final class VibrationStepConductor implements IBinder.DeathRecipient {
+final class VibrationStepConductor {
private static final boolean DEBUG = VibrationThread.DEBUG;
private static final String TAG = VibrationThread.TAG;
@@ -346,42 +343,6 @@
}
/**
- * Binder death notification. VibrationThread registers this when it's running a conductor.
- * Note that cancellation could theoretically happen immediately, before the conductor has
- * started, but in this case it will be processed in the first signals loop.
- */
- @Override
- public void binderDied() {
- if (DEBUG) {
- Slog.d(TAG, "Binder died, cancelling vibration...");
- }
- notifyCancelled(new Vibration.EndInfo(Status.CANCELLED_BINDER_DIED),
- /* immediate= */ false);
- }
-
- /**
- * Returns true if successfully linked this conductor to the death of the binder that requested
- * the vibration.
- */
- public boolean linkToDeath() {
- try {
- mVibration.callerToken.linkToDeath(this, 0);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error linking vibration to token death", e);
- return false;
- }
- return true;
- }
-
- public void unlinkToDeath() {
- try {
- mVibration.callerToken.unlinkToDeath(this, 0);
- } catch (NoSuchElementException e) {
- Slog.wtf(TAG, "Failed to unlink vibration to token death", e);
- }
- }
-
- /**
* Notify the execution that cancellation is requested. This will be acted upon
* asynchronously in the VibrationThread.
*
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 07473d1..9b7bdec 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -51,7 +51,6 @@
import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.ShellCommand;
-import android.os.SystemClock;
import android.os.Trace;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
@@ -159,9 +158,9 @@
@GuardedBy("mLock")
private final SparseArray<AlwaysOnVibration> mAlwaysOnEffects = new SparseArray<>();
@GuardedBy("mLock")
- private VibrationStepConductor mCurrentVibration;
+ private SingleVibrationSession mCurrentVibration;
@GuardedBy("mLock")
- private VibrationStepConductor mNextVibration;
+ private SingleVibrationSession mNextVibration;
@GuardedBy("mLock")
private ExternalVibrationSession mCurrentExternalVibration;
@GuardedBy("mLock")
@@ -188,24 +187,20 @@
// When the system is entering a non-interactive state, we want to cancel
// vibrations in case a misbehaving app has abandoned them.
if (shouldCancelOnScreenOffLocked(mNextVibration)) {
- clearNextVibrationLocked(new Vibration.EndInfo(
- Status.CANCELLED_BY_SCREEN_OFF));
+ clearNextVibrationLocked(Status.CANCELLED_BY_SCREEN_OFF);
}
if (shouldCancelOnScreenOffLocked(mCurrentVibration)) {
- mCurrentVibration.notifyCancelled(new Vibration.EndInfo(
- Status.CANCELLED_BY_SCREEN_OFF), /* immediate= */ false);
+ mCurrentVibration.requestEnd(Status.CANCELLED_BY_SCREEN_OFF);
}
}
} else if (android.multiuser.Flags.addUiForSoundsFromBackgroundUsers()
&& intent.getAction().equals(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND)) {
synchronized (mLock) {
if (shouldCancelOnFgUserRequest(mNextVibration)) {
- clearNextVibrationLocked(new Vibration.EndInfo(
- Status.CANCELLED_BY_FOREGROUND_USER));
+ clearNextVibrationLocked(Status.CANCELLED_BY_FOREGROUND_USER);
}
if (shouldCancelOnFgUserRequest(mCurrentVibration)) {
- mCurrentVibration.notifyCancelled(new Vibration.EndInfo(
- Status.CANCELLED_BY_FOREGROUND_USER), /* immediate= */ false);
+ mCurrentVibration.requestEnd(Status.CANCELLED_BY_FOREGROUND_USER);
}
}
}
@@ -222,12 +217,10 @@
}
synchronized (mLock) {
if (shouldCancelAppOpModeChangedLocked(mNextVibration)) {
- clearNextVibrationLocked(new Vibration.EndInfo(
- Status.CANCELLED_BY_APP_OPS));
+ clearNextVibrationLocked(Status.CANCELLED_BY_APP_OPS);
}
if (shouldCancelAppOpModeChangedLocked(mCurrentVibration)) {
- mCurrentVibration.notifyCancelled(new Vibration.EndInfo(
- Status.CANCELLED_BY_APP_OPS), /* immediate= */ false);
+ mCurrentVibration.requestEnd(Status.CANCELLED_BY_APP_OPS);
}
}
}
@@ -602,8 +595,9 @@
return null;
}
// Create Vibration.Stats as close to the received request as possible, for tracking.
- HalVibration vib = new HalVibration(token, effect, callerInfo);
- fillVibrationFallbacks(vib, effect);
+ SingleVibrationSession session = new SingleVibrationSession(token, callerInfo, effect);
+ HalVibration vib = session.getVibration();
+ vib.fillFallbacks(mVibrationSettings::getFallbackEffect);
if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) {
// Force update of user settings before checking if this vibration effect should
@@ -617,21 +611,26 @@
}
// Check if user settings or DnD is set to ignore this vibration.
- Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(vib.callerInfo);
+ Status ignoreStatus = shouldIgnoreVibrationLocked(callerInfo);
+ CallerInfo ignoredBy = null;
// Check if ongoing vibration is more important than this vibration.
- if (vibrationEndInfo == null) {
- vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(vib);
+ if (ignoreStatus == null) {
+ Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(session);
+ if (vibrationEndInfo != null) {
+ ignoreStatus = vibrationEndInfo.status;
+ ignoredBy = vibrationEndInfo.endedBy;
+ }
}
// If not ignored so far then try to start this vibration.
- if (vibrationEndInfo == null) {
+ if (ignoreStatus == null) {
final long ident = Binder.clearCallingIdentity();
try {
if (mCurrentExternalVibration != null) {
vib.stats.reportInterruptedAnotherVibration(
mCurrentExternalVibration.getCallerInfo());
- endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED, vib.callerInfo,
+ endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED, callerInfo,
/* continueExternalControl= */ false);
} else if (mCurrentVibration != null) {
if (mCurrentVibration.getVibration().canPipelineWith(vib)) {
@@ -645,21 +644,19 @@
} else {
vib.stats.reportInterruptedAnotherVibration(
mCurrentVibration.getVibration().callerInfo);
- mCurrentVibration.notifyCancelled(
- new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED,
- vib.callerInfo),
+ mCurrentVibration.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo,
/* immediate= */ false);
}
}
- vibrationEndInfo = startVibrationLocked(vib);
+ ignoreStatus = startVibrationLocked(session);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
// Ignored or failed to start the vibration, end it and report metrics right away.
- if (vibrationEndInfo != null) {
- endVibrationLocked(vib, vibrationEndInfo);
+ if (ignoreStatus != null) {
+ endVibrationLocked(session, ignoreStatus, ignoredBy);
}
return vib;
}
@@ -677,27 +674,18 @@
if (DEBUG) {
Slog.d(TAG, "Canceling vibration");
}
- Vibration.EndInfo cancelledByUserInfo =
- new Vibration.EndInfo(Status.CANCELLED_BY_USER);
final long ident = Binder.clearCallingIdentity();
try {
- if (mNextVibration != null
- && shouldCancelVibration(mNextVibration.getVibration(),
- usageFilter, token)) {
- clearNextVibrationLocked(cancelledByUserInfo);
+ if (shouldCancelVibration(mNextVibration, usageFilter, token)) {
+ clearNextVibrationLocked(Status.CANCELLED_BY_USER);
}
- if (mCurrentVibration != null
- && shouldCancelVibration(mCurrentVibration.getVibration(),
- usageFilter, token)) {
- mCurrentVibration.notifyCancelled(
- cancelledByUserInfo, /* immediate= */false);
+ if (shouldCancelVibration(mCurrentVibration, usageFilter, token)) {
+ mCurrentVibration.requestEnd(Status.CANCELLED_BY_USER);
}
- if (mCurrentExternalVibration != null
- && shouldCancelVibration(
- mCurrentExternalVibration.getCallerInfo().attrs,
- usageFilter)) {
- endExternalVibrateLocked(cancelledByUserInfo.status,
- cancelledByUserInfo.endedBy, /* continueExternalControl= */ false);
+ // TODO(b/370948466): investigate why token is not checked here and fix it.
+ if (shouldCancelVibration(mCurrentExternalVibration, usageFilter, null)) {
+ endExternalVibrateLocked(Status.CANCELLED_BY_USER,
+ /* endedBy= */ null, /* continueExternalControl= */ false);
}
} finally {
Binder.restoreCallingIdentity(ident);
@@ -842,18 +830,13 @@
return;
}
- HalVibration vib = mCurrentVibration.getVibration();
- Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(vib.callerInfo);
-
- if (inputDevicesChanged || (vibrationEndInfo != null)) {
+ Status ignoreStatus = shouldIgnoreVibrationLocked(mCurrentVibration.getCallerInfo());
+ if (inputDevicesChanged || (ignoreStatus != null)) {
if (DEBUG) {
Slog.d(TAG, "Canceling vibration because settings changed: "
- + (inputDevicesChanged ? "input devices changed"
- : vibrationEndInfo.status));
+ + (inputDevicesChanged ? "input devices changed" : ignoreStatus));
}
- mCurrentVibration.notifyCancelled(
- new Vibration.EndInfo(Status.CANCELLED_BY_SETTINGS_UPDATE),
- /* immediate= */ false);
+ mCurrentVibration.requestEnd(Status.CANCELLED_BY_SETTINGS_UPDATE);
}
}
}
@@ -873,8 +856,8 @@
if (vibrator == null) {
continue;
}
- Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(vib.callerInfo);
- if (vibrationEndInfo == null) {
+ Status ignoreStatus = shouldIgnoreVibrationLocked(vib.callerInfo);
+ if (ignoreStatus == null) {
effect = mVibrationScaler.scale(effect, vib.callerInfo.attrs.getUsage());
} else {
// Vibration should not run, use null effect to remove registered effect.
@@ -886,25 +869,21 @@
@GuardedBy("mLock")
@Nullable
- private Vibration.EndInfo startVibrationLocked(HalVibration vib) {
+ private Status startVibrationLocked(SingleVibrationSession session) {
Trace.traceBegin(TRACE_TAG_VIBRATOR, "startVibrationLocked");
try {
if (mInputDeviceDelegate.isAvailable()) {
- return startVibrationOnInputDevicesLocked(vib);
+ return startVibrationOnInputDevicesLocked(session.getVibration());
}
-
- VibrationStepConductor conductor = createVibrationStepConductor(vib);
-
if (mCurrentVibration == null) {
- return startVibrationOnThreadLocked(conductor);
+ return startVibrationOnThreadLocked(session);
}
// If there's already a vibration queued (waiting for the previous one to finish
// cancelling), end it cleanly and replace it with the new one.
// Note that we don't consider pipelining here, because new pipelined ones should
// replace pending non-executing pipelined ones anyway.
- clearNextVibrationLocked(
- new Vibration.EndInfo(Status.IGNORED_SUPERSEDED, vib.callerInfo));
- mNextVibration = conductor;
+ clearNextVibrationLocked(Status.IGNORED_SUPERSEDED, session.getCallerInfo());
+ mNextVibration = session;
return null;
} finally {
Trace.traceEnd(TRACE_TAG_VIBRATOR);
@@ -913,50 +892,45 @@
@GuardedBy("mLock")
@Nullable
- private Vibration.EndInfo startVibrationOnThreadLocked(VibrationStepConductor conductor) {
- HalVibration vib = conductor.getVibration();
- int mode = startAppOpModeLocked(vib.callerInfo);
+ private Status startVibrationOnThreadLocked(SingleVibrationSession session) {
+ VibrationStepConductor conductor = createVibrationStepConductor(session.getVibration());
+ session.setVibrationConductor(conductor);
+ int mode = startAppOpModeLocked(session.getCallerInfo());
switch (mode) {
case AppOpsManager.MODE_ALLOWED:
Trace.asyncTraceBegin(TRACE_TAG_VIBRATOR, "vibration", 0);
// Make sure mCurrentVibration is set while triggering the VibrationThread.
- mCurrentVibration = conductor;
- if (!mCurrentVibration.linkToDeath()) {
+ mCurrentVibration = session;
+ if (!mCurrentVibration.linkToDeath(null)) {
// Shouldn't happen. The method call already logs a wtf.
mCurrentVibration = null; // Aborted.
- return new Vibration.EndInfo(Status.IGNORED_ERROR_TOKEN);
+ return Status.IGNORED_ERROR_TOKEN;
}
- if (!mVibrationThread.runVibrationOnVibrationThread(mCurrentVibration)) {
+ if (!mVibrationThread.runVibrationOnVibrationThread(conductor)) {
// Shouldn't happen. The method call already logs a wtf.
+ mCurrentVibration.setVibrationConductor(null);
mCurrentVibration = null; // Aborted.
- return new Vibration.EndInfo(Status.IGNORED_ERROR_SCHEDULING);
+ return Status.IGNORED_ERROR_SCHEDULING;
}
return null;
case AppOpsManager.MODE_ERRORED:
Slog.w(TAG, "Start AppOpsManager operation errored for uid "
- + vib.callerInfo.uid);
- return new Vibration.EndInfo(Status.IGNORED_ERROR_APP_OPS);
+ + session.getCallerInfo().uid);
+ return Status.IGNORED_ERROR_APP_OPS;
default:
- return new Vibration.EndInfo(Status.IGNORED_APP_OPS);
+ return Status.IGNORED_APP_OPS;
}
}
@GuardedBy("mLock")
- private void endVibrationLocked(Vibration vib, Status status) {
- endVibrationLocked(vib, new Vibration.EndInfo(status));
+ private void endVibrationLocked(VibrationSession session, Status status) {
+ endVibrationLocked(session, status, /* endedBy= */ null);
}
@GuardedBy("mLock")
- private void endVibrationLocked(Vibration vib, Vibration.EndInfo vibrationEndInfo) {
- vib.end(vibrationEndInfo);
- reportEndedVibrationLocked(vib);
- }
-
- @GuardedBy("mLock")
- private void reportEndedVibrationLocked(Vibration vib) {
- logAndRecordVibration(vib.getDebugInfo());
- mFrameworkStatsLogger.writeVibrationReportedAsync(
- vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis()));
+ private void endVibrationLocked(VibrationSession session, Status status, CallerInfo endedBy) {
+ session.requestEnd(status, endedBy, /* immediate= */ false);
+ logAndRecordVibration(session.getDebugInfo());
}
private VibrationStepConductor createVibrationStepConductor(HalVibration vib) {
@@ -975,12 +949,11 @@
mFrameworkStatsLogger, requestVibrationParamsFuture, mVibrationThreadCallbacks);
}
- private Vibration.EndInfo startVibrationOnInputDevicesLocked(HalVibration vib) {
+ private Status startVibrationOnInputDevicesLocked(HalVibration vib) {
// Scale resolves the default amplitudes from the effect before scaling them.
vib.scaleEffects(mVibrationScaler);
mInputDeviceDelegate.vibrateIfAvailable(vib.callerInfo, vib.getEffectToPlay());
-
- return new Vibration.EndInfo(Status.FORWARDED_TO_INPUT_DEVICES);
+ return Status.FORWARDED_TO_INPUT_DEVICES;
}
private void logAndRecordPerformHapticFeedbackAttempt(int uid, int deviceId, String opPkg,
@@ -994,9 +967,10 @@
private void logAndRecordVibrationAttempt(@Nullable CombinedVibration effect,
CallerInfo callerInfo, Status status) {
logAndRecordVibration(
- new Vibration.DebugInfoImpl(status, new VibrationStats(),
+ new Vibration.DebugInfoImpl(status, callerInfo,
+ VibrationStats.StatsInfo.findVibrationType(effect), new VibrationStats(),
effect, /* originalEffect= */ null, VibrationScaler.SCALE_NONE,
- VibrationScaler.ADAPTIVE_SCALE_NONE, callerInfo));
+ VibrationScaler.ADAPTIVE_SCALE_NONE));
}
private void logAndRecordVibration(DebugInfo info) {
@@ -1050,39 +1024,25 @@
}
}
- @GuardedBy("mLock")
- private void reportFinishedVibrationLocked() {
- Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
- mCurrentVibration.unlinkToDeath();
- HalVibration vib = mCurrentVibration.getVibration();
- if (DEBUG) {
- Slog.d(TAG, "Reporting vibration " + vib.id + " finished with " + vib.getStatus());
- }
- finishAppOpModeLocked(vib.callerInfo);
- reportEndedVibrationLocked(vib);
- }
-
private void onSyncedVibrationComplete(long vibrationId) {
synchronized (mLock) {
- if (mCurrentVibration != null
- && mCurrentVibration.getVibration().id == vibrationId) {
+ if (mCurrentVibration != null) {
if (DEBUG) {
Slog.d(TAG, "Synced vibration " + vibrationId + " complete, notifying thread");
}
- mCurrentVibration.notifySyncedVibrationComplete();
+ mCurrentVibration.notifySyncedVibratorsCallback(vibrationId);
}
}
}
private void onVibrationComplete(int vibratorId, long vibrationId) {
synchronized (mLock) {
- if (mCurrentVibration != null
- && mCurrentVibration.getVibration().id == vibrationId) {
+ if (mCurrentVibration != null) {
if (DEBUG) {
Slog.d(TAG, "Vibration " + vibrationId + " on vibrator " + vibratorId
+ " complete, notifying thread");
}
- mCurrentVibration.notifyVibratorComplete(vibratorId);
+ mCurrentVibration.notifyVibratorCallback(vibratorId, vibrationId);
}
}
}
@@ -1094,14 +1054,14 @@
*/
@GuardedBy("mLock")
@Nullable
- private Vibration.EndInfo shouldIgnoreVibrationForOngoingLocked(Vibration vib) {
+ private Vibration.EndInfo shouldIgnoreVibrationForOngoingLocked(VibrationSession session) {
if (mCurrentExternalVibration != null) {
- return shouldIgnoreVibrationForOngoing(vib, mCurrentExternalVibration);
+ return shouldIgnoreVibrationForOngoing(session, mCurrentExternalVibration);
}
if (mNextVibration != null) {
- Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationForOngoing(vib,
- mNextVibration.getVibration());
+ Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationForOngoing(session,
+ mNextVibration);
if (vibrationEndInfo != null) {
// Next vibration has higher importance than the new one, so the new vibration
// should be ignored.
@@ -1110,14 +1070,12 @@
}
if (mCurrentVibration != null) {
- HalVibration currentVibration = mCurrentVibration.getVibration();
- if (currentVibration.hasEnded() || mCurrentVibration.wasNotifiedToCancel()) {
- // Current vibration has ended or is cancelling, should not block incoming
- // vibrations.
+ if (mCurrentVibration.wasEndRequested()) {
+ // Current session has ended or is cancelling, should not block incoming vibrations.
return null;
}
- return shouldIgnoreVibrationForOngoing(vib, currentVibration);
+ return shouldIgnoreVibrationForOngoing(session, mCurrentVibration);
}
return null;
@@ -1125,32 +1083,33 @@
/**
* Checks if the ongoing vibration has higher importance than the new one. If they have similar
- * importance, then {@link Vibration#isRepeating()} is used as a tiebreaker.
+ * importance, then {@link VibrationSession#isRepeating()} is used as a tiebreaker.
*
* @return a Vibration.EndInfo if the vibration should be ignored, null otherwise.
*/
@Nullable
private static Vibration.EndInfo shouldIgnoreVibrationForOngoing(
- @NonNull Vibration newVibration, @NonNull Vibration ongoingVibration) {
+ @NonNull VibrationSession newSession, @NonNull VibrationSession ongoingSession) {
- int newVibrationImportance = getVibrationImportance(newVibration);
- int ongoingVibrationImportance = getVibrationImportance(ongoingVibration);
+ int newSessionImportance = getVibrationImportance(newSession);
+ int ongoingSessionImportance = getVibrationImportance(ongoingSession);
- if (newVibrationImportance > ongoingVibrationImportance) {
+ if (newSessionImportance > ongoingSessionImportance) {
// New vibration has higher importance and should not be ignored.
return null;
}
- if (ongoingVibrationImportance > newVibrationImportance) {
+ if (ongoingSessionImportance > newSessionImportance) {
// Existing vibration has higher importance and should not be cancelled.
return new Vibration.EndInfo(Status.IGNORED_FOR_HIGHER_IMPORTANCE,
- ongoingVibration.callerInfo);
+ ongoingSession.getCallerInfo());
}
// Same importance, use repeating as a tiebreaker.
- if (ongoingVibration.isRepeating() && !newVibration.isRepeating()) {
+ if (ongoingSession.isRepeating() && !newSession.isRepeating()) {
// Ongoing vibration is repeating and new one is not, give priority to ongoing
- return new Vibration.EndInfo(Status.IGNORED_FOR_ONGOING, ongoingVibration.callerInfo);
+ return new Vibration.EndInfo(Status.IGNORED_FOR_ONGOING,
+ ongoingSession.getCallerInfo());
}
// New vibration is repeating or this is a complete tie between them,
// give priority to new vibration.
@@ -1164,10 +1123,10 @@
* @return a numeric representation for the vibration importance, larger values represent a
* higher importance
*/
- private static int getVibrationImportance(Vibration vibration) {
- int usage = vibration.callerInfo.attrs.getUsage();
+ private static int getVibrationImportance(VibrationSession session) {
+ int usage = session.getCallerInfo().attrs.getUsage();
if (usage == VibrationAttributes.USAGE_UNKNOWN) {
- if (vibration.isRepeating()) {
+ if (session.isRepeating()) {
usage = VibrationAttributes.USAGE_RINGTONE;
} else {
usage = VibrationAttributes.USAGE_TOUCH;
@@ -1201,10 +1160,10 @@
*/
@GuardedBy("mLock")
@Nullable
- private Vibration.EndInfo shouldIgnoreVibrationLocked(CallerInfo callerInfo) {
+ private Status shouldIgnoreVibrationLocked(CallerInfo callerInfo) {
Status statusFromSettings = mVibrationSettings.shouldIgnoreVibration(callerInfo);
if (statusFromSettings != null) {
- return new Vibration.EndInfo(statusFromSettings);
+ return statusFromSettings;
}
int mode = checkAppOpModeLocked(callerInfo);
@@ -1212,9 +1171,9 @@
if (mode == AppOpsManager.MODE_ERRORED) {
// We might be getting calls from within system_server, so we don't actually
// want to throw a SecurityException here.
- return new Vibration.EndInfo(Status.IGNORED_ERROR_APP_OPS);
+ return Status.IGNORED_ERROR_APP_OPS;
} else {
- return new Vibration.EndInfo(Status.IGNORED_APP_OPS);
+ return Status.IGNORED_APP_OPS;
}
}
@@ -1239,32 +1198,29 @@
/**
* Return true if the vibration has the same token and usage belongs to given usage class.
*
- * @param vib The ongoing or pending vibration to be cancelled.
+ * @param session The ongoing or pending vibration session to be cancelled.
* @param usageFilter The vibration usages to be cancelled, any bitwise combination of
* VibrationAttributes.USAGE_* values.
- * @param token The binder token to identify the vibration origin. Only vibrations
+ * @param tokenFilter The binder token to identify the vibration origin. Only vibrations
* started with the same token can be cancelled with it.
*/
- private boolean shouldCancelVibration(HalVibration vib, int usageFilter, IBinder token) {
- return (vib.callerToken == token) && shouldCancelVibration(vib.callerInfo.attrs,
- usageFilter);
- }
-
- /**
- * Return true if the external vibration usage belongs to given usage class.
- *
- * @param attrs The attributes of an ongoing or pending vibration to be cancelled.
- * @param usageFilter The vibration usages to be cancelled, any bitwise combination of
- * VibrationAttributes.USAGE_* values.
- */
- private boolean shouldCancelVibration(VibrationAttributes attrs, int usageFilter) {
- if (attrs.getUsage() == VibrationAttributes.USAGE_UNKNOWN) {
+ private boolean shouldCancelVibration(@Nullable VibrationSession session, int usageFilter,
+ @Nullable IBinder tokenFilter) {
+ if (session == null) {
+ return false;
+ }
+ if ((tokenFilter != null) && (tokenFilter != session.getCallerToken())) {
+ // Vibration from a different app, this should not cancel it.
+ return false;
+ }
+ int usage = session.getCallerInfo().attrs.getUsage();
+ if (usage == VibrationAttributes.USAGE_UNKNOWN) {
// Special case, usage UNKNOWN would match all filters. Instead it should only match if
// it's cancelling that usage specifically, or if cancelling all usages.
return usageFilter == VibrationAttributes.USAGE_UNKNOWN
|| usageFilter == VibrationAttributes.USAGE_FILTER_MATCH_ALL;
}
- return (usageFilter & attrs.getUsage()) == attrs.getUsage();
+ return (usageFilter & usage) == usage;
}
/**
@@ -1340,45 +1296,6 @@
}
/**
- * Sets fallback effects to all prebaked ones in given combination of effects, based on {@link
- * VibrationSettings#getFallbackEffect}.
- */
- private void fillVibrationFallbacks(HalVibration vib, CombinedVibration effect) {
- if (effect instanceof CombinedVibration.Mono) {
- fillVibrationFallbacks(vib, ((CombinedVibration.Mono) effect).getEffect());
- } else if (effect instanceof CombinedVibration.Stereo) {
- SparseArray<VibrationEffect> effects =
- ((CombinedVibration.Stereo) effect).getEffects();
- for (int i = 0; i < effects.size(); i++) {
- fillVibrationFallbacks(vib, effects.valueAt(i));
- }
- } else if (effect instanceof CombinedVibration.Sequential) {
- List<CombinedVibration> effects =
- ((CombinedVibration.Sequential) effect).getEffects();
- for (int i = 0; i < effects.size(); i++) {
- fillVibrationFallbacks(vib, effects.get(i));
- }
- }
- }
-
- private void fillVibrationFallbacks(HalVibration vib, VibrationEffect effect) {
- if (!(effect instanceof VibrationEffect.Composed composed)) {
- return;
- }
- int segmentCount = composed.getSegments().size();
- for (int i = 0; i < segmentCount; i++) {
- VibrationEffectSegment segment = composed.getSegments().get(i);
- if (segment instanceof PrebakedSegment prebaked) {
- VibrationEffect fallback = mVibrationSettings.getFallbackEffect(
- prebaked.getEffectId());
- if (prebaked.shouldFallback() && fallback != null) {
- vib.addFallback(prebaked.getEffectId(), fallback);
- }
- }
- }
- }
-
- /**
* Return new {@link VibrationAttributes} that only applies flags that this user has permissions
* to use.
*/
@@ -1475,30 +1392,28 @@
}
@GuardedBy("mLock")
- private boolean shouldCancelOnScreenOffLocked(@Nullable VibrationStepConductor conductor) {
- if (conductor == null) {
+ private boolean shouldCancelOnScreenOffLocked(@Nullable VibrationSession session) {
+ if (session == null) {
return false;
}
- HalVibration vib = conductor.getVibration();
- return mVibrationSettings.shouldCancelVibrationOnScreenOff(vib.callerInfo,
- vib.stats.getCreateUptimeMillis());
+ return mVibrationSettings.shouldCancelVibrationOnScreenOff(session.getCallerInfo(),
+ session.getCreateUptimeMillis());
}
@GuardedBy("mLock")
- private boolean shouldCancelAppOpModeChangedLocked(@Nullable VibrationStepConductor conductor) {
- if (conductor == null) {
+ private boolean shouldCancelAppOpModeChangedLocked(@Nullable VibrationSession session) {
+ if (session == null) {
return false;
}
- return checkAppOpModeLocked(conductor.getVibration().callerInfo)
- != AppOpsManager.MODE_ALLOWED;
+ return checkAppOpModeLocked(session.getCallerInfo()) != AppOpsManager.MODE_ALLOWED;
}
@GuardedBy("mLock")
- private boolean shouldCancelOnFgUserRequest(@Nullable VibrationStepConductor conductor) {
- if (conductor == null) {
+ private boolean shouldCancelOnFgUserRequest(@Nullable VibrationSession session) {
+ if (session == null) {
return false;
}
- return conductor.getVibration().callerInfo.attrs.getUsageClass() == USAGE_CLASS_ALARM;
+ return session.getCallerInfo().attrs.getUsageClass() == USAGE_CLASS_ALARM;
}
@GuardedBy("mLock")
@@ -1660,17 +1575,21 @@
}
if (mCurrentVibration != null) {
// This is when we consider the current vibration complete, report metrics.
- reportFinishedVibrationLocked();
+ if (DEBUG) {
+ Slog.d(TAG, "Reporting vibration " + vibrationId + " finished.");
+ }
+ mCurrentVibration.unlinkToDeath();
+ finishAppOpModeLocked(mCurrentVibration.getCallerInfo());
+ logAndRecordVibration(mCurrentVibration.getDebugInfo());
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
mCurrentVibration = null;
}
if (mNextVibration != null) {
- VibrationStepConductor nextConductor = mNextVibration;
+ SingleVibrationSession nextVibration = mNextVibration;
mNextVibration = null;
- Vibration.EndInfo vibrationEndInfo = startVibrationOnThreadLocked(
- nextConductor);
- if (vibrationEndInfo != null) {
- // Failed to start the vibration, end it and report metrics right away.
- endVibrationLocked(nextConductor.getVibration(), vibrationEndInfo);
+ Status startErrorStatus = startVibrationOnThreadLocked(nextVibration);
+ if (startErrorStatus != null) {
+ endVibrationLocked(nextVibration, startErrorStatus);
}
}
}
@@ -1892,17 +1811,22 @@
mInfo.dump(proto, fieldId);
}
}
+ /** Clears mNextVibration if set, ending it cleanly */
+ @GuardedBy("mLock")
+ private void clearNextVibrationLocked(Status status) {
+ clearNextVibrationLocked(status, /* endedBy= */ null);
+ }
/** Clears mNextVibration if set, ending it cleanly */
@GuardedBy("mLock")
- private void clearNextVibrationLocked(Vibration.EndInfo vibrationEndInfo) {
+ private void clearNextVibrationLocked(Status status, CallerInfo endedBy) {
if (mNextVibration != null) {
if (DEBUG) {
- Slog.d(TAG, "Dropping pending vibration " + mNextVibration.getVibration().id
- + " with end info: " + vibrationEndInfo);
+ Slog.d(TAG, "Dropping pending vibration from " + mNextVibration.getCallerInfo()
+ + " with status: " + status);
}
// Clearing next vibration before playing it, end it and report metrics right away.
- endVibrationLocked(mNextVibration.getVibration(), vibrationEndInfo);
+ endVibrationLocked(mNextVibration, status, endedBy);
mNextVibration = null;
}
}
@@ -1927,7 +1851,7 @@
setExternalControl(false, mCurrentExternalVibration.stats);
}
// The external control was turned off, end it and report metrics right away.
- reportEndedVibrationLocked(mCurrentExternalVibration);
+ logAndRecordVibration(mCurrentExternalVibration.getDebugInfo());
mCurrentExternalVibration = null;
}
@@ -1987,16 +1911,16 @@
try {
// Create Vibration.Stats as close to the received request as possible, for
// tracking.
- ExternalVibrationSession externalVibration = new ExternalVibrationSession(vib);
+ ExternalVibrationSession session = new ExternalVibrationSession(vib);
// Mute the request until we run all the checks and accept the vibration.
- externalVibration.muteScale();
+ session.muteScale();
boolean alreadyUnderExternalControl = false;
boolean waitForCompletion = false;
synchronized (mLock) {
if (!hasExternalControlCapability()) {
- endVibrationLocked(externalVibration, Status.IGNORED_UNSUPPORTED);
- return externalVibration.getScale();
+ endVibrationLocked(session, Status.IGNORED_UNSUPPORTED);
+ return session.getScale();
}
if (ActivityManager.checkComponentPermission(
@@ -2006,29 +1930,30 @@
Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid()
+ " tried to play externally controlled vibration"
+ " without VIBRATE permission, ignoring.");
- endVibrationLocked(externalVibration, Status.IGNORED_MISSING_PERMISSION);
- return externalVibration.getScale();
+ endVibrationLocked(session, Status.IGNORED_MISSING_PERMISSION);
+ return session.getScale();
}
- Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(
- externalVibration.callerInfo);
+ Status ignoreStatus = shouldIgnoreVibrationLocked(session.callerInfo);
+ if (ignoreStatus != null) {
+ endVibrationLocked(session, ignoreStatus);
+ return session.getScale();
+ }
- if (vibrationEndInfo == null
- && mCurrentExternalVibration != null
+ if (mCurrentExternalVibration != null
&& mCurrentExternalVibration.isHoldingSameVibration(vib)) {
// We are already playing this external vibration, so we can return the same
// scale calculated in the previous call to this method.
return mCurrentExternalVibration.getScale();
}
- if (vibrationEndInfo == null) {
- // Check if ongoing vibration is more important than this vibration.
- vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(externalVibration);
- }
-
+ // Check if ongoing vibration is more important than this vibration.
+ Vibration.EndInfo vibrationEndInfo =
+ shouldIgnoreVibrationForOngoingLocked(session);
if (vibrationEndInfo != null) {
- endVibrationLocked(externalVibration, vibrationEndInfo);
- return externalVibration.getScale();
+ endVibrationLocked(session, vibrationEndInfo.status,
+ vibrationEndInfo.endedBy);
+ return session.getScale();
}
if (mCurrentExternalVibration == null) {
@@ -2036,15 +1961,12 @@
// vibration that may be playing and ready the vibrator for external
// control.
if (mCurrentVibration != null) {
- externalVibration.stats.reportInterruptedAnotherVibration(
+ session.stats.reportInterruptedAnotherVibration(
mCurrentVibration.getVibration().callerInfo);
- clearNextVibrationLocked(
- new Vibration.EndInfo(Status.IGNORED_FOR_EXTERNAL,
- externalVibration.callerInfo));
- mCurrentVibration.notifyCancelled(
- new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED,
- externalVibration.callerInfo),
- /* immediate= */ true);
+ clearNextVibrationLocked(Status.IGNORED_FOR_EXTERNAL,
+ session.callerInfo);
+ mCurrentVibration.requestEnd(Status.CANCELLED_SUPERSEDED,
+ session.callerInfo, /* immediate= */ true);
waitForCompletion = true;
}
} else {
@@ -2060,10 +1982,10 @@
// as we would need to mute the old one still if it came from a different
// controller.
alreadyUnderExternalControl = true;
- externalVibration.stats.reportInterruptedAnotherVibration(
+ session.stats.reportInterruptedAnotherVibration(
mCurrentExternalVibration.getCallerInfo());
endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED,
- externalVibration.callerInfo, /* continueExternalControl= */ true);
+ session.callerInfo, /* continueExternalControl= */ true);
}
}
// Wait for lock and interact with HAL to set external control outside main lock.
@@ -2071,8 +1993,8 @@
if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) {
Slog.e(TAG, "Timed out waiting for vibration to cancel");
synchronized (mLock) {
- endVibrationLocked(externalVibration, Status.IGNORED_ERROR_CANCELLING);
- return externalVibration.getScale();
+ endVibrationLocked(session, Status.IGNORED_ERROR_CANCELLING);
+ return session.getScale();
}
}
}
@@ -2080,7 +2002,7 @@
if (DEBUG) {
Slog.d(TAG, "Vibrator going under external control.");
}
- setExternalControl(true, externalVibration.stats);
+ setExternalControl(true, session.stats);
}
synchronized (mLock) {
if (DEBUG) {
@@ -2095,14 +2017,14 @@
mVibrationSettings.update();
}
- mCurrentExternalVibration = externalVibration;
- externalVibration.linkToDeath(this::onExternalVibrationBinderDied);
- externalVibration.scale(mVibrationScaler, attrs.getUsage());
+ mCurrentExternalVibration = session;
+ session.linkToDeath(this::onExternalVibrationBinderDied);
+ session.scale(mVibrationScaler, attrs.getUsage());
// Vibrator will start receiving data from external channels after this point.
// Report current time as the vibration start time, for debugging.
- externalVibration.stats.reportStarted();
- return externalVibration.getScale();
+ session.stats.reportStarted();
+ return session.getScale();
}
} finally {
Trace.traceEnd(TRACE_TAG_VIBRATOR);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 2972771..0b36c7e 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -6429,11 +6429,7 @@
// and the token could be null.
return;
}
- final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy
- .getAppCompatCameraPolicy(r);
- if (cameraPolicy != null) {
- cameraPolicy.onActivityRefreshed(r);
- }
+ AppCompatCameraPolicy.onActivityRefreshed(r);
}
static void splashScreenAttachedLocked(IBinder token) {
@@ -9476,11 +9472,7 @@
return;
}
- final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
- this);
- if (cameraPolicy != null) {
- cameraPolicy.onActivityConfigurationChanging(this, newConfig, lastReportedConfig);
- }
+ AppCompatCameraPolicy.onActivityConfigurationChanging(this, newConfig, lastReportedConfig);
}
/** Get process configuration, or global config if the process is not set. */
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index 0e66629..d59046f 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
@@ -255,13 +255,10 @@
mActivityRecord.getOverrideOrientation());
final AppCompatCameraOverrides cameraOverrides =
mActivityRecord.mAppCompatController.getAppCompatCameraOverrides();
- final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
- mActivityRecord);
// Don't resize to split screen size when in book mode if letterbox position is centered
return (isBookMode && isNotCenteredHorizontally || isTabletopMode && isLandscape)
|| cameraOverrides.isCameraCompatSplitScreenAspectRatioAllowed()
- && (cameraPolicy != null
- && cameraPolicy.isTreatmentEnabledForActivity(mActivityRecord));
+ && AppCompatCameraPolicy.isTreatmentEnabledForActivity(mActivityRecord);
}
/**
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
index 3b023fe..548c0a3 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
@@ -74,11 +74,9 @@
@NonNull Rect parentBounds) {
// If in camera compat mode, aspect ratio from the camera compat policy has priority over
// default letterbox aspect ratio.
- final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
- mActivityRecord);
- if (cameraPolicy != null && cameraPolicy.shouldCameraCompatControlAspectRatio(
+ if (AppCompatCameraPolicy.shouldCameraCompatControlAspectRatio(
mActivityRecord)) {
- return cameraPolicy.getCameraCompatAspectRatio(mActivityRecord);
+ return AppCompatCameraPolicy.getCameraCompatAspectRatio(mActivityRecord);
}
final float letterboxAspectRatioOverride =
@@ -128,12 +126,8 @@
if (aspectRatioOverrides.shouldApplyUserMinAspectRatioOverride()) {
return aspectRatioOverrides.getUserMinAspectRatio();
}
- final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
- mActivityRecord);
- final boolean shouldOverrideMinAspectRatioForCamera = cameraPolicy != null
- && cameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord);
if (!aspectRatioOverrides.shouldOverrideMinAspectRatio()
- && !shouldOverrideMinAspectRatioForCamera) {
+ && !AppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord)) {
if (mActivityRecord.isUniversalResizeable()) {
return 0;
}
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index f6090eb..1d00136 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -70,9 +70,10 @@
}
}
- void onActivityRefreshed(@NonNull ActivityRecord activity) {
- if (mActivityRefresher != null) {
- mActivityRefresher.onActivityRefreshed(activity);
+ static void onActivityRefreshed(@NonNull ActivityRecord activity) {
+ final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+ if (cameraPolicy != null && cameraPolicy.mActivityRefresher != null) {
+ cameraPolicy.mActivityRefresher.onActivityRefreshed(activity);
}
}
@@ -88,10 +89,11 @@
* camera preview and can lead to sideways or stretching issues persisting even after force
* rotation.
*/
- void onActivityConfigurationChanging(@NonNull ActivityRecord activity,
+ static void onActivityConfigurationChanging(@NonNull ActivityRecord activity,
@NonNull Configuration newConfig, @NonNull Configuration lastReportedConfig) {
- if (mActivityRefresher != null) {
- mActivityRefresher.onActivityConfigurationChanging(activity, newConfig,
+ final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+ if (cameraPolicy != null && cameraPolicy.mActivityRefresher != null) {
+ cameraPolicy.mActivityRefresher.onActivityConfigurationChanging(activity, newConfig,
lastReportedConfig);
}
}
@@ -108,11 +110,11 @@
}
}
- boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) {
- if (mDisplayRotationCompatPolicy != null) {
- return mDisplayRotationCompatPolicy.isActivityEligibleForOrientationOverride(activity);
- }
- return false;
+ static boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) {
+ final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+ return cameraPolicy != null && cameraPolicy.mDisplayRotationCompatPolicy != null
+ && cameraPolicy.mDisplayRotationCompatPolicy
+ .isActivityEligibleForOrientationOverride(activity);
}
/**
@@ -125,11 +127,11 @@
* <li>The activity has fixed orientation but not "locked" or "nosensor" one.
* </ul>
*/
- boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
- if (mDisplayRotationCompatPolicy != null) {
- return mDisplayRotationCompatPolicy.isTreatmentEnabledForActivity(activity);
- }
- return false;
+ static boolean isTreatmentEnabledForActivity(@NonNull ActivityRecord activity) {
+ final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+ return cameraPolicy != null && cameraPolicy.mDisplayRotationCompatPolicy != null
+ && cameraPolicy.mDisplayRotationCompatPolicy
+ .isTreatmentEnabledForActivity(activity);
}
void start() {
@@ -176,23 +178,31 @@
}
// TODO(b/369070416): have policies implement the same interface.
- boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) {
- return (mDisplayRotationCompatPolicy != null
- && mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(
- activity))
- || (mCameraCompatFreeformPolicy != null
- && mCameraCompatFreeformPolicy.shouldCameraCompatControlOrientation(
- activity));
+ static boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) {
+ final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+ if (cameraPolicy == null) {
+ return false;
+ }
+ return (cameraPolicy.mDisplayRotationCompatPolicy != null
+ && cameraPolicy.mDisplayRotationCompatPolicy
+ .shouldCameraCompatControlOrientation(activity))
+ || (cameraPolicy.mCameraCompatFreeformPolicy != null
+ && cameraPolicy.mCameraCompatFreeformPolicy
+ .shouldCameraCompatControlOrientation(activity));
}
// TODO(b/369070416): have policies implement the same interface.
- boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) {
- return (mDisplayRotationCompatPolicy != null
- && mDisplayRotationCompatPolicy.shouldCameraCompatControlAspectRatio(
- activity))
- || (mCameraCompatFreeformPolicy != null
- && mCameraCompatFreeformPolicy.shouldCameraCompatControlAspectRatio(
- activity));
+ static boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) {
+ final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+ if (cameraPolicy == null) {
+ return false;
+ }
+ return (cameraPolicy.mDisplayRotationCompatPolicy != null
+ && cameraPolicy.mDisplayRotationCompatPolicy
+ .shouldCameraCompatControlAspectRatio(activity))
+ || (cameraPolicy.mCameraCompatFreeformPolicy != null
+ && cameraPolicy.mCameraCompatFreeformPolicy
+ .shouldCameraCompatControlAspectRatio(activity));
}
// TODO(b/369070416): have policies implement the same interface.
@@ -200,29 +210,41 @@
* @return {@code true} if the Camera is active for the provided {@link ActivityRecord} and
* any camera compat treatment could be triggered for the current windowing mode.
*/
- private boolean isCameraRunningAndWindowingModeEligible(@NonNull ActivityRecord activity) {
- return (mDisplayRotationCompatPolicy != null
- && mDisplayRotationCompatPolicy.isCameraRunningAndWindowingModeEligible(activity,
- /* mustBeFullscreen */ true))
- || (mCameraCompatFreeformPolicy != null && mCameraCompatFreeformPolicy
- .isCameraRunningAndWindowingModeEligible(activity));
+ private static boolean isCameraRunningAndWindowingModeEligible(
+ @NonNull ActivityRecord activity) {
+ final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+ if (cameraPolicy == null) {
+ return false;
+ }
+ return (cameraPolicy.mDisplayRotationCompatPolicy != null
+ && cameraPolicy.mDisplayRotationCompatPolicy
+ .isCameraRunningAndWindowingModeEligible(activity,
+ /* mustBeFullscreen */ true))
+ || (cameraPolicy.mCameraCompatFreeformPolicy != null
+ && cameraPolicy.mCameraCompatFreeformPolicy
+ .isCameraRunningAndWindowingModeEligible(activity));
}
@Nullable
String getSummaryForDisplayRotationHistoryRecord() {
- if (mDisplayRotationCompatPolicy != null) {
- return mDisplayRotationCompatPolicy.getSummaryForDisplayRotationHistoryRecord();
- }
- return null;
+ return mDisplayRotationCompatPolicy != null
+ ? mDisplayRotationCompatPolicy.getSummaryForDisplayRotationHistoryRecord()
+ : null;
}
// TODO(b/369070416): have policies implement the same interface.
- float getCameraCompatAspectRatio(@NonNull ActivityRecord activity) {
- float displayRotationCompatPolicyAspectRatio = mDisplayRotationCompatPolicy != null
- ? mDisplayRotationCompatPolicy.getCameraCompatAspectRatio(activity)
+ static float getCameraCompatAspectRatio(@NonNull ActivityRecord activity) {
+ final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+ if (cameraPolicy == null) {
+ return 1.0f;
+ }
+ float displayRotationCompatPolicyAspectRatio =
+ cameraPolicy.mDisplayRotationCompatPolicy != null
+ ? cameraPolicy.mDisplayRotationCompatPolicy.getCameraCompatAspectRatio(activity)
: MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
- float cameraCompatFreeformPolicyAspectRatio = mCameraCompatFreeformPolicy != null
- ? mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(activity)
+ float cameraCompatFreeformPolicyAspectRatio =
+ cameraPolicy.mCameraCompatFreeformPolicy != null
+ ? cameraPolicy.mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(activity)
: MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
return Math.max(displayRotationCompatPolicyAspectRatio,
cameraCompatFreeformPolicyAspectRatio);
@@ -232,8 +254,8 @@
* Whether we should apply the min aspect ratio per-app override only when an app is connected
* to the camera.
*/
- boolean shouldOverrideMinAspectRatioForCamera(@NonNull ActivityRecord activityRecord) {
- return isCameraRunningAndWindowingModeEligible(activityRecord)
+ static boolean shouldOverrideMinAspectRatioForCamera(@NonNull ActivityRecord activityRecord) {
+ return AppCompatCameraPolicy.isCameraRunningAndWindowingModeEligible(activityRecord)
&& activityRecord.mAppCompatController.getAppCompatCameraOverrides()
.isOverrideMinAspectRatioForCameraEnabled();
}
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
index 5bd4aeb..f5d58ea 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
@@ -58,10 +58,8 @@
&& displayContent.getIgnoreOrientationRequest();
final boolean shouldApplyUserFullscreenOverride = mAppCompatOverrides
.getAppCompatAspectRatioOverrides().shouldApplyUserFullscreenOverride();
- final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy
- .getAppCompatCameraPolicy(mActivityRecord);
- final boolean shouldCameraCompatControlOrientation = cameraPolicy != null
- && cameraPolicy.shouldCameraCompatControlOrientation(mActivityRecord);
+ final boolean shouldCameraCompatControlOrientation =
+ AppCompatCameraPolicy.shouldCameraCompatControlOrientation(mActivityRecord);
if (shouldApplyUserFullscreenOverride && isIgnoreOrientationRequestEnabled
// Do not override orientation to fullscreen for camera activities.
// Fixed-orientation activities are rarely tested in other orientations, and it
@@ -98,7 +96,7 @@
if (displayContent != null
&& mAppCompatOverrides.getAppCompatCameraOverrides()
.isOverrideOrientationOnlyForCameraEnabled()
- && !displayContent.mAppCompatCameraPolicy
+ && !AppCompatCameraPolicy
.isActivityEligibleForOrientationOverride(mActivityRecord)) {
return candidate;
}
@@ -213,5 +211,4 @@
}
return false;
}
-
}
diff --git a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
index 3b2f723..c8cb621 100644
--- a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
+++ b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
@@ -194,12 +194,8 @@
return aspectRatioOverrides.getUserMinAspectRatio();
}
- final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
- mActivityRecord);
- final boolean shouldOverrideMinAspectRatioForCamera = cameraPolicy != null
- && cameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord);
if (!aspectRatioOverrides.shouldOverrideMinAspectRatio()
- && !shouldOverrideMinAspectRatioForCamera) {
+ && !AppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord)) {
if (mActivityRecord.isUniversalResizeable()) {
return 0;
}
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index b414a862..61b13a8 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -47,6 +47,7 @@
import android.view.WindowInsetsAnimation;
import android.view.WindowInsetsAnimation.Bounds;
import android.view.WindowManager;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodManager;
@@ -412,6 +413,22 @@
state.addSource(imeSource);
return state;
}
+ } else if (Flags.refactorInsetsController()
+ && (w.mMergedExcludeInsetsTypes & WindowInsets.Type.ime()) != 0) {
+ // In some cases (e.g. split screen from when the IME was requested and the animation
+ // actually starts) the insets should not be send, unless the flag is unset.
+ final InsetsSource originalImeSource = originalState.peekSource(ID_IME);
+ if (originalImeSource != null && originalImeSource.isVisible()) {
+ final InsetsState state = copyState
+ ? new InsetsState(originalState)
+ : originalState;
+ final InsetsSource imeSource = new InsetsSource(originalImeSource);
+ // Setting the height to zero, pretending we're in floating mode
+ imeSource.setFrame(0, 0, 0, 0);
+ imeSource.setVisibleFrame(imeSource.getFrame());
+ state.addSource(imeSource);
+ return state;
+ }
}
return originalState;
}
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index ede587c..5dddf36 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -458,6 +458,12 @@
mDisplayContent.notifyInsetsChanged(mDispatchInsetsChanged);
}
+ void notifyInsetsChanged(ArraySet<WindowState> changedWindows) {
+ for (int i = changedWindows.size() - 1; i >= 0; i--) {
+ mDispatchInsetsChanged.accept(changedWindows.valueAt(i));
+ }
+ }
+
/**
* Checks if the control target has pending controls.
*
diff --git a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java
index 8e32813..2428836 100644
--- a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java
+++ b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java
@@ -156,7 +156,7 @@
Listeners mRegisteredListeners = new Listeners();
- private InputWindowHandle[] mLastWindowHandles;
+ private Pair<InputWindowHandle[], WindowInfosListener.DisplayInfo[]> mLastWindowHandles;
private void startHandlerThreadIfNeeded() {
synchronized (mHandlerThreadLock) {
@@ -222,10 +222,10 @@
@Override
public void onWindowInfosChanged(InputWindowHandle[] windowHandles,
DisplayInfo[] displayInfos) {
- mHandler.post(() -> computeTpl(windowHandles));
+ mHandler.post(() -> computeTpl(new Pair<>(windowHandles, displayInfos)));
}
};
- mLastWindowHandles = mWindowInfosListener.register().first;
+ mLastWindowHandles = mWindowInfosListener.register();
}
private void unregisterWindowInfosListener() {
@@ -238,28 +238,52 @@
mLastWindowHandles = null;
}
- private void computeTpl(InputWindowHandle[] windowHandles) {
+ private void computeTpl(
+ Pair<InputWindowHandle[], WindowInfosListener.DisplayInfo[]> windowHandles) {
mLastWindowHandles = windowHandles;
- if (mLastWindowHandles == null || mLastWindowHandles.length == 0
+ if (mLastWindowHandles == null || mLastWindowHandles.first.length == 0
|| mRegisteredListeners.isEmpty()) {
return;
}
Rect tmpRect = new Rect();
+ RectF tmpRectF = new RectF();
+ Rect tmpLogicalDisplaySize = new Rect();
Matrix tmpInverseMatrix = new Matrix();
float[] tmpMatrix = new float[9];
Region coveredRegionsAbove = new Region();
long currTimeMs = System.currentTimeMillis();
- ProtoLog.v(WM_DEBUG_TPL, "Checking %d windows", mLastWindowHandles.length);
+ ProtoLog.v(WM_DEBUG_TPL, "Checking %d windows", mLastWindowHandles.first.length);
ArrayMap<ITrustedPresentationListener, Pair<IntArray, IntArray>> listenerUpdates =
new ArrayMap<>();
- for (var windowHandle : mLastWindowHandles) {
+ for (var windowHandle : mLastWindowHandles.first) {
if (!windowHandle.canOccludePresentation) {
ProtoLog.v(WM_DEBUG_TPL, "Skipping %s", windowHandle.name);
continue;
}
- tmpRect.set(windowHandle.frame);
+ var displayFound = false;
+ tmpRectF.set(windowHandle.frame);
+ for (var displayHandle : mLastWindowHandles.second) {
+ if (displayHandle.mDisplayId == windowHandle.displayId) {
+ // Transform the window frame into display logical space and then
+ // crop by the logical display size
+ displayHandle.mTransform.mapRect(tmpRectF);
+ tmpRectF.round(tmpRect);
+ tmpLogicalDisplaySize.set(0, 0, displayHandle.mLogicalSize.getWidth(),
+ displayHandle.mLogicalSize.getHeight());
+ tmpRect.intersect(tmpLogicalDisplaySize);
+ displayFound = true;
+ break;
+ }
+ }
+
+ if (!displayFound) {
+ ProtoLog.v(WM_DEBUG_TPL, "Skipping %s, no associated display %d", windowHandle.name,
+ windowHandle.displayId);
+ continue;
+ }
+
var listeners = mRegisteredListeners.get(windowHandle.getWindowToken());
if (listeners != null) {
Region region = new Region();
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index da64a5f..af57c84 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -32,6 +32,7 @@
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.os.UserHandle.USER_NULL;
import static android.view.SurfaceControl.Transaction;
+import static android.view.WindowInsets.Type.InsetsType;
import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR;
@@ -173,6 +174,13 @@
*/
protected SparseArray<InsetsSourceProvider> mInsetsSourceProviders = null;
+ /**
+ * The combined excluded insets types (combined mExcludeInsetsTypes and the
+ * mMergedExcludeInsetsTypes from its parent)
+ */
+ protected @InsetsType int mMergedExcludeInsetsTypes = 0;
+ private @InsetsType int mExcludeInsetsTypes = 0;
+
@Nullable
private ArrayMap<IBinder, DeathRecipient> mInsetsOwnerDeathRecipientMap;
@@ -555,6 +563,49 @@
return mControllableInsetProvider;
}
+ /**
+ * Sets the excludeInsetsTypes of this window and updates the mMergedExcludeInsetsTypes of
+ * all child nodes in the hierarchy.
+ *
+ * @param excludeInsetsTypes the excluded {@link InsetsType} that should be set on this
+ * WindowContainer
+ */
+ void setExcludeInsetsTypes(@InsetsType int excludeInsetsTypes) {
+ if (excludeInsetsTypes == mExcludeInsetsTypes) {
+ return;
+ }
+ mExcludeInsetsTypes = excludeInsetsTypes;
+ mergeExcludeInsetsTypesAndNotifyInsetsChanged(
+ mParent != null ? mParent.mMergedExcludeInsetsTypes : 0);
+ }
+
+ private void mergeExcludeInsetsTypesAndNotifyInsetsChanged(
+ @InsetsType int excludeInsetsTypesFromParent) {
+ final ArraySet<WindowState> changedWindows = new ArraySet<>();
+ updateMergedExcludeInsetsTypes(excludeInsetsTypesFromParent, changedWindows);
+ if (getDisplayContent() != null) {
+ getDisplayContent().getInsetsStateController().notifyInsetsChanged(changedWindows);
+ }
+ }
+
+ private void updateMergedExcludeInsetsTypes(
+ @InsetsType int excludeInsetsTypesFromParent, ArraySet<WindowState> changedWindows) {
+ final int newMergedExcludeInsetsTypes = mExcludeInsetsTypes | excludeInsetsTypesFromParent;
+ if (newMergedExcludeInsetsTypes == mMergedExcludeInsetsTypes) {
+ return;
+ }
+ mMergedExcludeInsetsTypes = newMergedExcludeInsetsTypes;
+
+ final WindowState win = asWindowState();
+ if (win != null) {
+ changedWindows.add(win);
+ }
+ // Apply to all children
+ for (int i = mChildren.size() - 1; i >= 0; i--) {
+ final WindowContainer<?> child = mChildren.get(i);
+ child.updateMergedExcludeInsetsTypes(mMergedExcludeInsetsTypes, changedWindows);
+ }
+ }
@Override
final protected WindowContainer getParent() {
@@ -653,6 +704,7 @@
void onParentChanged(ConfigurationContainer newParent, ConfigurationContainer oldParent) {
super.onParentChanged(newParent, oldParent);
if (mParent == null) {
+ mergeExcludeInsetsTypesAndNotifyInsetsChanged(0);
return;
}
@@ -667,6 +719,7 @@
// new parent.
reparentSurfaceControl(getSyncTransaction(), mParent.mSurfaceControl);
}
+ mergeExcludeInsetsTypesAndNotifyInsetsChanged(mParent.mMergedExcludeInsetsTypes);
// Either way we need to ask the parent to assign us a Z-order.
mParent.assignChildLayers();
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index e1e4714..ebf645d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -6810,30 +6810,36 @@
* @param logLevel Determines the amount of data to be written to the Protobuf.
*/
void dumpDebugLocked(ProtoOutputStream proto, @WindowTracingLogLevel int logLevel) {
- mPolicy.dumpDebug(proto, POLICY);
- mRoot.dumpDebug(proto, ROOT_WINDOW_CONTAINER, logLevel);
- final DisplayContent topFocusedDisplayContent = mRoot.getTopFocusedDisplayContent();
- if (topFocusedDisplayContent.mCurrentFocus != null) {
- topFocusedDisplayContent.mCurrentFocus.writeIdentifierToProto(proto, FOCUSED_WINDOW);
- }
- if (topFocusedDisplayContent.mFocusedApp != null) {
- topFocusedDisplayContent.mFocusedApp.writeNameToProto(proto, FOCUSED_APP);
- }
- final WindowState imeWindow = mRoot.getCurrentInputMethodWindow();
- if (imeWindow != null) {
- imeWindow.writeIdentifierToProto(proto, INPUT_METHOD_WINDOW);
- }
- proto.write(DISPLAY_FROZEN, mDisplayFrozen);
- proto.write(FOCUSED_DISPLAY_ID, topFocusedDisplayContent.getDisplayId());
- proto.write(HARD_KEYBOARD_AVAILABLE, mHardKeyboardAvailable);
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "dumpDebugLocked");
+ try {
+ mPolicy.dumpDebug(proto, POLICY);
+ mRoot.dumpDebug(proto, ROOT_WINDOW_CONTAINER, logLevel);
+ final DisplayContent topFocusedDisplayContent = mRoot.getTopFocusedDisplayContent();
+ if (topFocusedDisplayContent.mCurrentFocus != null) {
+ topFocusedDisplayContent.mCurrentFocus
+ .writeIdentifierToProto(proto, FOCUSED_WINDOW);
+ }
+ if (topFocusedDisplayContent.mFocusedApp != null) {
+ topFocusedDisplayContent.mFocusedApp.writeNameToProto(proto, FOCUSED_APP);
+ }
+ final WindowState imeWindow = mRoot.getCurrentInputMethodWindow();
+ if (imeWindow != null) {
+ imeWindow.writeIdentifierToProto(proto, INPUT_METHOD_WINDOW);
+ }
+ proto.write(DISPLAY_FROZEN, mDisplayFrozen);
+ proto.write(FOCUSED_DISPLAY_ID, topFocusedDisplayContent.getDisplayId());
+ proto.write(HARD_KEYBOARD_AVAILABLE, mHardKeyboardAvailable);
- // This is always true for now since we still update the window frames at the server side.
- // Once we move the window layout to the client side, this can be false when we are waiting
- // for the frames.
- proto.write(WINDOW_FRAMES_VALID, true);
+ // This is always true for now since we still update the window frames at the server
+ // side. Once we move the window layout to the client side, this can be false when we
+ // are waiting for the frames.
+ proto.write(WINDOW_FRAMES_VALID, true);
- // Write the BackNavigationController's state into the protocol buffer
- mAtmService.mBackNavigationController.dumpDebug(proto, BACK_NAVIGATION);
+ // Write the BackNavigationController's state into the protocol buffer
+ mAtmService.mBackNavigationController.dumpDebug(proto, BACK_NAVIGATION);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ }
}
private void dumpWindowsLocked(PrintWriter pw, boolean dumpAll,
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 09a2bf9..f8d0bc2 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -67,6 +67,7 @@
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_IS_TRIMMABLE;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT;
@@ -1449,6 +1450,16 @@
}
break;
}
+ case HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES: {
+ final WindowContainer container = WindowContainer.fromBinder(hop.getContainer());
+ if (container == null) {
+ Slog.e(TAG, "Attempt to operate on unknown or detached container: "
+ + container);
+ break;
+ }
+ container.setExcludeInsetsTypes(hop.getExcludeInsetsTypes());
+ break;
+ }
}
return effects;
}
diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java
index fe26726..6883437 100644
--- a/services/core/java/com/android/server/wm/WindowTracing.java
+++ b/services/core/java/com/android/server/wm/WindowTracing.java
@@ -167,12 +167,7 @@
long token = os.start(WINDOW_MANAGER_SERVICE);
synchronized (mGlobalLock) {
- Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "dumpDebugLocked");
- try {
- mService.dumpDebugLocked(os, logLevel);
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
- }
+ mService.dumpDebugLocked(os, logLevel);
}
os.end(token);
} catch (Exception e) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 4e89b85..2be999f 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -8031,8 +8031,7 @@
"DevicePolicyManager.wipeDataWithReason() from %s, organization-owned? %s",
adminName, calledByProfileOwnerOnOrgOwnedDevice);
- wipeDataNoLock(adminComp, flags, internalReason, wipeReasonForUser, userId,
- calledOnParentInstance, factoryReset);
+ wipeDataNoLock(adminComp, flags, internalReason, wipeReasonForUser, userId, factoryReset);
}
private String getGenericWipeReason(
@@ -8188,17 +8187,16 @@
* factory reset
*/
private void wipeDataNoLock(@Nullable ComponentName admin, int flags, String internalReason,
- String wipeReasonForUser, int userId, boolean calledOnParentInstance,
- @Nullable Boolean factoryReset) {
+ String wipeReasonForUser, int userId, @Nullable Boolean factoryReset) {
wtfIfInLock();
final String adminPackage;
if (admin != null) {
adminPackage = admin.getPackageName();
} else {
- int callerId = mInjector.binderGetCallingUid();
- String[] adminPackages = mInjector.getPackageManager().getPackagesForUid(callerId);
+ int callerUid = mInjector.binderGetCallingUid();
+ String[] adminPackages = mInjector.getPackageManager().getPackagesForUid(callerUid);
Preconditions.checkState(adminPackages.length > 0,
- "Caller %s does not have any associated packages", callerId);
+ "Caller %s does not have any associated packages", callerUid);
adminPackage = adminPackages[0];
}
mInjector.binderWithCleanCallingIdentity(() -> {
@@ -8220,32 +8218,22 @@
throw new SecurityException("Cannot wipe data. " + restriction
+ " restriction is set for user " + userId);
}
- });
- boolean isSystemUser = userId == UserHandle.USER_SYSTEM;
- boolean isMainUser = userId == getMainUserId();
- boolean wipeDevice;
- if (factoryReset == null || !mInjector.isChangeEnabled(EXPLICIT_WIPE_BEHAVIOUR,
- adminPackage,
- userId)) {
- // Legacy mode
- wipeDevice = getHeadlessDeviceOwnerModeForDeviceOwner()
- == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER ? isMainUser : isSystemUser;
- } else {
- // Explicit behaviour
- if (factoryReset) {
- EnforcingAdmin enforcingAdmin = enforcePermissionsAndGetEnforcingAdmin(
- /*admin=*/ null,
- /*permission=*/ new String[]{MANAGE_DEVICE_POLICY_WIPE_DATA,
- MASTER_CLEAR},
- USES_POLICY_WIPE_DATA,
- adminPackage,
- factoryReset ? UserHandle.USER_ALL :
- getAffectedUser(calledOnParentInstance));
- wipeDevice = true;
+ boolean isSystemUser = userId == UserHandle.USER_SYSTEM;
+ boolean isMainUser = userId == getMainUserId();
+ boolean wipeDevice;
+ if (factoryReset == null || !mInjector.isChangeEnabled(EXPLICIT_WIPE_BEHAVIOUR,
+ adminPackage,
+ userId)) {
+ // Legacy mode
+ wipeDevice = getHeadlessDeviceOwnerModeForDeviceOwner()
+ == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER ? isMainUser : isSystemUser;
} else {
- mInjector.binderWithCleanCallingIdentity(() -> {
- Preconditions.checkCallAuthorization(!isSystemUser,
+ // Explicit behaviour
+ if (factoryReset) {
+ wipeDevice = true;
+ } else {
+ Preconditions.checkState(!isSystemUser,
"User %s is a system user and cannot be removed", userId);
boolean isLastNonHeadlessUser = getUserInfo(userId).isFull()
&& mUserManager.getAliveUsers().stream()
@@ -8253,13 +8241,11 @@
.noneMatch(UserInfo::isFull);
Preconditions.checkState(!isLastNonHeadlessUser,
"Removing user %s would leave the device without any active users. "
- + "Consider factory resetting the device instead.",
- userId);
- });
- wipeDevice = false;
+ + "Consider factory resetting the device instead.", userId);
+ wipeDevice = false;
+ }
}
- }
- mInjector.binderWithCleanCallingIdentity(() -> {
+
if (wipeDevice) {
forceWipeDeviceNoLock(
(flags & WIPE_EXTERNAL_STORAGE) != 0,
@@ -8600,7 +8586,6 @@
/* reason= */ "reportFailedPasswordAttempt()",
getFailedPasswordAttemptWipeMessage(),
userId,
- /* calledOnParentInstance= */ parent,
// factoryReset=null to enable U- behaviour
/* factoryReset= */ null);
} catch (SecurityException e) {
diff --git a/services/tests/appfunctions/Android.bp b/services/tests/appfunctions/Android.bp
index 9560ec9..c841643 100644
--- a/services/tests/appfunctions/Android.bp
+++ b/services/tests/appfunctions/Android.bp
@@ -36,6 +36,7 @@
"androidx.test.core",
"androidx.test.runner",
"androidx.test.ext.truth",
+ "kotlin-test",
"platform-test-annotations",
"services.appfunctions",
"servicestests-core-utils",
diff --git a/services/tests/appfunctions/src/android/app/appfunctions/AppFunctionRuntimeMetadataTest.kt b/services/tests/appfunctions/src/android/app/appfunctions/AppFunctionRuntimeMetadataTest.kt
index da3e94f..d326204 100644
--- a/services/tests/appfunctions/src/android/app/appfunctions/AppFunctionRuntimeMetadataTest.kt
+++ b/services/tests/appfunctions/src/android/app/appfunctions/AppFunctionRuntimeMetadataTest.kt
@@ -15,8 +15,12 @@
*/
package android.app.appfunctions
+import android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DEFAULT
+import android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DISABLED
+import android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED
import android.app.appsearch.AppSearchSchema
import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -108,32 +112,43 @@
assertThat(runtimeMetadata.packageName).isEqualTo("com.pkg")
assertThat(runtimeMetadata.functionId).isEqualTo("funcId")
- assertThat(runtimeMetadata.enabled).isNull()
+ assertThat(runtimeMetadata.enabled).isEqualTo(APP_FUNCTION_STATE_DEFAULT)
assertThat(runtimeMetadata.appFunctionStaticMetadataQualifiedId)
.isEqualTo("android\$apps-db/app_functions#com.pkg/funcId")
}
@Test
- fun setEnabled_true() {
+ fun setEnabled_enabled() {
val runtimeMetadata =
- AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId").setEnabled(true).build()
+ AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId").setEnabled(APP_FUNCTION_STATE_ENABLED).build()
- assertThat(runtimeMetadata.enabled).isTrue()
+ assertThat(runtimeMetadata.enabled).isEqualTo(APP_FUNCTION_STATE_ENABLED)
}
@Test
- fun setEnabled_false() {
+ fun setEnabled_disabled() {
val runtimeMetadata =
- AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId").setEnabled(false).build()
+ AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId").setEnabled(
+ APP_FUNCTION_STATE_DISABLED).build()
- assertThat(runtimeMetadata.enabled).isFalse()
+ assertThat(runtimeMetadata.enabled).isEqualTo(APP_FUNCTION_STATE_DISABLED)
}
@Test
- fun setEnabled_null() {
+ fun setEnabled_default() {
val runtimeMetadata =
- AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId").setEnabled(null).build()
+ AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId").setEnabled(
+ APP_FUNCTION_STATE_DEFAULT).build()
- assertThat(runtimeMetadata.enabled).isNull()
+ assertThat(runtimeMetadata.enabled).isEqualTo(APP_FUNCTION_STATE_DEFAULT)
+ }
+
+ @Test
+ fun setEnabled_illegalArgument() {
+ val runtimeMetadataBuilder =
+ AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId")
+ assertFailsWith<IllegalArgumentException>("Value of EnabledState is unsupported.") {
+ runtimeMetadataBuilder.setEnabled(-1)
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
index 50f3a88..5bcddc4 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
@@ -1,6 +1,10 @@
package com.android.server.locksettings;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
import android.platform.test.annotations.Presubmit;
@@ -56,4 +60,44 @@
mService.initializeSyntheticPassword(userId); // This should allocate a Weaver slot.
assertEquals(Sets.newHashSet(0), mPasswordSlotManager.getUsedSlots());
}
+
+ private int getNumUsedWeaverSlots() {
+ return mPasswordSlotManager.getUsedSlots().size();
+ }
+
+ @Test
+ public void testDisableWeaverOnUnsecuredUsers_false() {
+ final int userId = PRIMARY_USER_ID;
+ when(mResources.getBoolean(eq(
+ com.android.internal.R.bool.config_disableWeaverOnUnsecuredUsers)))
+ .thenReturn(false);
+ assertEquals(0, getNumUsedWeaverSlots());
+ mService.initializeSyntheticPassword(userId);
+ assertEquals(1, getNumUsedWeaverSlots());
+ assertTrue(mService.setLockCredential(newPassword("password"), nonePassword(), userId));
+ assertEquals(1, getNumUsedWeaverSlots());
+ assertTrue(mService.setLockCredential(nonePassword(), newPassword("password"), userId));
+ assertEquals(1, getNumUsedWeaverSlots());
+ }
+
+ @Test
+ public void testDisableWeaverOnUnsecuredUsers_true() {
+ final int userId = PRIMARY_USER_ID;
+ when(mResources.getBoolean(eq(
+ com.android.internal.R.bool.config_disableWeaverOnUnsecuredUsers)))
+ .thenReturn(true);
+ assertEquals(0, getNumUsedWeaverSlots());
+ mService.initializeSyntheticPassword(userId);
+ assertEquals(0, getNumUsedWeaverSlots());
+ assertTrue(mService.setLockCredential(newPassword("password"), nonePassword(), userId));
+ assertEquals(1, getNumUsedWeaverSlots());
+ assertTrue(mService.setLockCredential(nonePassword(), newPassword("password"), userId));
+ assertEquals(0, getNumUsedWeaverSlots());
+ }
+
+ @Test
+ public void testDisableWeaverOnUnsecuredUsers_defaultsToFalse() {
+ assertFalse(mResources.getBoolean(
+ com.android.internal.R.bool.config_disableWeaverOnUnsecuredUsers));
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java
index 18ca09b..bf0586c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java
@@ -18,11 +18,21 @@
import static android.app.NotificationManager.IMPORTANCE_LOW;
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION;
+import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING;
+
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.PendingIntent;
@@ -30,12 +40,16 @@
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.Adjustment;
import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
import com.android.server.UiServiceTestCase;
+import org.junit.Rule;
import org.junit.Test;
import java.util.ArrayList;
@@ -43,6 +57,9 @@
public class NotificationAdjustmentExtractorTest extends UiServiceTestCase {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+
@Test
public void testExtractsAdjustment() {
NotificationAdjustmentExtractor extractor = new NotificationAdjustmentExtractor();
@@ -111,6 +128,44 @@
assertEquals(snoozeCriteria, r.getSnoozeCriteria());
}
+ @Test
+ @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_FORCE_GROUPING})
+ public void testClassificationAdjustments_triggerRegrouping() {
+ GroupHelper groupHelper = mock(GroupHelper.class);
+ NotificationAdjustmentExtractor extractor = new NotificationAdjustmentExtractor();
+ extractor.setGroupHelper(groupHelper);
+
+ NotificationRecord r = generateRecord();
+
+ Bundle classificationAdj = new Bundle();
+ classificationAdj.putParcelable(Adjustment.KEY_TYPE, mock(NotificationChannel.class));
+ Adjustment adjustment = new Adjustment("pkg", r.getKey(), classificationAdj, "", 0);
+ r.addAdjustment(adjustment);
+
+ RankingReconsideration regroupingTask = extractor.process(r);
+ assertThat(regroupingTask).isNotNull();
+ regroupingTask.applyChangesLocked(r);
+ verify(groupHelper, times(1)).onChannelUpdated(r);
+ }
+
+ @Test
+ @DisableFlags({FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_FORCE_GROUPING})
+ public void testClassificationAdjustments_notTriggerRegrouping_flagsDisabled() {
+ GroupHelper groupHelper = mock(GroupHelper.class);
+ NotificationAdjustmentExtractor extractor = new NotificationAdjustmentExtractor();
+ extractor.setGroupHelper(groupHelper);
+
+ NotificationRecord r = generateRecord();
+
+ Bundle classificationAdj = new Bundle();
+ classificationAdj.putParcelable(Adjustment.KEY_TYPE, mock(NotificationChannel.class));
+ Adjustment adjustment = new Adjustment("pkg", r.getKey(), classificationAdj, "", 0);
+ r.addAdjustment(adjustment);
+
+ RankingReconsideration regroupingTask = extractor.process(r);
+ assertThat(regroupingTask).isNull();
+ }
+
private NotificationRecord generateRecord() {
NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
final Notification.Builder builder = new Notification.Builder(getContext())
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index 45cd571..03cad24 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -2637,6 +2637,63 @@
}
@Test
+ public void testBeepVolume_politeNotif_AvalancheStrategy_exempt_msgCategory() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+ mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+ mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE);
+ TestableFlagResolver flagResolver = new TestableFlagResolver();
+ flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+ flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+ initAttentionHelper(flagResolver);
+
+ // Trigger avalanche trigger intent
+ final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ intent.putExtra("state", false);
+ mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+
+ // Create a conversation group with GROUP_ALERT_SUMMARY behavior
+ // Where the summary is not MessagingStyle
+ final String groupKey = "grup_name";
+ final String shortcutId = "shortcut";
+ NotificationRecord summary = getBeepyNotificationRecord(groupKey, GROUP_ALERT_SUMMARY);
+ summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
+ summary.getNotification().category = Notification.CATEGORY_MESSAGE;
+ ShortcutInfo.Builder sb = new ShortcutInfo.Builder(getContext());
+ summary.setShortcutInfo(sb.setId(shortcutId).build());
+
+ // Should beep at 100% volume
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+ verifyBeepVolume(1.0f);
+ assertNotEquals(-1, summary.getLastAudiblyAlertedMs());
+ Mockito.reset(mRingtonePlayer);
+
+ // Post child notifications with GROUP_ALERT_SUMMARY
+ NotificationRecord child = getConversationNotificationRecord(mId, false /* insistent */,
+ false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true,
+ true, false, groupKey, Notification.GROUP_ALERT_SUMMARY, false, mUser, mPkg,
+ shortcutId);
+
+ // Should not beep
+ mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+ verifyNeverBeep();
+ assertFalse(child.isInterruptive());
+ assertEquals(-1, child.getLastAudiblyAlertedMs());
+ Mockito.reset(mRingtonePlayer);
+
+ // 2nd update for summary should beep at 50% volume
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+ verifyBeepVolume(0.5f);
+ assertNotEquals(-1, summary.getLastAudiblyAlertedMs());
+ Mockito.reset(mRingtonePlayer);
+
+ // 3rd update for summary should not beep
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+ verifyNeverBeep();
+ assertFalse(summary.isInterruptive());
+ assertEquals(-1, summary.getLastAudiblyAlertedMs());
+ }
+
+ @Test
public void testBeepVolume_politeNotif_exemptEmergency() throws Exception {
mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 3c120e1..1349ee0 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -3206,7 +3206,6 @@
// Send two cancelations.
mBinderService.cancelNotificationWithTag(mPkg, mPkg, sbn.getTag(), sbn.getId(),
sbn.getUserId());
- waitForIdle();
mBinderService.cancelNotificationWithTag(mPkg, mPkg, sbn.getTag(), sbn.getId(),
sbn.getUserId());
waitForIdle();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
index 9a6e818..5d4382a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
@@ -92,6 +92,7 @@
@Mock ZenModeHelper mMockZenModeHelper;
@Mock RankingConfig mConfig;
@Mock Vibrator mVibrator;
+ @Mock GroupHelper mGroupHelper;
private NotificationManager.Policy mTestNotificationPolicy;
private Notification mNotiGroupGSortA;
@@ -157,7 +158,7 @@
when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy);
mHelper = new RankingHelper(getContext(), mHandler, mConfig, mMockZenModeHelper,
mUsageStats, new String[] {ImportanceExtractor.class.getName()},
- mock(IPlatformCompat.class));
+ mock(IPlatformCompat.class), mGroupHelper);
mNotiGroupGSortA = new Notification.Builder(mContext, TEST_CHANNEL_ID)
.setContentTitle("A")
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index e83a4b2..7536f5f 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -47,7 +47,6 @@
import android.hardware.vibrator.IVibratorManager;
import android.os.CombinedVibration;
import android.os.Handler;
-import android.os.IBinder;
import android.os.PersistableBundle;
import android.os.PowerManager;
import android.os.Process;
@@ -119,7 +118,6 @@
@Mock private PackageManagerInternal mPackageManagerInternalMock;
@Mock private VibrationThread.VibratorManagerHooks mManagerHooks;
@Mock private VibratorController.OnVibrationCompleteListener mControllerCallbacks;
- @Mock private IBinder mVibrationToken;
@Mock private VibrationConfig mVibrationConfigMock;
@Mock private VibratorFrameworkStatsLogger mStatsLoggerMock;
@@ -668,7 +666,7 @@
VibrationEffect fallback = VibrationEffect.createOneShot(10, 100);
HalVibration vibration = createVibration(CombinedVibration.createParallel(
VibrationEffect.get(VibrationEffect.EFFECT_CLICK)));
- vibration.addFallback(VibrationEffect.EFFECT_CLICK, fallback);
+ vibration.fillFallbacks(unused -> fallback);
startThreadAndDispatcher(vibration);
waitForCompletion();
@@ -848,7 +846,7 @@
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
.compose();
HalVibration vibration = createVibration(CombinedVibration.createParallel(effect));
- vibration.addFallback(VibrationEffect.EFFECT_TICK, fallback);
+ vibration.fillFallbacks(unused -> fallback);
startThreadAndDispatcher(vibration);
waitForCompletion();
@@ -954,7 +952,8 @@
assertTrue(mThread.isRunningVibrationId(vibration.id));
assertTrue(mControllers.get(VIBRATOR_ID).isVibrating());
- mVibrationConductor.binderDied();
+ mVibrationConductor.notifyCancelled(
+ new Vibration.EndInfo(Status.CANCELLED_BINDER_DIED), /* immediate= */ false);
waitForCompletion();
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
@@ -1575,7 +1574,8 @@
TEST_TIMEOUT_MILLIS));
assertTrue(mThread.isRunningVibrationId(vibration.id));
- mVibrationConductor.binderDied();
+ mVibrationConductor.notifyCancelled(
+ new Vibration.EndInfo(Status.CANCELLED_BINDER_DIED), /* immediate= */ false);
waitForCompletion();
verifyCallbacksTriggered(vibration, Status.CANCELLED_BINDER_DIED);
@@ -1865,9 +1865,9 @@
VibrationAttributes attrs = new VibrationAttributes.Builder()
.setUsage(usage)
.build();
- HalVibration vib = new HalVibration(mVibrationToken,
- CombinedVibration.createParallel(effect),
- new CallerInfo(attrs, UID, DEVICE_ID, PACKAGE_NAME, "reason"));
+ HalVibration vib = new HalVibration(
+ new CallerInfo(attrs, UID, DEVICE_ID, PACKAGE_NAME, "reason"),
+ CombinedVibration.createParallel(effect));
return startThreadAndDispatcher(vib, requestVibrationParamsFuture);
}
@@ -1903,8 +1903,8 @@
}
private HalVibration createVibration(CombinedVibration effect) {
- return new HalVibration(mVibrationToken, effect,
- new CallerInfo(ATTRS, UID, DEVICE_ID, PACKAGE_NAME, "reason"));
+ return new HalVibration(new CallerInfo(ATTRS, UID, DEVICE_ID, PACKAGE_NAME, "reason"),
+ effect);
}
private SparseArray<VibratorController> createVibratorControllers() {
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorFrameworkStatsLoggerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorFrameworkStatsLoggerTest.java
index cd057b6..1d1b4e2 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorFrameworkStatsLoggerTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorFrameworkStatsLoggerTest.java
@@ -115,6 +115,6 @@
}
private static VibrationStats.StatsInfo newEmptyStatsInfo() {
- return new VibrationStats.StatsInfo(0, 0, 0, Status.FINISHED, new VibrationStats(), 0L);
+ return new VibrationStats.StatsInfo(0, 0, 0, Status.FINISHED, new VibrationStats());
}
}
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index 538c3fc..b782162 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -1791,28 +1791,6 @@
}
@Test
- public void performHapticFeedback_usesServiceAsToken() throws Exception {
- VibratorManagerService service = createSystemReadyService();
-
- HalVibration vibration =
- performHapticFeedbackAndWaitUntilFinished(
- service, HapticFeedbackConstants.SCROLL_TICK, /* always= */ true);
-
- assertTrue(vibration.callerToken == service);
- }
-
- @Test
- public void performHapticFeedbackForInputDevice_usesServiceAsToken() throws Exception {
- VibratorManagerService service = createSystemReadyService();
-
- HalVibration vibration = performHapticFeedbackForInputDeviceAndWaitUntilFinished(
- service, HapticFeedbackConstants.SCROLL_TICK, /* inputDeviceId= */ 0,
- InputDevice.SOURCE_ROTARY_ENCODER, /* always= */ true);
-
- assertTrue(vibration.callerToken == service);
- }
-
- @Test
@RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
public void vibrate_vendorEffectsWithoutPermission_doesNotVibrate() throws Exception {
// Deny permission to vibrate with vendor effects
@@ -2147,6 +2125,27 @@
}
@Test
+ public void cancelVibrate_externalVibration_cancelWithDifferentToken() {
+ mockVibrators(1);
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+ createSystemReadyService();
+
+ IBinder vibrationBinderToken = mock(IBinder.class);
+ ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
+ AUDIO_ALARM_ATTRS,
+ mock(IExternalVibrationController.class), vibrationBinderToken);
+ ExternalVibrationScale scale = mExternalVibratorService.onExternalVibrationStart(
+ externalVibration);
+
+ IBinder cancelBinderToken = mock(IBinder.class);
+ mService.cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, cancelBinderToken);
+
+ assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
+ assertEquals(Arrays.asList(false, true, false),
+ mVibratorProviders.get(1).getExternalControlStates());
+ }
+
+ @Test
public void onExternalVibration_ignoreVibrationFromVirtualDevices() {
mockVibrators(1);
mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 3dc893a..a85f866 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -700,7 +700,7 @@
void assertPowerWakeUp() {
mTestLooper.dispatchAll();
verify(mWindowWakeUpPolicy)
- .wakeUpFromKey(anyInt(), anyLong(), eq(KeyEvent.KEYCODE_POWER), anyBoolean());
+ .wakeUpFromKey(anyLong(), eq(KeyEvent.KEYCODE_POWER), anyBoolean());
}
void assertNoPowerSleep() {
diff --git a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
index 4119ad3..7322e5a 100644
--- a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
@@ -35,7 +35,6 @@
import static com.android.internal.R.bool.config_allowTheaterModeWakeFromLidSwitch;
import static com.android.internal.R.bool.config_allowTheaterModeWakeFromGesture;
import static com.android.server.policy.Flags.FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE;
-import static com.android.server.power.feature.flags.Flags.FLAG_PER_DISPLAY_WAKE_BY_TOUCH;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -44,7 +43,6 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -54,8 +52,6 @@
import android.content.ContextWrapper;
import android.content.res.Resources;
import android.os.PowerManager;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.view.Display;
@@ -129,7 +125,6 @@
}
@Test
- @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
public void testMotionWakeUpDelegation_wakePowerManagerIfDelegateDoesNotHandleWake() {
setTheaterModeEnabled(false);
mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
@@ -141,8 +136,7 @@
// Verify the policy wake up call succeeds because of the call on the delegate, and not
// because of a PowerManager wake up.
- assertThat(mPolicy.wakeUpFromMotion(
- mDefaultDisplay.getDisplayId(), 200, SOURCE_TOUCHSCREEN, true)).isTrue();
+ assertThat(mPolicy.wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true)).isTrue();
verify(mInputWakeUpDelegate).wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true);
verifyNoPowerManagerWakeUp();
@@ -150,14 +144,12 @@
// Verify the policy wake up call succeeds because of the PowerManager wake up, since the
// delegate would not handle the wake up request.
- assertThat(mPolicy.wakeUpFromMotion(
- mDefaultDisplay.getDisplayId(), 300, SOURCE_ROTARY_ENCODER, false)).isTrue();
+ assertThat(mPolicy.wakeUpFromMotion(300, SOURCE_ROTARY_ENCODER, false)).isTrue();
verify(mInputWakeUpDelegate).wakeUpFromMotion(300, SOURCE_ROTARY_ENCODER, false);
verify(mPowerManager).wakeUp(300, WAKE_REASON_WAKE_MOTION, "android.policy:MOTION");
}
@Test
- @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
public void testKeyWakeUpDelegation_wakePowerManagerIfDelegateDoesNotHandleWake() {
setTheaterModeEnabled(false);
mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
@@ -169,8 +161,7 @@
// Verify the policy wake up call succeeds because of the call on the delegate, and not
// because of a PowerManager wake up.
- assertThat(mPolicy.wakeUpFromKey(
- mDefaultDisplay.getDisplayId(), 200, KEYCODE_POWER, true)).isTrue();
+ assertThat(mPolicy.wakeUpFromKey(200, KEYCODE_POWER, true)).isTrue();
verify(mInputWakeUpDelegate).wakeUpFromKey(200, KEYCODE_POWER, true);
verifyNoPowerManagerWakeUp();
@@ -178,8 +169,7 @@
// Verify the policy wake up call succeeds because of the PowerManager wake up, since the
// delegate would not handle the wake up request.
- assertThat(mPolicy.wakeUpFromKey(
- mDefaultDisplay.getDisplayId(), 300, KEYCODE_STEM_PRIMARY, false)).isTrue();
+ assertThat(mPolicy.wakeUpFromKey(300, KEYCODE_STEM_PRIMARY, false)).isTrue();
verify(mInputWakeUpDelegate).wakeUpFromKey(300, KEYCODE_STEM_PRIMARY, false);
verify(mPowerManager).wakeUp(300, WAKE_REASON_WAKE_KEY, "android.policy:KEY");
}
@@ -196,8 +186,7 @@
.setInputWakeUpDelegate(mInputWakeUpDelegate);
// Check that the wake up does not happen because the theater mode policy check fails.
- assertThat(mPolicy.wakeUpFromKey(
- mDefaultDisplay.getDisplayId(), 200, KEYCODE_POWER, true)).isFalse();
+ assertThat(mPolicy.wakeUpFromKey(200, KEYCODE_POWER, true)).isFalse();
verify(mInputWakeUpDelegate, never()).wakeUpFromKey(anyLong(), anyInt(), anyBoolean());
}
@@ -212,13 +201,11 @@
.setInputWakeUpDelegate(mInputWakeUpDelegate);
// Check that the wake up does not happen because the theater mode policy check fails.
- assertThat(mPolicy.wakeUpFromMotion(
- mDefaultDisplay.getDisplayId(), 200, SOURCE_TOUCHSCREEN, true)).isFalse();
+ assertThat(mPolicy.wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true)).isFalse();
verify(mInputWakeUpDelegate, never()).wakeUpFromMotion(anyLong(), anyInt(), anyBoolean());
}
@Test
- @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
public void testTheaterModeChecksNotAppliedWhenScreenIsOn() {
mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
setDefaultDisplayState(Display.STATE_ON);
@@ -226,69 +213,30 @@
setBooleanRes(config_allowTheaterModeWakeFromMotion, false);
mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
- mPolicy.wakeUpFromMotion(mDefaultDisplay.getDisplayId(), 200L, SOURCE_TOUCHSCREEN, true);
+ mPolicy.wakeUpFromMotion(200L, SOURCE_TOUCHSCREEN, true);
verify(mPowerManager).wakeUp(200L, WAKE_REASON_WAKE_MOTION, "android.policy:MOTION");
}
@Test
- @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
public void testWakeUpFromMotion() {
runPowerManagerUpChecks(
- () -> mPolicy.wakeUpFromMotion(mDefaultDisplay.getDisplayId(),
- mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, true),
+ () -> mPolicy.wakeUpFromMotion(mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, true),
config_allowTheaterModeWakeFromMotion,
WAKE_REASON_WAKE_MOTION,
"android.policy:MOTION");
}
@Test
- @EnableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
- public void testWakeUpFromMotion_perDisplayWakeByTouchEnabled() {
- setTheaterModeEnabled(false);
- final int displayId = 555;
- mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
-
- boolean displayWokeUp = mPolicy.wakeUpFromMotion(
- displayId, mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, /* isDown= */ true);
-
- // Verify that display is woken up
- assertThat(displayWokeUp).isTrue();
- verify(mPowerManager).wakeUp(anyLong(), eq(WAKE_REASON_WAKE_MOTION),
- eq("android.policy:MOTION"), eq(displayId));
- }
-
- @Test
- @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
- public void testWakeUpFromMotion_perDisplayWakeByTouchDisabled() {
- setTheaterModeEnabled(false);
- final int displayId = 555;
- mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
-
- boolean displayWokeUp = mPolicy.wakeUpFromMotion(
- displayId, mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, /* isDown= */ true);
-
- // Verify that power is woken up and display isn't woken up individually
- assertThat(displayWokeUp).isTrue();
- verify(mPowerManager).wakeUp(
- anyLong(), eq(WAKE_REASON_WAKE_MOTION), eq("android.policy:MOTION"));
- verify(mPowerManager, never()).wakeUp(anyLong(), eq(WAKE_REASON_WAKE_MOTION),
- eq("android.policy:MOTION"), eq(displayId));
- }
-
- @Test
- @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
public void testWakeUpFromKey_nonPowerKey() {
runPowerManagerUpChecks(
- () -> mPolicy.wakeUpFromKey(
- mDefaultDisplay.getDisplayId(), mClock.uptimeMillis(), KEYCODE_HOME, true),
+ () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_HOME, true),
config_allowTheaterModeWakeFromKey,
WAKE_REASON_WAKE_KEY,
"android.policy:KEY");
}
@Test
- @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
public void testWakeUpFromKey_powerKey() {
// Disable the resource affecting all wake keys because it affects power key as well.
// That way, power key wake during theater mode will solely be controlled by
@@ -297,8 +245,7 @@
// Test with power key
runPowerManagerUpChecks(
- () -> mPolicy.wakeUpFromKey(
- mDefaultDisplay.getDisplayId(), mClock.uptimeMillis(), KEYCODE_POWER, true),
+ () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_POWER, true),
config_allowTheaterModeWakeFromPowerKey,
WAKE_REASON_POWER_BUTTON,
"android.policy:POWER");
@@ -307,15 +254,13 @@
// even if the power-key specific theater mode config is disabled.
setBooleanRes(config_allowTheaterModeWakeFromPowerKey, false);
runPowerManagerUpChecks(
- () -> mPolicy.wakeUpFromKey(mDefaultDisplay.getDisplayId(), mClock.uptimeMillis(),
- KEYCODE_POWER, false),
+ () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_POWER, false),
config_allowTheaterModeWakeFromKey,
WAKE_REASON_POWER_BUTTON,
"android.policy:POWER");
}
@Test
- @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
public void testWakeUpFromLid() {
runPowerManagerUpChecks(
() -> mPolicy.wakeUpFromLid(),
@@ -325,7 +270,6 @@
}
@Test
- @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
public void testWakeUpFromWakeGesture() {
runPowerManagerUpChecks(
() -> mPolicy.wakeUpFromWakeGesture(),
@@ -335,7 +279,6 @@
}
@Test
- @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
public void testwakeUpFromCameraCover() {
runPowerManagerUpChecks(
() -> mPolicy.wakeUpFromCameraCover(mClock.uptimeMillis()),
@@ -345,7 +288,6 @@
}
@Test
- @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
public void testWakeUpFromPowerKeyCameraGesture() {
// Disable the resource affecting all wake keys because it affects power key as well.
// That way, power key wake during theater mode will solely be controlled by
@@ -353,7 +295,7 @@
setBooleanRes(config_allowTheaterModeWakeFromKey, false);
runPowerManagerUpChecks(
- () -> mPolicy.wakeUpFromPowerKeyCameraGesture(mDefaultDisplay.getDisplayId()),
+ () -> mPolicy.wakeUpFromPowerKeyCameraGesture(),
config_allowTheaterModeWakeFromPowerKey,
WAKE_REASON_CAMERA_LAUNCH,
"android.policy:CAMERA_GESTURE_PREVENT_LOCK");
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 65736cb..c8a3559 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -179,17 +179,25 @@
.getAppCompatAspectRatioPolicy()).isLetterboxedForFixedOrientationAndAspectRatio();
}
- void enableTreatmentForTopActivity(boolean enabled) {
- doReturn(enabled).when(mDisplayContent.mAppCompatCameraPolicy)
- .isTreatmentEnabledForActivity(eq(mActivityStack.top()));
+ void enableFullscreenCameraCompatTreatmentForTopActivity(boolean enabled) {
+ if (mDisplayContent.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()) {
+ doReturn(enabled).when(
+ mDisplayContent.mAppCompatCameraPolicy.mDisplayRotationCompatPolicy)
+ .isTreatmentEnabledForActivity(eq(mActivityStack.top()));
+ }
}
- void setTopActivityCameraActive(boolean enabled) {
+ void setIsCameraRunningAndWindowingModeEligibleFullscreen(boolean enabled) {
doReturn(enabled).when(getTopDisplayRotationCompatPolicy())
.isCameraRunningAndWindowingModeEligible(eq(mActivityStack.top()),
/* mustBeFullscreen= */ eq(true));
}
+ void setIsCameraRunningAndWindowingModeEligibleFreeform(boolean enabled) {
+ doReturn(enabled).when(getTopCameraCompatFreeformPolicy())
+ .isCameraRunningAndWindowingModeEligible(eq(mActivityStack.top()));
+ }
+
void setTopActivityEligibleForOrientationOverride(boolean enabled) {
doReturn(enabled).when(getTopDisplayRotationCompatPolicy())
.isActivityEligibleForOrientationOverride(eq(mActivityStack.top()));
@@ -508,8 +516,13 @@
}
private DisplayRotationCompatPolicy getTopDisplayRotationCompatPolicy() {
- return mActivityStack.top().mDisplayContent
- .mAppCompatCameraPolicy.mDisplayRotationCompatPolicy;
+ return mActivityStack.top().mDisplayContent.mAppCompatCameraPolicy
+ .mDisplayRotationCompatPolicy;
+ }
+
+ private CameraCompatFreeformPolicy getTopCameraCompatFreeformPolicy() {
+ return mActivityStack.top().mDisplayContent.mAppCompatCameraPolicy
+ .mCameraCompatFreeformPolicy;
}
// We add the activity to the stack and spyOn() on its properties.
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
index 1e40aa0..b839113 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
@@ -282,7 +282,8 @@
robot.activity().createActivityWithComponentInNewTaskAndDisplay();
robot.checkFixedOrientationLetterboxAspectRatioForTopParent(/* expected */ 1.5f);
- robot.activity().enableTreatmentForTopActivity(/* enabled */ true);
+ robot.activity().enableFullscreenCameraCompatTreatmentForTopActivity(
+ /* enabled */ true);
robot.checkAspectRatioForTopParentIsSplitScreenRatio(/* expected */ true);
});
}
@@ -308,6 +309,12 @@
void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) {
super.onPostDisplayContentCreation(displayContent);
spyOn(displayContent.mAppCompatCameraPolicy);
+ if (displayContent.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()) {
+ spyOn(displayContent.mAppCompatCameraPolicy.mDisplayRotationCompatPolicy);
+ }
+ if (displayContent.mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()) {
+ spyOn(displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy);
+ }
}
@Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
index 41102d6..9b9040b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
@@ -20,6 +20,8 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.AppCompatCameraPolicy.isTreatmentEnabledForActivity;
+import static com.android.server.wm.AppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera;
import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static org.junit.Assert.assertEquals;
@@ -194,9 +196,10 @@
@Test
public void testIsCameraCompatTreatmentActive_whenTreatmentForTopActivityIsEnabled() {
runTestScenario((robot) -> {
+ robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true);
robot.applyOnActivity((a)-> {
- a.createActivityWithComponent();
- a.enableTreatmentForTopActivity(/* enabled */ true);
+ a.createActivityWithComponentInNewTaskAndDisplay();
+ a.enableFullscreenCameraCompatTreatmentForTopActivity(/* enabled */ true);
});
robot.checkIsCameraCompatTreatmentActiveForTopActivity(/* active */ true);
@@ -206,9 +209,10 @@
@Test
public void testIsCameraCompatTreatmentNotActive_whenTreatmentForTopActivityIsDisabled() {
runTestScenario((robot) -> {
+ robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true);
robot.applyOnActivity((a)-> {
a.createActivityWithComponent();
- a.enableTreatmentForTopActivity(/* enabled */ false);
+ a.enableFullscreenCameraCompatTreatmentForTopActivity(/* enabled */ false);
});
robot.checkIsCameraCompatTreatmentActiveForTopActivity(/* active */ false);
@@ -220,9 +224,10 @@
public void testShouldOverrideMinAspectRatioForCamera_whenCameraIsNotRunning() {
runTestScenario((robot) -> {
robot.applyOnActivity((a)-> {
+ robot.allowEnterDesktopMode(true);
robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true);
a.createActivityWithComponentInNewTaskAndDisplay();
- a.setTopActivityCameraActive(/* active */ false);
+ a.setIsCameraRunningAndWindowingModeEligibleFullscreen(/* enabled */ false);
});
robot.checkShouldOverrideMinAspectRatioForCamera(/* active */ false);
@@ -234,9 +239,10 @@
public void testShouldOverrideMinAspectRatioForCamera_whenCameraIsRunning_overrideDisabled() {
runTestScenario((robot) -> {
robot.applyOnActivity((a)-> {
+ robot.allowEnterDesktopMode(true);
robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true);
a.createActivityWithComponentInNewTaskAndDisplay();
- a.setTopActivityCameraActive(/* active */ true);
+ a.setIsCameraRunningAndWindowingModeEligibleFullscreen(/* active */ true);
});
robot.checkShouldOverrideMinAspectRatioForCamera(/* active */ false);
@@ -245,12 +251,28 @@
@Test
@EnableCompatChanges(OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA)
- public void testShouldOverrideMinAspectRatioForCamera_whenCameraIsRunning_overrideEnabled() {
+ public void testShouldOverrideMinAspectRatioForCameraFullscr_cameraIsRunning_overrideEnabled() {
runTestScenario((robot) -> {
robot.applyOnActivity((a)-> {
robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true);
a.createActivityWithComponentInNewTaskAndDisplay();
- a.setTopActivityCameraActive(/* active */ true);
+ a.setIsCameraRunningAndWindowingModeEligibleFullscreen(/* active */ true);
+ });
+
+ robot.checkShouldOverrideMinAspectRatioForCamera(/* active */ true);
+ });
+ }
+
+
+ @Test
+ @EnableCompatChanges(OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+ public void testShouldOverrideMinAspectRatioForCameraFreeform_cameraRunning_overrideEnabled() {
+ runTestScenario((robot) -> {
+ robot.applyOnActivity((a)-> {
+ robot.allowEnterDesktopMode(true);
+ a.createActivityWithComponentInNewTaskAndDisplay();
+ a.setIsCameraRunningAndWindowingModeEligibleFreeform(/* active */ true);
});
robot.checkShouldOverrideMinAspectRatioForCamera(/* active */ true);
@@ -318,13 +340,11 @@
}
void checkIsCameraCompatTreatmentActiveForTopActivity(boolean active) {
- assertEquals(getTopAppCompatCameraPolicy()
- .isTreatmentEnabledForActivity(activity().top()), active);
+ assertEquals(active, isTreatmentEnabledForActivity(activity().top()));
}
void checkShouldOverrideMinAspectRatioForCamera(boolean expected) {
- assertEquals(getTopAppCompatCameraPolicy()
- .shouldOverrideMinAspectRatioForCamera(activity().top()), expected);
+ assertEquals(expected, shouldOverrideMinAspectRatioForCamera(activity().top()));
}
// TODO(b/350460645): Create Desktop Windowing Robot to reuse common functionalities.
@@ -332,9 +352,5 @@
doReturn(isAllowed).when(() ->
DesktopModeHelper.canEnterDesktopMode(any()));
}
-
- private AppCompatCameraPolicy getTopAppCompatCameraPolicy() {
- return activity().top().mDisplayContent.mAppCompatCameraPolicy;
- }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
index 9057b6c..76101d5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
@@ -37,14 +37,18 @@
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import android.compat.testing.PlatformCompatChangeRule;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import androidx.annotation.NonNull;
@@ -321,7 +325,22 @@
});
robot.applyOnActivity((a) -> {
a.createActivityWithComponentInNewTaskAndDisplay();
- a.setTopActivityCameraActive(false);
+ a.setIsCameraRunningAndWindowingModeEligibleFullscreen(false);
+ });
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+ public void testOverrideOrientationIfNeeded_fullscrOverrideFreeform_cameraActivity_unchanged() {
+ runTestScenario((robot) -> {
+ robot.applyOnActivity((a) -> {
+ robot.allowEnterDesktopMode(true);
+ a.createActivityWithComponentInNewTaskAndDisplay();
+ a.setIsCameraRunningAndWindowingModeEligibleFreeform(false);
});
robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
@@ -426,8 +445,8 @@
c.enablePolicyForIgnoringRequestedOrientation(true);
});
robot.applyOnActivity((a) -> {
- a.createActivityWithComponentInNewTask();
- a.enableTreatmentForTopActivity(true);
+ a.createActivityWithComponentInNewTaskAndDisplay();
+ a.enableFullscreenCameraCompatTreatmentForTopActivity(true);
});
robot.prepareRelaunchingAfterRequestedOrientationChanged(false);
@@ -591,5 +610,11 @@
private AppCompatOrientationPolicy getTopAppCompatOrientationPolicy() {
return activity().top().mAppCompatController.getOrientationPolicy();
}
+
+ // TODO(b/350460645): Create Desktop Windowing Robot to reuse common functionalities.
+ void allowEnterDesktopMode(boolean isAllowed) {
+ doReturn(isAllowed).when(() ->
+ DesktopModeHelper.canEnterDesktopMode(any()));
+ }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 401964c..1fa6578 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -23,6 +23,10 @@
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
+import static android.view.WindowInsets.Type.captionBar;
+import static android.view.WindowInsets.Type.ime;
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
import static android.view.WindowInsets.Type.systemOverlays;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
@@ -36,6 +40,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyFloat;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
@@ -80,6 +85,8 @@
import android.os.ShellCallback;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.util.ArraySet;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.InsetsFrameProvider;
@@ -103,6 +110,7 @@
import java.io.FileDescriptor;
import java.util.ArrayList;
import java.util.Comparator;
+import java.util.List;
import java.util.NoSuchElementException;
@@ -967,7 +975,7 @@
Rect insetsRect = new Rect(0, 200, 1080, 700);
final int flags = FLAG_FORCE_CONSUMING;
final InsetsFrameProvider provider =
- new InsetsFrameProvider(owner, 1, WindowInsets.Type.captionBar())
+ new InsetsFrameProvider(owner, 1, captionBar())
.setArbitraryRectangle(insetsRect)
.setFlags(flags);
task.addLocalInsetsFrameProvider(provider, owner);
@@ -1678,6 +1686,178 @@
assertFalse("The source must be removed.", hasLocalSource(task, provider.getId()));
}
+ @Test
+ public void testSetExcludeInsetsTypes_appliedOnNodeAndChildren() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+ final TestWindowContainer child21 = child2.addChildWindow();
+
+ assertEquals(0, root.mMergedExcludeInsetsTypes);
+ assertEquals(0, child1.mMergedExcludeInsetsTypes);
+ assertEquals(0, child11.mMergedExcludeInsetsTypes);
+ assertEquals(0, child12.mMergedExcludeInsetsTypes);
+ assertEquals(0, child2.mMergedExcludeInsetsTypes);
+ assertEquals(0, child21.mMergedExcludeInsetsTypes);
+
+ child1.setExcludeInsetsTypes(ime());
+ assertEquals(0, root.mMergedExcludeInsetsTypes);
+ assertEquals(ime(), child1.mMergedExcludeInsetsTypes);
+ assertEquals(ime(), child11.mMergedExcludeInsetsTypes);
+ assertEquals(ime(), child12.mMergedExcludeInsetsTypes);
+ assertEquals(0, child2.mMergedExcludeInsetsTypes);
+ assertEquals(0, child21.mMergedExcludeInsetsTypes);
+
+ child11.setExcludeInsetsTypes(navigationBars());
+ assertEquals(0, root.mMergedExcludeInsetsTypes);
+ assertEquals(ime(), child1.mMergedExcludeInsetsTypes);
+ assertEquals(ime() | navigationBars(), child11.mMergedExcludeInsetsTypes);
+ assertEquals(ime(), child12.mMergedExcludeInsetsTypes);
+ assertEquals(0, child2.mMergedExcludeInsetsTypes);
+ assertEquals(0, child21.mMergedExcludeInsetsTypes);
+
+ // overwriting the same value has no change
+ for (int i = 0; i < 2; i++) {
+ root.setExcludeInsetsTypes(statusBars());
+ assertEquals(statusBars(), root.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars() | ime(), child1.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars() | ime() | navigationBars(),
+ child11.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars() | ime(), child12.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars(), child2.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars(), child21.mMergedExcludeInsetsTypes);
+ }
+
+ // set and reset type statusBars on child. Should have no effect because of parent
+ child2.setExcludeInsetsTypes(statusBars());
+ assertEquals(statusBars(), root.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars() | ime(), child1.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars() | ime() | navigationBars(), child11.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars() | ime(), child12.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars(), child2.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars(), child21.mMergedExcludeInsetsTypes);
+
+ // reset
+ child2.setExcludeInsetsTypes(0);
+ assertEquals(statusBars(), root.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars() | ime(), child1.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars() | ime() | navigationBars(), child11.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars() | ime(), child12.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars(), child2.mMergedExcludeInsetsTypes);
+ assertEquals(statusBars(), child21.mMergedExcludeInsetsTypes);
+
+ // when parent has statusBars also removed, it should be cleared from all children in the
+ // hierarchy
+ root.setExcludeInsetsTypes(0);
+ assertEquals(0, root.mMergedExcludeInsetsTypes);
+ assertEquals(ime(), child1.mMergedExcludeInsetsTypes);
+ assertEquals(ime() | navigationBars(), child11.mMergedExcludeInsetsTypes);
+ assertEquals(ime(), child12.mMergedExcludeInsetsTypes);
+ assertEquals(0, child2.mMergedExcludeInsetsTypes);
+ assertEquals(0, child21.mMergedExcludeInsetsTypes);
+
+ // change on node should have no effect on siblings
+ child12.setExcludeInsetsTypes(captionBar());
+ assertEquals(0, root.mMergedExcludeInsetsTypes);
+ assertEquals(ime(), child1.mMergedExcludeInsetsTypes);
+ assertEquals(ime() | navigationBars(), child11.mMergedExcludeInsetsTypes);
+ assertEquals(captionBar() | ime(), child12.mMergedExcludeInsetsTypes);
+ assertEquals(0, child2.mMergedExcludeInsetsTypes);
+ assertEquals(0, child21.mMergedExcludeInsetsTypes);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.view.inputmethod.Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
+ public void testSetExcludeInsetsTypes_appliedAfterReparenting() {
+ final SurfaceControl mockSurfaceControl = mock(SurfaceControl.class);
+ final DisplayContent mockDisplayContent = mock(DisplayContent.class);
+ final var mockInsetsStateController = mock(InsetsStateController.class);
+ doReturn(mockInsetsStateController).when(mockDisplayContent).getInsetsStateController();
+
+ final WindowContainer root1 = createWindowContainerSpy(mockSurfaceControl,
+ mockDisplayContent);
+ final WindowContainer root2 = createWindowContainerSpy(mockSurfaceControl,
+ mockDisplayContent);
+ final WindowContainer child = createWindowContainerSpy(mockSurfaceControl,
+ mockDisplayContent);
+ doNothing().when(child).onConfigurationChanged(any());
+
+ root1.setExcludeInsetsTypes(ime());
+ root2.setExcludeInsetsTypes(captionBar());
+ assertEquals(ime(), root1.mMergedExcludeInsetsTypes);
+ assertEquals(captionBar(), root2.mMergedExcludeInsetsTypes);
+ assertEquals(0, child.mMergedExcludeInsetsTypes);
+ clearInvocations(mockInsetsStateController);
+
+ root1.addChild(child, 0);
+ assertEquals(ime(), child.mMergedExcludeInsetsTypes);
+ verify(mockInsetsStateController).notifyInsetsChanged(
+ new ArraySet<>(List.of(child.asWindowState())));
+ clearInvocations(mockInsetsStateController);
+
+ // Make sure that reparenting does not call notifyInsetsChanged twice
+ child.reparent(root2, 0);
+ assertEquals(captionBar(), child.mMergedExcludeInsetsTypes);
+ verify(mockInsetsStateController).notifyInsetsChanged(
+ new ArraySet<>(List.of(child.asWindowState())));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.view.inputmethod.Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
+ public void testSetExcludeInsetsTypes_notifyInsetsAfterChange() {
+ final var mockDisplayContent = mock(DisplayContent.class);
+ final var mockInsetsStateController = mock(InsetsStateController.class);
+ doReturn(mockInsetsStateController).when(mockDisplayContent).getInsetsStateController();
+
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ final WindowState mockRootWs = mock(WindowState.class);
+ final TestWindowContainer root = builder.setLayer(0).setAsWindowState(mockRootWs).build();
+ root.mDisplayContent = mockDisplayContent;
+ verify(mockInsetsStateController, never()).notifyInsetsChanged(any());
+
+ root.setExcludeInsetsTypes(ime());
+ assertEquals(ime(), root.mMergedExcludeInsetsTypes);
+ verify(mockInsetsStateController).notifyInsetsChanged(
+ new ArraySet<>(List.of(root.asWindowState())));
+ clearInvocations(mockInsetsStateController);
+
+ // adding a child (while parent has set excludedInsetsTypes) should trigger
+ // notifyInsetsChanged
+ final WindowState mockChildWs = mock(WindowState.class);
+ final TestWindowContainer child1 = builder.setLayer(0).setAsWindowState(
+ mockChildWs).build();
+ child1.mDisplayContent = mockDisplayContent;
+ root.addChildWindow(child1);
+ // TestWindowContainer overrides onParentChanged and therefore doesn't call into
+ // mergeExcludeInsetsTypesAndNotifyInsetsChanged. This is checked in another test
+ assertTrue(child1.mOnParentChangedCalled);
+ child1.setExcludeInsetsTypes(ime());
+ assertEquals(ime(), child1.mMergedExcludeInsetsTypes);
+ verify(mockInsetsStateController).notifyInsetsChanged(
+ new ArraySet<>(List.of(child1.asWindowState())));
+ clearInvocations(mockInsetsStateController);
+
+ // not changing excludedInsetsTypes should not trigger notifyInsetsChanged again
+ root.setExcludeInsetsTypes(ime());
+ assertEquals(ime(), root.mMergedExcludeInsetsTypes);
+ assertEquals(ime(), child1.mMergedExcludeInsetsTypes);
+ verify(mockInsetsStateController, never()).notifyInsetsChanged(any());
+ }
+
+ private WindowContainer<?> createWindowContainerSpy(SurfaceControl mockSurfaceControl,
+ DisplayContent mockDisplayContent) {
+ final WindowContainer<?> wc = spy(new WindowContainer<>(mWm));
+ final WindowState mocWs = mock(WindowState.class);
+ doReturn(mocWs).when(wc).asWindowState();
+ wc.mSurfaceControl = mockSurfaceControl;
+ wc.mDisplayContent = mockDisplayContent;
+ return wc;
+ }
+
private static boolean hasLocalSource(WindowContainer container, int sourceId) {
if (container.mLocalInsetsSources == null) {
return false;
@@ -1693,6 +1873,7 @@
private boolean mFillsParent;
private boolean mWaitForTransitStart;
private Integer mOrientation;
+ private WindowState mWindowState;
private boolean mOnParentChangedCalled;
private boolean mOnDescendantOverrideCalled;
@@ -1714,7 +1895,7 @@
};
TestWindowContainer(WindowManagerService wm, int layer, boolean isAnimating,
- boolean isVisible, boolean waitTransitStart, Integer orientation) {
+ boolean isVisible, boolean waitTransitStart, Integer orientation, WindowState ws) {
super(wm);
mLayer = layer;
@@ -1723,6 +1904,7 @@
mFillsParent = true;
mOrientation = orientation;
mWaitForTransitStart = waitTransitStart;
+ mWindowState = ws;
spyOn(mSurfaceAnimator);
doReturn(mIsAnimating).when(mSurfaceAnimator).isAnimating();
doReturn(ANIMATION_TYPE_APP_TRANSITION).when(mSurfaceAnimator).getAnimationType();
@@ -1790,6 +1972,11 @@
boolean isWaitingForTransitionStart() {
return mWaitForTransitStart;
}
+
+ @Override
+ WindowState asWindowState() {
+ return mWindowState;
+ }
}
private static class TestWindowContainerBuilder {
@@ -1799,6 +1986,7 @@
private boolean mIsVisible;
private boolean mIsWaitTransitStart;
private Integer mOrientation;
+ private WindowState mWindowState;
TestWindowContainerBuilder(WindowManagerService wm) {
mWm = wm;
@@ -1806,6 +1994,7 @@
mIsAnimating = false;
mIsVisible = false;
mOrientation = null;
+ mWindowState = null;
}
TestWindowContainerBuilder setLayer(int layer) {
@@ -1828,6 +2017,11 @@
return this;
}
+ TestWindowContainerBuilder setAsWindowState(WindowState ws) {
+ mWindowState = ws;
+ return this;
+ }
+
TestWindowContainerBuilder setWaitForTransitionStart(boolean waitTransitStart) {
mIsWaitTransitStart = waitTransitStart;
return this;
@@ -1835,7 +2029,7 @@
TestWindowContainer build() {
return new TestWindowContainer(mWm, mLayer, mIsAnimating, mIsVisible,
- mIsWaitTransitStart, mOrientation);
+ mIsWaitTransitStart, mOrientation, mWindowState);
}
}
diff --git a/telephony/common/com/android/internal/telephony/CarrierAppUtils.java b/telephony/common/com/android/internal/telephony/CarrierAppUtils.java
index ff9cba2..f001232 100644
--- a/telephony/common/com/android/internal/telephony/CarrierAppUtils.java
+++ b/telephony/common/com/android/internal/telephony/CarrierAppUtils.java
@@ -33,6 +33,7 @@
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.flags.Flags;
import com.android.internal.telephony.util.TelephonyUtils;
import java.util.ArrayList;
@@ -185,11 +186,7 @@
if (hasPrivileges) {
// Only update enabled state for the app on /system. Once it has been
// updated we shouldn't touch it.
- if (!isUpdatedSystemApp(ai) && enabledSetting
- == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
- || enabledSetting
- == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
- || (ai.flags & ApplicationInfo.FLAG_INSTALLED) == 0) {
+ if (shouldUpdateEnabledState(ai, enabledSetting)) {
Log.i(TAG, "Update state (" + packageName + "): ENABLED for user "
+ userId);
context.createContextAsUser(UserHandle.of(userId), 0)
@@ -330,6 +327,21 @@
}
}
+ private static boolean shouldUpdateEnabledState(ApplicationInfo appInfo, int enabledSetting) {
+ if (Flags.cleanupCarrierAppUpdateEnabledStateLogic()) {
+ return !isUpdatedSystemApp(appInfo)
+ && (enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+ || enabledSetting
+ == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
+ || (appInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0);
+ } else {
+ return !isUpdatedSystemApp(appInfo)
+ && enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+ || enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
+ || (appInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0;
+ }
+ }
+
/**
* Returns the list of "default" carrier apps.
*
diff --git a/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java
index e3a129f..d03ad5c 100644
--- a/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java
+++ b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java
@@ -61,8 +61,13 @@
@Test
public void canRead() {
ApplicationSharedMemory instance = ApplicationSharedMemory.getInstance();
- instance.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis();
- // Don't actually care about the value of the above.
+ try {
+ instance.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis();
+ // Don't actually care about the value of the above.
+ } catch (java.time.DateTimeException e) {
+ // This exception is okay during testing. It means there was no time source, which
+ // could be because of network problems or a feature being flagged off.
+ }
}
/** Application processes should not have mutable access. */
diff --git a/tests/Internal/src/com/android/internal/os/OWNERS b/tests/Internal/src/com/android/internal/os/OWNERS
new file mode 100644
index 0000000..64ffa46
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/os/OWNERS
@@ -0,0 +1,2 @@
+# ApplicationSharedMemory
+per-file *ApplicationSharedMemory* = file:/PERFORMANCE_OWNERS
diff --git a/tools/processors/property_cache/Android.bp b/tools/processors/property_cache/Android.bp
new file mode 100644
index 0000000..81fab7a
--- /dev/null
+++ b/tools/processors/property_cache/Android.bp
@@ -0,0 +1,57 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+ default_team: "trendy_team_framework_android_multiuser",
+}
+
+java_library_host {
+ name: "libcached-property-annotation-processor",
+ srcs: [
+ ":framework-annotations",
+ "src/**/*.java",
+ ],
+ static_libs: [
+ "codegen-version-info",
+ "android.multiuser.flags-aconfig-java-host",
+ "guava",
+ ],
+ use_tools_jar: true,
+}
+
+java_plugin {
+ name: "cached-property-annotation-processor",
+ processor_class: "android.processor.property_cache.CachedPropertyProcessor",
+ static_libs: ["libcached-property-annotation-processor"],
+}
+
+java_aconfig_library {
+ name: "android.multiuser.flags-aconfig-java-host",
+ aconfig_declarations: "android.multiuser.flags-aconfig",
+ host_supported: true,
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
+java_test_host {
+ name: "cached-property-annotation-processor-test",
+ srcs: ["test/java/**/*.java"],
+ java_resources: [":CachedPropertyAnnotationJavaTestSource"],
+ static_libs: [
+ "compile-testing-prebuilt",
+ "truth",
+ "junit",
+ "guava",
+ "libcached-property-annotation-processor",
+ ],
+ test_suites: ["general-tests"],
+}
+
+filegroup {
+ name: "CachedPropertyAnnotationJavaTestSource",
+ srcs: ["test/resources/*.java"],
+ path: "test/resources/",
+ visibility: ["//visibility:private"],
+}
diff --git a/tools/processors/property_cache/TEST_MAPPING b/tools/processors/property_cache/TEST_MAPPING
new file mode 100644
index 0000000..7177abc
--- /dev/null
+++ b/tools/processors/property_cache/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "postsubmit": [
+ {
+ "name": "cached-property-annotation-processor-test"
+ }
+ ]
+}
diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/CacheConfig.java b/tools/processors/property_cache/src/java/android/processor/property_cache/CacheConfig.java
new file mode 100644
index 0000000..c665c84
--- /dev/null
+++ b/tools/processors/property_cache/src/java/android/processor/property_cache/CacheConfig.java
@@ -0,0 +1,138 @@
+/*
+ * 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 android.processor.property_cache;
+
+import com.android.internal.annotations.CachedProperty;
+import com.android.internal.annotations.CachedPropertyDefaults;
+
+import com.google.common.base.CaseFormat;
+
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+
+public class CacheConfig {
+ private final CacheModifiers mModifiers;
+ private final int mMaxSize;
+ private final String mModuleName;
+ private final String mApiName;
+ private final String mClassName;
+ private final String mQualifiedName;
+ private String mPropertyName;
+ private String mMethodName;
+ private int mNumberOfParams = 0;
+ private String mInputType = Constants.JAVA_LANG_VOID;
+ private String mResultType;
+
+ public CacheConfig(TypeElement classElement, ExecutableElement method) {
+ CachedPropertyDefaults classAnnotation = classElement.getAnnotation(
+ CachedPropertyDefaults.class);
+ CachedProperty methodAnnotation = method.getAnnotation(CachedProperty.class);
+
+ mModuleName = methodAnnotation.module().isEmpty() ? classAnnotation.module()
+ : methodAnnotation.module();
+ mClassName = classElement.getSimpleName().toString();
+ mQualifiedName = classElement.getQualifiedName().toString();
+ mModifiers = new CacheModifiers(methodAnnotation.modsFlagOnOrNone());
+ mMethodName = method.getSimpleName().toString();
+ mPropertyName = getPropertyName(mMethodName);
+ mApiName = methodAnnotation.api().isEmpty() ? getUniqueApiName(mClassName, mPropertyName)
+ : methodAnnotation.api();
+ mMaxSize = methodAnnotation.max() == -1 ? classAnnotation.max() : methodAnnotation.max();
+ mNumberOfParams = method.getParameters().size();
+ if (mNumberOfParams > 0) {
+ mInputType = primitiveTypeToObjectEquivalent(
+ method.getParameters().get(0).asType().toString());
+ }
+ mResultType = primitiveTypeToObjectEquivalent(method.getReturnType().toString());
+ }
+
+ public CacheModifiers getModifiers() {
+ return mModifiers;
+ }
+
+ public int getMaxSize() {
+ return mMaxSize;
+ }
+
+ public String getApiName() {
+ return mApiName;
+ }
+
+ public String getClassName() {
+ return mClassName;
+ }
+
+ public String getQualifiedName() {
+ return mQualifiedName;
+ }
+
+ public String getModuleName() {
+ return mModuleName;
+ }
+
+ public String getMethodName() {
+ return mMethodName;
+ }
+
+ public String getPropertyName() {
+ return mPropertyName;
+ }
+
+ public String getPropertyVariable() {
+ return (mModifiers.isStatic() ? "s" : "m") + mPropertyName;
+ }
+
+ private String getPropertyName(String methodName) {
+ if (methodName.startsWith("get")) {
+ return methodName.substring(3);
+ } else if (methodName.startsWith("is")) {
+ return methodName.substring(2);
+ } else {
+ return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, methodName);
+ }
+ }
+
+ public int getNumberOfParams() {
+ return mNumberOfParams;
+ }
+
+ public String getInputType() {
+ return mInputType;
+ }
+
+ public String getResultType() {
+ return mResultType;
+ }
+
+ /**
+ * This method returns the unique api name for a given class and property name.
+ * Property name is retrieved from the method name.
+ * Both names are combined and converted to lower snake case.
+ *
+ * @param className The name of the class that contains the property.
+ * @param propertyName The name of the property.
+ * @return The registration name for the property.
+ */
+ private String getUniqueApiName(String className, String propertyName) {
+ return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, className + propertyName);
+ }
+
+ private String primitiveTypeToObjectEquivalent(String simpleType) {
+ // checking against primitive types
+ return Constants.PRIMITIVE_TYPE_MAP.getOrDefault(simpleType, simpleType);
+ }
+}
diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/CacheModifiers.java b/tools/processors/property_cache/src/java/android/processor/property_cache/CacheModifiers.java
new file mode 100644
index 0000000..fda9b2c
--- /dev/null
+++ b/tools/processors/property_cache/src/java/android/processor/property_cache/CacheModifiers.java
@@ -0,0 +1,40 @@
+/*
+ * 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 android.processor.property_cache;
+
+import com.android.internal.annotations.CacheModifier;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class CacheModifiers {
+ private final boolean mIsStatic;
+ private static final String STATIC_MODIFIER_STRING = "static ";
+
+ CacheModifiers(CacheModifier[] modifierArray) {
+ final List<CacheModifier> modifiers = Arrays.asList(modifierArray);
+ mIsStatic = modifiers.contains(CacheModifier.STATIC);
+ }
+
+ public boolean isStatic() {
+ return mIsStatic;
+ }
+
+ public String getStaticModifier() {
+ return mIsStatic ? STATIC_MODIFIER_STRING : Constants.EMPTY_STRING;
+ }
+}
diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/CachedPropertyProcessor.java b/tools/processors/property_cache/src/java/android/processor/property_cache/CachedPropertyProcessor.java
new file mode 100644
index 0000000..0361012
--- /dev/null
+++ b/tools/processors/property_cache/src/java/android/processor/property_cache/CachedPropertyProcessor.java
@@ -0,0 +1,95 @@
+/*
+ * 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 android.processor.property_cache;
+
+import com.android.internal.annotations.CachedProperty;
+import com.android.internal.annotations.CachedPropertyDefaults;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.ElementFilter;
+import javax.tools.JavaFileObject;
+
+public class CachedPropertyProcessor extends AbstractProcessor {
+
+ IpcDataCacheComposer mIpcDataCacheComposer =
+ new IpcDataCacheComposer();
+
+ @Override
+ public Set<String> getSupportedAnnotationTypes() {
+ return new HashSet<String>(
+ ImmutableSet.of(CachedPropertyDefaults.class.getCanonicalName()));
+ }
+
+ @Override
+ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ for (Element element : roundEnv.getElementsAnnotatedWith(CachedPropertyDefaults.class)) {
+ try {
+ generateCachedClass((TypeElement) element, processingEnv.getFiler());
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+ return false;
+ }
+
+ private void generateCachedClass(TypeElement classElement, Filer filer) throws IOException {
+ String packageName =
+ processingEnv
+ .getElementUtils()
+ .getPackageOf(classElement)
+ .getQualifiedName()
+ .toString();
+ String className = classElement.getSimpleName().toString() + "Cache";
+ JavaFileObject jfo = filer.createSourceFile(packageName + "." + className);
+ Writer writer = jfo.openWriter();
+ writer.write("package " + packageName + ";\n\n");
+ writer.write("import android.os.IpcDataCache;\n");
+ writer.write("\n /** \n * This class is auto-generated \n * @hide \n **/");
+ writer.write("\npublic class " + className + " {\n");
+
+ List<ExecutableElement> methods =
+ ElementFilter.methodsIn(classElement.getEnclosedElements());
+ String initCache = String.format(Constants.METHOD_COMMENT,
+ " - initialise all caches for class " + className)
+ + "\npublic static void initCache() {";
+ for (ExecutableElement method : methods) {
+ if (method.getAnnotation(CachedProperty.class) != null) {
+ mIpcDataCacheComposer.generatePropertyCache(writer, classElement, method);
+ initCache += "\n " + mIpcDataCacheComposer.generateInvalidatePropertyCall();
+ }
+ }
+ initCache += "\n}";
+ writer.write(initCache);
+ writer.write("\n}");
+ writer.write("\n");
+ writer.close();
+ }
+}
diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/Constants.java b/tools/processors/property_cache/src/java/android/processor/property_cache/Constants.java
new file mode 100644
index 0000000..03961bc
--- /dev/null
+++ b/tools/processors/property_cache/src/java/android/processor/property_cache/Constants.java
@@ -0,0 +1,40 @@
+/*
+ * 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 android.processor.property_cache;
+
+import com.google.common.collect.ImmutableMap;
+
+public final class Constants {
+ public static final String EMPTY_STRING = "";
+ public static final String JAVA_LANG_VOID = "java.lang.Void";
+ public static final ImmutableMap<String, String> PRIMITIVE_TYPE_MAP =
+ ImmutableMap.of(
+ "int", "java.lang.Integer",
+ "boolean", "java.lang.Boolean",
+ "long", "java.lang.Long",
+ "float", "java.lang.Float",
+ "double", "java.lang.Double",
+ "byte", "java.lang.Byte",
+ "short", "java.lang.Short",
+ "char", "java.lang.Character");
+
+ public static final String METHOD_COMMENT = "\n /**"
+ + "\n * This method is auto-generated%s"
+ + "\n * "
+ + "\n * @hide"
+ + "\n */";
+}
diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/IpcDataCacheComposer.java b/tools/processors/property_cache/src/java/android/processor/property_cache/IpcDataCacheComposer.java
new file mode 100644
index 0000000..8526a04
--- /dev/null
+++ b/tools/processors/property_cache/src/java/android/processor/property_cache/IpcDataCacheComposer.java
@@ -0,0 +1,152 @@
+/*
+ * 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 android.processor.property_cache;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+
+public class IpcDataCacheComposer {
+
+ private static final String PROPERTY_DEFINITION_LINE = "private %s%s %s;\n";
+ private static final String METHOD_NAME_LINE = "\npublic %s%s %s(%s%s%s\n) {\n";
+ private static final String RETURN_IF_NOT_NULL_LINE =
+ "if (%s != null) {\n return %s.%s;\n }";
+
+ private CacheConfig mCacheConfig;
+
+ /**
+ * Generates code for property cache.
+ *
+ * @param writer writer to write code to.
+ * @param classElement class element to generate code for.
+ * @param method method element to generate code for.
+ * @throws IOException if writer throws IOException.
+ */
+ public void generatePropertyCache(Writer writer, TypeElement classElement,
+ ExecutableElement method) throws IOException {
+
+ mCacheConfig = new CacheConfig(classElement, method);
+
+ ParamComposer inputParam = new ParamComposer(null, null);
+ ParamComposer binderParam = new ParamComposer(
+ String.format("IpcDataCache.RemoteCall<%s, %s>", mCacheConfig.getInputType(),
+ mCacheConfig.getResultType()), "binderCall");
+
+ ParamComposer bypassParam = new ParamComposer(null, null); // empty if method have no params
+ String queryCall = "query(null)";
+ if (mCacheConfig.getNumberOfParams() > 0) {
+ bypassParam = new ParamComposer(
+ String.format("IpcDataCache.BypassCall<%s> ", mCacheConfig.getInputType()),
+ "bypassPredicate");
+ inputParam = new ParamComposer(mCacheConfig.getInputType(), "query");
+ queryCall = "query(query)";
+ }
+ String propertyClass =
+ "IpcDataCache<" + mCacheConfig.getInputType() + ", " + mCacheConfig.getResultType()
+ + ">";
+ String invalidateName = "invalidate" + mCacheConfig.getPropertyName();
+ String lockObject = mCacheConfig.getPropertyVariable() + "Lock";
+ writer.write("private " + mCacheConfig.getModifiers().getStaticModifier() + "final Object "
+ + lockObject + " = new Object();\n");
+ writer.write(String.format(PROPERTY_DEFINITION_LINE,
+ mCacheConfig.getModifiers().getStaticModifier(), propertyClass,
+ mCacheConfig.getPropertyVariable()));
+
+ writer.write(propertyInvalidatedCacheMethod(binderParam, bypassParam, inputParam, queryCall,
+ lockObject));
+
+ // If binder param is not empty then generate getter without binder param to be called
+ if (!bypassParam.getParam().isEmpty()) {
+ writer.write(propertyInvalidatedCacheMethod(binderParam, new ParamComposer(null, null),
+ inputParam, queryCall, lockObject));
+ }
+ writer.write(String.format(Constants.METHOD_COMMENT,
+ "- invalidate cache for {@link " + mCacheConfig.getQualifiedName() + "#"
+ + mCacheConfig.getMethodName() + "}"));
+ writer.write("\n public static final void " + invalidateName + "() {");
+ writer.write(
+ "\n IpcDataCache.invalidateCache(\"" + mCacheConfig.getModuleName() + "\", \""
+ + mCacheConfig.getApiName() + "\");");
+ writer.write("\n }");
+ writer.write("\n");
+ writer.write("\n");
+ }
+
+ /**
+ * Generates code to call cache invalidation.
+ *
+ * @return code string calling cache invalidation.
+ */
+ public String generateInvalidatePropertyCall() {
+ String invalidateName = "invalidate" + mCacheConfig.getPropertyName();
+ return mCacheConfig.getClassName() + "Cache." + invalidateName + "();";
+ }
+
+ /**
+ * Generates code for getter that returns cached value or calls binder and caches result.
+ *
+ * @param binderParam parameter for binder call.
+ * @param bypassParam parameter for bypass predicate.
+ * @param inputParam parameter for input value.
+ * @param queryCall cache query call syntax.
+ * @param lockObject object to synchronize on.
+ * @return String with code for method.
+ */
+ private String propertyInvalidatedCacheMethod(ParamComposer binderParam,
+ ParamComposer bypassParam, ParamComposer inputParam, String queryCall,
+ String lockObject) {
+ String result = "\n";
+ CacheModifiers modifiers = mCacheConfig.getModifiers();
+ String paramsComments = binderParam.getParamComment(
+ "lambda for remote call" + " {@link " + mCacheConfig.getQualifiedName() + "#"
+ + mCacheConfig.getMethodName() + " }") + bypassParam.getParamComment(
+ "lambda to bypass remote call") + inputParam.getParamComment(
+ "parameter to call remote lambda");
+ result += String.format(Constants.METHOD_COMMENT, paramsComments);
+ result += String.format(METHOD_NAME_LINE, modifiers.getStaticModifier(),
+ mCacheConfig.getResultType(), mCacheConfig.getMethodName(),
+ binderParam.getParam(), bypassParam.getNextParam(),
+ inputParam.getNextParam());
+ result += String.format(RETURN_IF_NOT_NULL_LINE, mCacheConfig.getPropertyVariable(),
+ mCacheConfig.getPropertyVariable(), queryCall);
+ result += "\n synchronized (" + lockObject + " ) {";
+ result += "\n if (" + mCacheConfig.getPropertyVariable() + " == null) {";
+ result += "\n " + mCacheConfig.getPropertyVariable() + " = new IpcDataCache" + "("
+ + generateCreateIpcConfig() + ", " + binderParam.getName()
+ + bypassParam.getNextName() + ");\n";
+ result += "\n }";
+ result += "\n }";
+ result += "\n return " + mCacheConfig.getPropertyVariable() + "." + queryCall + ";";
+ result += "\n }";
+ result += "\n";
+ return result;
+ }
+
+ /**
+ * Generates code for new IpcDataCache.Config object for given configuration.
+ *
+ * @return String with code for new IpcDataCache.Config object.
+ */
+ public String generateCreateIpcConfig() {
+ return "new IpcDataCache.Config(" + mCacheConfig.getMaxSize() + ", " + "\""
+ + mCacheConfig.getModuleName() + "\"" + ", " + "\"" + mCacheConfig.getApiName()
+ + "\"" + ", " + "\"" + mCacheConfig.getPropertyName() + "\"" + ")";
+ }
+}
diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/ParamComposer.java b/tools/processors/property_cache/src/java/android/processor/property_cache/ParamComposer.java
new file mode 100644
index 0000000..307443a
--- /dev/null
+++ b/tools/processors/property_cache/src/java/android/processor/property_cache/ParamComposer.java
@@ -0,0 +1,103 @@
+/*
+ * 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 android.processor.property_cache;
+
+public class ParamComposer {
+ private String mType;
+ private String mName;
+
+ /** Creates ParamComposer with given type and name.
+ *
+ * @param type type of parameter.
+ * @param name name of parameter.
+ */
+ public ParamComposer(String type, String name) {
+ mType = type;
+ mName = name;
+ }
+
+ /** Returns name of parameter.
+ *
+ * @return name of parameter.
+ */
+ public String getName() {
+ if (mName != null) {
+ return mName;
+ }
+ return Constants.EMPTY_STRING;
+ }
+
+ /** Returns name of parameter for next parameter followed by comma.
+ *
+ * @return name of parameter for next parameter if exists, empty string otherwise.
+ */
+ public String getNextName() {
+ if (!getName().isEmpty()) {
+ return ", " + getName();
+ }
+ return Constants.EMPTY_STRING;
+ }
+
+ /**
+ * Returns type of parameter.
+ *
+ * @return type of parameter.
+ */
+ public String getType() {
+ if (mType != null) {
+ return mType;
+ }
+ return Constants.EMPTY_STRING;
+ }
+
+ /**
+ * Returns type and name of parameter.
+ *
+ * @return type and name of parameter if exists, empty string otherwise.
+ */
+ public String getParam() {
+ if (!getType().isEmpty() && !getName().isEmpty()) {
+ return getType() + " " + getName();
+ }
+ return Constants.EMPTY_STRING;
+ }
+
+ /**
+ * Returns type and name of parameter for next parameter followed by comma.
+ *
+ * @return type and name of parameter for next parameter if exists, empty string otherwise.
+ */
+ public String getNextParam() {
+ if (!getType().isEmpty() && !getName().isEmpty()) {
+ return ", " + getParam();
+ }
+ return Constants.EMPTY_STRING;
+ }
+
+ /**
+ * Returns comment for parameter.
+ *
+ * @param description of parameter.
+ * @return comment for parameter if exists, empty string otherwise.
+ */
+ public String getParamComment(String description) {
+ if (!getType().isEmpty() && !getName().isEmpty()) {
+ return "\n * @param " + getName() + " - " + description;
+ }
+ return Constants.EMPTY_STRING;
+ }
+}
diff --git a/tools/processors/property_cache/test/java/android/processor/property_cache/CachedPropertyProcessorTest.java b/tools/processors/property_cache/test/java/android/processor/property_cache/CachedPropertyProcessorTest.java
new file mode 100644
index 0000000..1e23c78
--- /dev/null
+++ b/tools/processors/property_cache/test/java/android/processor/property_cache/CachedPropertyProcessorTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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 android.processor.property_cache.test;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.testing.compile.CompilationSubject.assertThat;
+
+import android.processor.property_cache.CachedPropertyProcessor;
+
+import com.google.testing.compile.Compilation;
+import com.google.testing.compile.Compiler;
+import com.google.testing.compile.JavaFileObjects;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import javax.tools.JavaFileObject;
+import javax.tools.StandardLocation;
+
+/** Tests the {@link CachedPropertyProcessor}. */
+@RunWith(JUnit4.class)
+public class CachedPropertyProcessorTest {
+ private final Compiler mCompiler =
+ Compiler.javac().withProcessors(new CachedPropertyProcessor());
+
+ @Test
+ public void testDefaultValues() {
+ JavaFileObject expectedJava = JavaFileObjects.forResource("DefaultCache.java");
+
+ Compilation compilation = mCompiler.compile(JavaFileObjects.forResource("Default.java"));
+ assertThat(compilation).succeeded();
+ assertThat(compilation)
+ .generatedFile(StandardLocation.SOURCE_OUTPUT,
+ "android/processor/property_cache/test/DefaultCache.java")
+ .hasSourceEquivalentTo(expectedJava);
+ }
+
+ @Test
+ public void testCustomValues() {
+ JavaFileObject expectedJava = JavaFileObjects.forResource("CustomCache.java");
+
+ Compilation compilation = mCompiler.compile(JavaFileObjects.forResource("Custom.java"));
+ assertThat(compilation).succeeded();
+ assertThat(compilation)
+ .generatedFile(StandardLocation.SOURCE_OUTPUT,
+ "android/processor/property_cache/test/CustomCache.java")
+ .hasSourceEquivalentTo(expectedJava);
+ }
+}
diff --git a/tools/processors/property_cache/test/java/android/processor/property_cache/shadows/IpcDataCache.java b/tools/processors/property_cache/test/java/android/processor/property_cache/shadows/IpcDataCache.java
new file mode 100644
index 0000000..e5ef48c
--- /dev/null
+++ b/tools/processors/property_cache/test/java/android/processor/property_cache/shadows/IpcDataCache.java
@@ -0,0 +1,134 @@
+/*
+ * 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 android.os;
+
+// Mocked class for generation compilation tests purposes only.
+public class IpcDataCache<Input, Output> {
+ public static class Config {
+ public Config(int max, String module, String api, String name) {
+ }
+ }
+
+ /** Shadow method for generated code compilation tests purposes only.
+ *
+ * @param query - shadow parameter from IpcDataCache in Frameworks.
+ * @return null
+ */
+ public Output query(Input query) {
+ return null;
+ }
+
+ /** Shadow method for generated code compilation tests purposes only.
+ *
+ * @param key - shadow parameter from IpcDataCache in Frameworks;
+ */
+ public static void invalidateCache(String key) {
+ }
+
+ /** Shadow method for generated code compilation tests purposes only.
+ *
+ * @param query - shadow parameter from IpcDataCache in Frameworks;
+ * @return null
+ */
+ public Output recompute(Input query) {
+ return null;
+ }
+
+ /** Shadow method for generated code compilation tests purposes only.
+ *
+ * @param query - parameter equivalent to IpcDataCache in android framework.
+ * @param query - shadow parameter from IpcDataCache in Frameworks;
+ * @return false
+ */
+ public boolean bypass(Input query) {
+ return false;
+ }
+
+ /** Shadow method for generated code compilation tests purposes only.
+ *
+ * @param module - parameter equivalent to IpcDataCache in android framework.
+ * @param key - parameter equivalent to IpcDataCache in android framework.
+ * @return module + key sttring
+ */
+ public static String createPropertyName(String module, String key) {
+ return module + key;
+ }
+
+ public abstract static class QueryHandler<Input, Output> {
+ /** Shadow method for generated code compilation tests purposes only.
+ *
+ * @param query - parameter equivalent to IpcDataCache.QueryHandler in android framework.
+ * @return expected value
+ */
+ public abstract Output apply(Input query);
+ /** Shadow method for generated code compilation tests purposes only.
+ *
+ * @param query - parameter equivalent to IpcDataCache.QueryHandler in android framework.
+ */
+ public boolean shouldBypassCache(Input query) {
+ return false;
+ }
+ }
+
+ public interface RemoteCall<Input, Output> {
+ /** Shadow method for generated code compilation tests purposes only.
+ *
+ * @param query - parameter equivalent to IpcDataCache.RemoteCall in android framework.
+ */
+ Output apply(Input query);
+ }
+
+ public interface BypassCall<Input> {
+ /** Shadow method for generated code compilation tests purposes only.
+ *
+ * @param query - parameter equivalent to IpcDataCache.BypassCall in android framework.
+ */
+ boolean apply(Input query);
+ }
+
+ public IpcDataCache(
+ int maxEntries,
+ String module,
+ String api,
+ String cacheName,
+ QueryHandler<Input, Output> computer) {
+ }
+
+ public IpcDataCache(Config config, QueryHandler<Input, Output> computer) {
+ }
+
+ public IpcDataCache(Config config, RemoteCall<Input, Output> computer) {
+ }
+
+ public IpcDataCache(Config config, RemoteCall<Input, Output> computer,
+ BypassCall<Input> bypassCall) {
+ }
+
+ /** Shadow method for generated code compilation tests purposes only.*/
+ public void invalidateCache() {
+ }
+
+
+ /** Shadow method for generated code compilation tests purposes only.
+ *
+ * @param module - shadow parameter from IpcDataCache in Frameworks.
+ * @param api - shadow parameter from IpcDataCache in Frameworks.
+ */
+ public static void invalidateCache(String module, String api) {
+ }
+
+}
diff --git a/tools/processors/property_cache/test/resources/Custom.java b/tools/processors/property_cache/test/resources/Custom.java
new file mode 100644
index 0000000..05024da
--- /dev/null
+++ b/tools/processors/property_cache/test/resources/Custom.java
@@ -0,0 +1,127 @@
+/*
+ * 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 android.processor.property_cache.test;
+
+import com.android.internal.annotations.CacheModifier;
+import com.android.internal.annotations.CachedProperty;
+import com.android.internal.annotations.CachedPropertyDefaults;
+
+import java.util.Date;
+
+@CachedPropertyDefaults(max = 4, module = "bluetooth")
+public class Custom {
+ BirthdayManagerService mService = new BirthdayManagerService();
+ Object mCache = new CustomCache();
+
+ public Custom() {
+ CustomCache.initCache();
+ }
+
+ /**
+ * Testing custom class values to generate static IpcDataCache
+ *
+ * @param userId - user Id
+ * @return birthday date of given user Id
+ */
+ @CachedProperty()
+ public Date getBirthday(int userId) {
+ return CustomCache.getBirthday(mService::getBirthday, userId);
+ }
+
+ /**
+ * Testing custom class values to generate static IpcDataCache
+ *
+ * @param userId - user Id
+ * @return number of days till birthday of given user Id
+ */
+ @CachedProperty(modsFlagOnOrNone = {CacheModifier.STATIC})
+ public int getDaysTillBirthday(int userId) {
+ return CustomCache.getDaysTillBirthday(mService::getDaysTillBirthday, userId);
+ }
+
+ /**
+ * Testing custom class values to generate non-static IpcDataCache
+ *
+ * @param userId - user Id
+ * @return number of days since birthday of given user Id
+ */
+ @CachedProperty(modsFlagOnOrNone = {})
+ public int getDaysSinceBirthday(int userId) {
+ return ((CustomCache) mCache).getDaysSinceBirthday(mService::getDaysSinceBirthday, userId);
+ }
+
+ /**
+ * Testing custom class values to generate static IpcDataCache with max capasity of 1
+ *
+ * @return number of days till birthay of current user
+ */
+ @CachedProperty(modsFlagOnOrNone = {CacheModifier.STATIC}, max = 1)
+ public int getDaysTillMyBirthday() {
+ return CustomCache.getDaysTillMyBirthday((Void) -> mService.getDaysTillMyBirthday());
+ }
+
+ /**
+ * Testing custom class values to generate static IpcDataCache with max capasity of 1 and custom
+ * api
+ *
+ * @return number of days since birthay of current user
+ */
+ @CachedProperty(modsFlagOnOrNone = {}, max = 1, api = "my_unique_key")
+ public int getDaysSinceMyBirthday() {
+ return ((CustomCache) mCache).getDaysSinceMyBirthday(
+ (Void) -> mService.getDaysSinceMyBirthday());
+ }
+
+ /**
+ * Testing custom class values to generate static IpcDataCache with custom module name
+ *
+ * @return birthday wishes of given user Id
+ */
+ @CachedProperty(module = "telephony")
+ public String getBirthdayWishesFromUser(int userId) {
+ return CustomCache.getBirthdayWishesFromUser(mService::getBirthdayWishesFromUser,
+ userId);
+ }
+
+ class BirthdayManagerService {
+ int mDaysTillBirthday = 182;
+
+ public Date getBirthday(int userId) {
+ return new Date(2024, 6, 1 + userId);
+ }
+
+ public int getDaysTillBirthday(int userId) {
+ return mDaysTillBirthday + userId;
+ }
+
+ public int getDaysSinceBirthday(int userId) {
+ return 365 - getDaysTillBirthday(userId);
+ }
+
+ public int getDaysTillMyBirthday() {
+ return 0;
+ }
+
+ public int getDaysSinceMyBirthday() {
+ return 365;
+ }
+
+ public String getBirthdayWishesFromUser(int userId) {
+ return "Happy Birthday!\n- " + userId;
+ }
+ }
+}
diff --git a/tools/processors/property_cache/test/resources/CustomCache.java b/tools/processors/property_cache/test/resources/CustomCache.java
new file mode 100644
index 0000000..326467f
--- /dev/null
+++ b/tools/processors/property_cache/test/resources/CustomCache.java
@@ -0,0 +1,384 @@
+/*
+ * 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 android.processor.property_cache.test;
+
+import android.os.IpcDataCache;
+
+/**
+ * This class is auto-generated
+ *
+ * @hide
+ **/
+public class CustomCache {
+ private static final Object sBirthdayLock = new Object();
+ private static IpcDataCache<java.lang.Integer, java.util.Date> sBirthday;
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link android.processor.property_cache.test.Custom#getBirthday }
+ * @param bypassPredicate - lambda to bypass remote call
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public static java.util.Date getBirthday(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.util.Date> binderCall,
+ IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query) {
+ if (sBirthday != null) {
+ return sBirthday.query(query);
+ }
+ synchronized (sBirthdayLock) {
+ if (sBirthday == null) {
+ sBirthday = new IpcDataCache(
+ new IpcDataCache.Config(4, "bluetooth", "custom_birthday", "Birthday"),
+ binderCall, bypassPredicate);
+
+ }
+ }
+ return sBirthday.query(query);
+ }
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link android.processor.property_cache.test.Custom#getBirthday }
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public static java.util.Date getBirthday(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.util.Date> binderCall,
+ java.lang.Integer query) {
+ if (sBirthday != null) {
+ return sBirthday.query(query);
+ }
+ synchronized (sBirthdayLock) {
+ if (sBirthday == null) {
+ sBirthday = new IpcDataCache(
+ new IpcDataCache.Config(4, "bluetooth", "custom_birthday", "Birthday"),
+ binderCall);
+ }
+ }
+ return sBirthday.query(query);
+ }
+
+ /**
+ * This method is auto-generated- invalidate cache for
+ * {@link android.processor.property_cache.test.Custom#getBirthday}
+ *
+ * @hide
+ */
+ public static final void invalidateBirthday() {
+ IpcDataCache.invalidateCache("bluetooth", "custom_birthday");
+ }
+
+ private static final Object sDaysTillBirthdayLock = new Object();
+ private static IpcDataCache<java.lang.Integer, java.lang.Integer> sDaysTillBirthday;
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link
+ * android.processor.property_cache.test.Custom#getDaysTillBirthday }
+ * @param bypassPredicate - lambda to bypass remote call
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public static java.lang.Integer getDaysTillBirthday(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+ IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query) {
+ if (sDaysTillBirthday != null) {
+ return sDaysTillBirthday.query(query);
+ }
+ synchronized (sDaysTillBirthdayLock) {
+ if (sDaysTillBirthday == null) {
+ sDaysTillBirthday = new IpcDataCache(
+ new IpcDataCache.Config(4, "bluetooth", "custom_days_till_birthday",
+ "DaysTillBirthday"), binderCall, bypassPredicate);
+
+ }
+ }
+ return sDaysTillBirthday.query(query);
+ }
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link android.processor.property_cache.test.Custom#getDaysTillBirthday }
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public static java.lang.Integer getDaysTillBirthday(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+ java.lang.Integer query) {
+ if (sDaysTillBirthday != null) {
+ return sDaysTillBirthday.query(query);
+ }
+ synchronized (sDaysTillBirthdayLock) {
+ if (sDaysTillBirthday == null) {
+ sDaysTillBirthday = new IpcDataCache(
+ new IpcDataCache.Config(4, "bluetooth", "custom_days_till_birthday",
+ "DaysTillBirthday"), binderCall);
+
+ }
+ }
+ return sDaysTillBirthday.query(query);
+ }
+
+ /**
+ * This method is auto-generated- invalidate cache for
+ * {@link android.processor.property_cache.test.Custom#getDaysTillBirthday}
+ *
+ * @hide
+ */
+ public static final void invalidateDaysTillBirthday() {
+ IpcDataCache.invalidateCache("bluetooth", "custom_days_till_birthday");
+ }
+
+ private final Object mDaysSinceBirthdayLock = new Object();
+ private IpcDataCache<java.lang.Integer, java.lang.Integer> mDaysSinceBirthday;
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link
+ * android.processor.property_cache.test.Custom#getDaysSinceBirthday }
+ * @param bypassPredicate - lambda to bypass remote call
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public java.lang.Integer getDaysSinceBirthday(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+ IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query) {
+ if (mDaysSinceBirthday != null) {
+ return mDaysSinceBirthday.query(query);
+ }
+ synchronized (mDaysSinceBirthdayLock) {
+ if (mDaysSinceBirthday == null) {
+ mDaysSinceBirthday = new IpcDataCache(
+ new IpcDataCache.Config(4, "bluetooth", "custom_days_since_birthday",
+ "DaysSinceBirthday"), binderCall, bypassPredicate);
+
+ }
+ }
+ return mDaysSinceBirthday.query(query);
+ }
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link android.processor.property_cache.test.Custom#getDaysSinceBirthday
+ * }
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public java.lang.Integer getDaysSinceBirthday(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+ java.lang.Integer query) {
+ if (mDaysSinceBirthday != null) {
+ return mDaysSinceBirthday.query(query);
+ }
+ synchronized (mDaysSinceBirthdayLock) {
+ if (mDaysSinceBirthday == null) {
+ mDaysSinceBirthday = new IpcDataCache(
+ new IpcDataCache.Config(4, "bluetooth", "custom_days_since_birthday",
+ "DaysSinceBirthday"), binderCall);
+
+ }
+ }
+ return mDaysSinceBirthday.query(query);
+ }
+
+ /**
+ * This method is auto-generated- invalidate cache for
+ * {@link android.processor.property_cache.test.Custom#getDaysSinceBirthday}
+ *
+ * @hide
+ */
+ public static final void invalidateDaysSinceBirthday() {
+ IpcDataCache.invalidateCache("bluetooth", "custom_days_since_birthday");
+ }
+
+ private static final Object sDaysTillMyBirthdayLock = new Object();
+ private static IpcDataCache<java.lang.Void, java.lang.Integer> sDaysTillMyBirthday;
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link android.processor.property_cache.test.Custom#getDaysTillMyBirthday
+ * }
+ * @hide
+ */
+ public static java.lang.Integer getDaysTillMyBirthday(
+ IpcDataCache.RemoteCall<java.lang.Void, java.lang.Integer> binderCall) {
+ if (sDaysTillMyBirthday != null) {
+ return sDaysTillMyBirthday.query(null);
+ }
+ synchronized (sDaysTillMyBirthdayLock) {
+ if (sDaysTillMyBirthday == null) {
+ sDaysTillMyBirthday = new IpcDataCache(
+ new IpcDataCache.Config(1, "bluetooth", "custom_days_till_my_birthday",
+ "DaysTillMyBirthday"), binderCall);
+
+ }
+ }
+ return sDaysTillMyBirthday.query(null);
+ }
+
+ /**
+ * This method is auto-generated- invalidate cache for
+ * {@link android.processor.property_cache.test.Custom#getDaysTillMyBirthday}
+ *
+ * @hide
+ */
+ public static final void invalidateDaysTillMyBirthday() {
+ IpcDataCache.invalidateCache("bluetooth", "custom_days_till_my_birthday");
+ }
+
+ private final Object mDaysSinceMyBirthdayLock = new Object();
+ private IpcDataCache<java.lang.Void, java.lang.Integer> mDaysSinceMyBirthday;
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link android.processor.property_cache.test.Custom#getDaysSinceMyBirthday
+ * }
+ * @hide
+ */
+ public java.lang.Integer getDaysSinceMyBirthday(
+ IpcDataCache.RemoteCall<java.lang.Void, java.lang.Integer> binderCall) {
+ if (mDaysSinceMyBirthday != null) {
+ return mDaysSinceMyBirthday.query(null);
+ }
+ synchronized (mDaysSinceMyBirthdayLock) {
+ if (mDaysSinceMyBirthday == null) {
+ mDaysSinceMyBirthday = new IpcDataCache(
+ new IpcDataCache.Config(1, "bluetooth", "my_unique_key",
+ "DaysSinceMyBirthday"), binderCall);
+
+ }
+ }
+ return mDaysSinceMyBirthday.query(null);
+ }
+
+ /**
+ * This method is auto-generated- invalidate cache for
+ * {@link android.processor.property_cache.test.Custom#getDaysSinceMyBirthday}
+ *
+ * @hide
+ */
+ public static final void invalidateDaysSinceMyBirthday() {
+ IpcDataCache.invalidateCache("bluetooth", "my_unique_key");
+ }
+
+ private static final Object sBirthdayWishesFromUserLock = new Object();
+ private static IpcDataCache<java.lang.Integer, java.lang.String> sBirthdayWishesFromUser;
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link
+ * android.processor.property_cache.test.Custom#getBirthdayWishesFromUser
+ * }
+ * @param bypassPredicate - lambda to bypass remote call
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public static java.lang.String getBirthdayWishesFromUser(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.lang.String> binderCall,
+ IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query) {
+ if (sBirthdayWishesFromUser != null) {
+ return sBirthdayWishesFromUser.query(query);
+ }
+ synchronized (sBirthdayWishesFromUserLock) {
+ if (sBirthdayWishesFromUser == null) {
+ sBirthdayWishesFromUser = new IpcDataCache(
+ new IpcDataCache.Config(4, "telephony", "custom_birthday_wishes_from_user",
+ "BirthdayWishesFromUser"), binderCall, bypassPredicate);
+
+ }
+ }
+ return sBirthdayWishesFromUser.query(query);
+ }
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link
+ * android.processor.property_cache.test.Custom#getBirthdayWishesFromUser }
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public static java.lang.String getBirthdayWishesFromUser(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.lang.String> binderCall,
+ java.lang.Integer query) {
+ if (sBirthdayWishesFromUser != null) {
+ return sBirthdayWishesFromUser.query(query);
+ }
+ synchronized (sBirthdayWishesFromUserLock) {
+ if (sBirthdayWishesFromUser == null) {
+ sBirthdayWishesFromUser = new IpcDataCache(
+ new IpcDataCache.Config(4, "telephony", "custom_birthday_wishes_from_user",
+ "BirthdayWishesFromUser"), binderCall);
+
+ }
+ }
+ return sBirthdayWishesFromUser.query(query);
+ }
+
+ /**
+ * This method is auto-generated- invalidate cache for
+ * {@link android.processor.property_cache.test.Custom#getBirthdayWishesFromUser}
+ *
+ * @hide
+ */
+ public static final void invalidateBirthdayWishesFromUser() {
+ IpcDataCache.invalidateCache("telephony", "custom_birthday_wishes_from_user");
+ }
+
+
+ /**
+ * This method is auto-generated - initialise all caches for class CustomCache
+ *
+ * @hide
+ */
+ public static void initCache() {
+ CustomCache.invalidateBirthday();
+ CustomCache.invalidateDaysTillBirthday();
+ CustomCache.invalidateDaysSinceBirthday();
+ CustomCache.invalidateDaysTillMyBirthday();
+ CustomCache.invalidateDaysSinceMyBirthday();
+ CustomCache.invalidateBirthdayWishesFromUser();
+ }
+}
diff --git a/tools/processors/property_cache/test/resources/Default.java b/tools/processors/property_cache/test/resources/Default.java
new file mode 100644
index 0000000..d2449aa
--- /dev/null
+++ b/tools/processors/property_cache/test/resources/Default.java
@@ -0,0 +1,125 @@
+/*
+ * 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 android.processor.property_cache.test;
+
+import com.android.internal.annotations.CacheModifier;
+import com.android.internal.annotations.CachedProperty;
+import com.android.internal.annotations.CachedPropertyDefaults;
+
+import java.util.Date;
+
+@CachedPropertyDefaults()
+public class Default {
+ BirthdayManagerService mService = new BirthdayManagerService();
+ Object mCache = new DefaultCache();
+
+ /** Testing default class values to generate static IpcDataCache
+ *
+ * @param userId - user Id
+ * @return birthday date of given user Id
+ */
+ @CachedProperty()
+ public Date getBirthday(int userId) {
+ return DefaultCache.getBirthday(mService::getBirthday, userId);
+ }
+
+ /** Testing default class values to generate static IpcDataCache
+ *
+ * @param userId - user Id
+ * @return number of days till birthday of given user Id
+ */
+ @CachedProperty(modsFlagOnOrNone = {CacheModifier.STATIC})
+ public int getDaysTillBirthday(int userId) {
+ return DefaultCache.getDaysTillBirthday(mService::getDaysTillBirthday, userId);
+ }
+
+ /** Testing generate non-static IpcDataCache
+ *
+ * @param userId - user Id
+ * @return number of days since birthday of given user Id
+ */
+ @CachedProperty(modsFlagOnOrNone = {})
+ public int getDaysSinceBirthday(int userId) {
+ return ((DefaultCache) mCache).getDaysSinceBirthday(mService::getDaysSinceBirthday, userId);
+ }
+
+ /** Testing default class values to generate static IpcDataCache with max capacity of 1
+ *
+ * @return number of days till birthay of current user
+ */
+ @CachedProperty(
+ modsFlagOnOrNone = {CacheModifier.STATIC},
+ max = 1)
+ public int getDaysTillMyBirthday() {
+ return DefaultCache.getDaysTillMyBirthday((Void) -> mService.getDaysTillMyBirthday());
+ }
+
+ /** Testing default class values to generate static IpcDataCache with max capacity of 1 and
+ custom api
+ *
+ * @return number of days since birthay of current user
+ */
+ @CachedProperty(
+ modsFlagOnOrNone = {},
+ max = 1,
+ api = "my_unique_key")
+ public int getDaysSinceMyBirthday() {
+ return ((DefaultCache) mCache).getDaysSinceMyBirthday(
+ (Void) -> mService.getDaysSinceMyBirthday());
+ }
+
+ /** Testing default class values to generate static IpcDataCache with custom module name
+ *
+ * @return birthday wishes of given user Id
+ */
+ @CachedProperty(module = "telephony")
+ public String getBirthdayWishesFromUser(int userId) {
+ return DefaultCache.getBirthdayWishesFromUser(mService::getBirthdayWishesFromUser,
+ userId);
+ }
+
+ class BirthdayManagerService {
+
+ BirthdayManagerService() {
+ DefaultCache.initCache();
+ }
+
+ public Date getBirthday(int userId) {
+ return new Date();
+ }
+
+ public int getDaysTillBirthday(int userId) {
+ return 0;
+ }
+
+ public int getDaysSinceBirthday(int userId) {
+ return 0;
+ }
+
+ public int getDaysTillMyBirthday() {
+ return 0;
+ }
+
+ public int getDaysSinceMyBirthday() {
+ return 0;
+ }
+
+ public String getBirthdayWishesFromUser(int userId) {
+ return "Happy Birthday!\n- " + userId;
+ }
+ }
+}
diff --git a/tools/processors/property_cache/test/resources/DefaultCache.java b/tools/processors/property_cache/test/resources/DefaultCache.java
new file mode 100644
index 0000000..9531118
--- /dev/null
+++ b/tools/processors/property_cache/test/resources/DefaultCache.java
@@ -0,0 +1,402 @@
+/*
+ * 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 android.processor.property_cache.test;
+
+import android.os.IpcDataCache;
+
+/**
+ * This class is auto-generated
+ *
+ * @hide
+ **/
+public class DefaultCache {
+ private static final Object sBirthdayLock = new Object();
+ private static IpcDataCache<java.lang.Integer, java.util.Date> sBirthday;
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link android.processor.property_cache.test.Default#getBirthday }
+ * @param bypassPredicate - lambda to bypass remote call
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public static java.util.Date getBirthday(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.util.Date> binderCall,
+ IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query
+ ) {
+ if (sBirthday != null) {
+ return sBirthday.query(query);
+ }
+ synchronized (sBirthdayLock) {
+ if (sBirthday == null) {
+ sBirthday = new IpcDataCache(
+ new IpcDataCache.Config(32, "system_server",
+ "default_birthday", "Birthday"),
+ binderCall, bypassPredicate);
+
+ }
+ }
+ return sBirthday.query(query);
+ }
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link android.processor.property_cache.test.Default#getBirthday }
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public static java.util.Date getBirthday(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.util.Date> binderCall,
+ java.lang.Integer query
+ ) {
+ if (sBirthday != null) {
+ return sBirthday.query(query);
+ }
+ synchronized (sBirthdayLock) {
+ if (sBirthday == null) {
+ sBirthday = new IpcDataCache(
+ new IpcDataCache.Config(32, "system_server",
+ "default_birthday", "Birthday"),
+ binderCall);
+
+ }
+ }
+ return sBirthday.query(query);
+ }
+
+ /**
+ * This method is auto-generated- invalidate cache for
+ * {@link android.processor.property_cache.test.Default#getBirthday}
+ *
+ * @hide
+ */
+ public static final void invalidateBirthday() {
+ IpcDataCache.invalidateCache("system_server", "default_birthday");
+ }
+
+ private static final Object sDaysTillBirthdayLock = new Object();
+ private static IpcDataCache<java.lang.Integer, java.lang.Integer> sDaysTillBirthday;
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link
+ * android.processor.property_cache.test.Default#getDaysTillBirthday }
+ * @param bypassPredicate - lambda to bypass remote call
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public static java.lang.Integer getDaysTillBirthday(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+ IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query
+ ) {
+ if (sDaysTillBirthday != null) {
+ return sDaysTillBirthday.query(query);
+ }
+ synchronized (sDaysTillBirthdayLock) {
+ if (sDaysTillBirthday == null) {
+ sDaysTillBirthday = new IpcDataCache(
+ new IpcDataCache.Config(32, "system_server", "default_days_till_birthday",
+ "DaysTillBirthday"), binderCall, bypassPredicate);
+
+ }
+ }
+ return sDaysTillBirthday.query(query);
+ }
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link android.processor.property_cache.test.Default#getDaysTillBirthday
+ * }
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public static java.lang.Integer getDaysTillBirthday(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+ java.lang.Integer query
+ ) {
+ if (sDaysTillBirthday != null) {
+ return sDaysTillBirthday.query(query);
+ }
+ synchronized (sDaysTillBirthdayLock) {
+ if (sDaysTillBirthday == null) {
+ sDaysTillBirthday = new IpcDataCache(
+ new IpcDataCache.Config(32, "system_server", "default_days_till_birthday",
+ "DaysTillBirthday"), binderCall);
+
+ }
+ }
+ return sDaysTillBirthday.query(query);
+ }
+
+ /**
+ * This method is auto-generated- invalidate cache for
+ * {@link android.processor.property_cache.test.Default#getDaysTillBirthday}
+ *
+ * @hide
+ */
+ public static final void invalidateDaysTillBirthday() {
+ IpcDataCache.invalidateCache("system_server", "default_days_till_birthday");
+ }
+
+ private final Object mDaysSinceBirthdayLock = new Object();
+ private IpcDataCache<java.lang.Integer, java.lang.Integer> mDaysSinceBirthday;
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link
+ * android.processor.property_cache.test.Default#getDaysSinceBirthday }
+ * @param bypassPredicate - lambda to bypass remote call
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public java.lang.Integer getDaysSinceBirthday(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+ IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query
+ ) {
+ if (mDaysSinceBirthday != null) {
+ return mDaysSinceBirthday.query(query);
+ }
+ synchronized (mDaysSinceBirthdayLock) {
+ if (mDaysSinceBirthday == null) {
+ mDaysSinceBirthday = new IpcDataCache(
+ new IpcDataCache.Config(32, "system_server", "default_days_since_birthday",
+ "DaysSinceBirthday"), binderCall, bypassPredicate);
+
+ }
+ }
+ return mDaysSinceBirthday.query(query);
+ }
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link android.processor.property_cache.test.Default#getDaysSinceBirthday
+ * }
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public java.lang.Integer getDaysSinceBirthday(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+ java.lang.Integer query
+ ) {
+ if (mDaysSinceBirthday != null) {
+ return mDaysSinceBirthday.query(query);
+ }
+ synchronized (mDaysSinceBirthdayLock) {
+ if (mDaysSinceBirthday == null) {
+ mDaysSinceBirthday = new IpcDataCache(
+ new IpcDataCache.Config(32, "system_server", "default_days_since_birthday",
+ "DaysSinceBirthday"), binderCall);
+
+ }
+ }
+ return mDaysSinceBirthday.query(query);
+ }
+
+ /**
+ * This method is auto-generated- invalidate cache for
+ * {@link android.processor.property_cache.test.Default#getDaysSinceBirthday}
+ *
+ * @hide
+ */
+ public static final void invalidateDaysSinceBirthday() {
+ IpcDataCache.invalidateCache("system_server", "default_days_since_birthday");
+ }
+
+ private static final Object sDaysTillMyBirthdayLock = new Object();
+ private static IpcDataCache<java.lang.Void, java.lang.Integer> sDaysTillMyBirthday;
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link android.processor.property_cache.test.Default#getDaysTillMyBirthday
+ * }
+ * @hide
+ */
+ public static java.lang.Integer getDaysTillMyBirthday(
+ IpcDataCache.RemoteCall<java.lang.Void, java.lang.Integer> binderCall
+ ) {
+ if (sDaysTillMyBirthday != null) {
+ return sDaysTillMyBirthday.query(null);
+ }
+ synchronized (sDaysTillMyBirthdayLock) {
+ if (sDaysTillMyBirthday == null) {
+ sDaysTillMyBirthday = new IpcDataCache(
+ new IpcDataCache.Config(1, "system_server", "default_days_till_my_birthday",
+ "DaysTillMyBirthday"), binderCall);
+
+ }
+ }
+ return sDaysTillMyBirthday.query(null);
+ }
+
+ /**
+ * This method is auto-generated- invalidate cache for
+ * {@link android.processor.property_cache.test.Default#getDaysTillMyBirthday}
+ *
+ * @hide
+ */
+ public static final void invalidateDaysTillMyBirthday() {
+ IpcDataCache.invalidateCache("system_server", "default_days_till_my_birthday");
+ }
+
+ private final Object mDaysSinceMyBirthdayLock = new Object();
+ private IpcDataCache<java.lang.Void, java.lang.Integer> mDaysSinceMyBirthday;
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link
+ * android.processor.property_cache.test.Default#getDaysSinceMyBirthday }
+ * @hide
+ */
+ public java.lang.Integer getDaysSinceMyBirthday(
+ IpcDataCache.RemoteCall<java.lang.Void, java.lang.Integer> binderCall
+ ) {
+ if (mDaysSinceMyBirthday != null) {
+ return mDaysSinceMyBirthday.query(null);
+ }
+ synchronized (mDaysSinceMyBirthdayLock) {
+ if (mDaysSinceMyBirthday == null) {
+ mDaysSinceMyBirthday = new IpcDataCache(
+ new IpcDataCache.Config(1, "system_server", "my_unique_key",
+ "DaysSinceMyBirthday"), binderCall);
+
+ }
+ }
+ return mDaysSinceMyBirthday.query(null);
+ }
+
+ /**
+ * This method is auto-generated- invalidate cache for
+ * {@link android.processor.property_cache.test.Default#getDaysSinceMyBirthday}
+ *
+ * @hide
+ */
+ public static final void invalidateDaysSinceMyBirthday() {
+ IpcDataCache.invalidateCache("system_server", "my_unique_key");
+ }
+
+ private static final Object sBirthdayWishesFromUserLock = new Object();
+ private static IpcDataCache<java.lang.Integer, java.lang.String> sBirthdayWishesFromUser;
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link
+ *
+ * android.processor.property_cache.test.Default#getBirthdayWishesFromUser
+ * }
+ * @param bypassPredicate - lambda to bypass remote call
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public static java.lang.String getBirthdayWishesFromUser(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.lang.String> binderCall,
+ IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query
+ ) {
+ if (sBirthdayWishesFromUser != null) {
+ return sBirthdayWishesFromUser.query(query);
+ }
+ synchronized (sBirthdayWishesFromUserLock) {
+ if (sBirthdayWishesFromUser == null) {
+ sBirthdayWishesFromUser = new IpcDataCache(
+ new IpcDataCache.Config(32, "telephony",
+ "default_birthday_wishes_from_user",
+ "BirthdayWishesFromUser"), binderCall, bypassPredicate);
+
+ }
+ }
+ return sBirthdayWishesFromUser.query(query);
+ }
+
+
+ /**
+ * This method is auto-generated
+ *
+ * @param binderCall - lambda for remote call
+ * {@link
+ * android.processor.property_cache.test.Default#getBirthdayWishesFromUser }
+ * @param query - parameter to call remote lambda
+ * @hide
+ */
+ public static java.lang.String getBirthdayWishesFromUser(
+ IpcDataCache.RemoteCall<java.lang.Integer, java.lang.String> binderCall,
+ java.lang.Integer query
+ ) {
+ if (sBirthdayWishesFromUser != null) {
+ return sBirthdayWishesFromUser.query(query);
+ }
+ synchronized (sBirthdayWishesFromUserLock) {
+ if (sBirthdayWishesFromUser == null) {
+ sBirthdayWishesFromUser = new IpcDataCache(
+ new IpcDataCache.Config(32, "telephony",
+ "default_birthday_wishes_from_user",
+ "BirthdayWishesFromUser"), binderCall);
+
+ }
+ }
+ return sBirthdayWishesFromUser.query(query);
+ }
+
+ /**
+ * This method is auto-generated- invalidate cache for
+ * {@link android.processor.property_cache.test.Default#getBirthdayWishesFromUser}
+ *
+ * @hide
+ */
+ public static final void invalidateBirthdayWishesFromUser() {
+ IpcDataCache.invalidateCache("telephony", "default_birthday_wishes_from_user");
+ }
+
+
+ /**
+ * This method is auto-generated - initialise all caches for class DefaultCache
+ *
+ * @hide
+ */
+ public static void initCache() {
+ DefaultCache.invalidateBirthday();
+ DefaultCache.invalidateDaysTillBirthday();
+ DefaultCache.invalidateDaysSinceBirthday();
+ DefaultCache.invalidateDaysTillMyBirthday();
+ DefaultCache.invalidateDaysSinceMyBirthday();
+ DefaultCache.invalidateBirthdayWishesFromUser();
+ }
+}