Merge "Add flag for Preference Service Consent dialog" into main
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index 14e2387..b43905b 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -451,7 +451,7 @@
auto token = SurfaceComposerClient::getPhysicalDisplayToken(
event.header.displayId);
- auto firstDisplay = mBootAnimation->mDisplays.front();
+ auto& firstDisplay = mBootAnimation->mDisplays.front();
if (token != firstDisplay.displayToken) {
// ignore hotplug of a secondary display
continue;
diff --git a/core/api/current.txt b/core/api/current.txt
index ea6d1a5..d4d4b24 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -37819,6 +37819,7 @@
field public static final String ACTION_DISPLAY_SETTINGS = "android.settings.DISPLAY_SETTINGS";
field public static final String ACTION_DREAM_SETTINGS = "android.settings.DREAM_SETTINGS";
field @Deprecated public static final String ACTION_FINGERPRINT_ENROLL = "android.settings.FINGERPRINT_ENROLL";
+ field @FlaggedApi("android.provider.system_regional_preferences_api_enabled") public static final String ACTION_FIRST_DAY_OF_WEEK_SETTINGS = "android.settings.FIRST_DAY_OF_WEEK_SETTINGS";
field public static final String ACTION_HARD_KEYBOARD_SETTINGS = "android.settings.HARD_KEYBOARD_SETTINGS";
field public static final String ACTION_HOME_SETTINGS = "android.settings.HOME_SETTINGS";
field public static final String ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS = "android.settings.IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS";
@@ -37839,6 +37840,7 @@
field public static final String ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING = "android.settings.MANAGE_SUPERVISOR_RESTRICTED_SETTING";
field public static final String ACTION_MANAGE_UNKNOWN_APP_SOURCES = "android.settings.MANAGE_UNKNOWN_APP_SOURCES";
field public static final String ACTION_MANAGE_WRITE_SETTINGS = "android.settings.action.MANAGE_WRITE_SETTINGS";
+ field @FlaggedApi("android.provider.system_regional_preferences_api_enabled") public static final String ACTION_MEASUREMENT_SYSTEM_SETTINGS = "android.settings.MEASUREMENT_SYSTEM_SETTINGS";
field public static final String ACTION_MEMORY_CARD_SETTINGS = "android.settings.MEMORY_CARD_SETTINGS";
field public static final String ACTION_NETWORK_OPERATOR_SETTINGS = "android.settings.NETWORK_OPERATOR_SETTINGS";
field public static final String ACTION_NFCSHARING_SETTINGS = "android.settings.NFCSHARING_SETTINGS";
@@ -37849,12 +37851,14 @@
field public static final String ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS = "android.settings.NOTIFICATION_LISTENER_DETAIL_SETTINGS";
field public static final String ACTION_NOTIFICATION_LISTENER_SETTINGS = "android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS";
field public static final String ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS = "android.settings.NOTIFICATION_POLICY_ACCESS_SETTINGS";
+ field @FlaggedApi("android.provider.system_regional_preferences_api_enabled") public static final String ACTION_NUMBERING_SYSTEM_SETTINGS = "android.settings.NUMBERING_SYSTEM_SETTINGS";
field public static final String ACTION_PRINT_SETTINGS = "android.settings.ACTION_PRINT_SETTINGS";
field public static final String ACTION_PRIVACY_SETTINGS = "android.settings.PRIVACY_SETTINGS";
field public static final String ACTION_PROCESS_WIFI_EASY_CONNECT_URI = "android.settings.PROCESS_WIFI_EASY_CONNECT_URI";
field public static final String ACTION_QUICK_ACCESS_WALLET_SETTINGS = "android.settings.QUICK_ACCESS_WALLET_SETTINGS";
field public static final String ACTION_QUICK_LAUNCH_SETTINGS = "android.settings.QUICK_LAUNCH_SETTINGS";
field public static final String ACTION_REGIONAL_PREFERENCES_SETTINGS = "android.settings.REGIONAL_PREFERENCES_SETTINGS";
+ field @FlaggedApi("android.provider.system_regional_preferences_api_enabled") public static final String ACTION_REGION_SETTINGS = "android.settings.REGION_SETTINGS";
field public static final String ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = "android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
field public static final String ACTION_REQUEST_MANAGE_MEDIA = "android.settings.REQUEST_MANAGE_MEDIA";
field @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") public static final String ACTION_REQUEST_MEDIA_ROUTING_CONTROL = "android.settings.REQUEST_MEDIA_ROUTING_CONTROL";
@@ -37870,6 +37874,7 @@
field public static final String ACTION_SOUND_SETTINGS = "android.settings.SOUND_SETTINGS";
field @Deprecated public static final String ACTION_STORAGE_VOLUME_ACCESS_SETTINGS = "android.settings.STORAGE_VOLUME_ACCESS_SETTINGS";
field public static final String ACTION_SYNC_SETTINGS = "android.settings.SYNC_SETTINGS";
+ field @FlaggedApi("android.provider.system_regional_preferences_api_enabled") public static final String ACTION_TEMPERATURE_UNIT_SETTINGS = "android.settings.TEMPERATURE_UNIT_SETTINGS";
field public static final String ACTION_USAGE_ACCESS_SETTINGS = "android.settings.USAGE_ACCESS_SETTINGS";
field public static final String ACTION_USER_DICTIONARY_SETTINGS = "android.settings.USER_DICTIONARY_SETTINGS";
field public static final String ACTION_VOICE_CONTROL_AIRPLANE_MODE = "android.settings.VOICE_CONTROL_AIRPLANE_MODE";
@@ -54043,6 +54048,7 @@
field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_GLOBAL_SAME_APPLICATION = 4096; // 0x1000
field public static final int DRAG_FLAG_GLOBAL_URI_READ = 1; // 0x1
field public static final int DRAG_FLAG_GLOBAL_URI_WRITE = 2; // 0x2
+ field @FlaggedApi("com.android.window.flags.supports_drag_assistant_to_multiwindow") public static final int DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START = 16384; // 0x4000
field public static final int DRAG_FLAG_OPAQUE = 512; // 0x200
field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG = 8192; // 0x2000
field @Deprecated public static final int DRAWING_CACHE_QUALITY_AUTO = 0; // 0x0
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 0e521c6..f9ef62f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -709,8 +709,8 @@
field public static final String OPSTR_READ_MEDIA_IMAGES = "android:read_media_images";
field public static final String OPSTR_READ_MEDIA_VIDEO = "android:read_media_video";
field public static final String OPSTR_READ_MEDIA_VISUAL_USER_SELECTED = "android:read_media_visual_user_selected";
- field @FlaggedApi("android.permission.flags.platform_oxygen_saturation_enabled") public static final String OPSTR_READ_OXYGEN_SATURATION = "android:read_oxygen_saturation";
- field @FlaggedApi("android.permission.flags.platform_skin_temperature_enabled") public static final String OPSTR_READ_SKIN_TEMPERATURE = "android:read_skin_temperature";
+ field @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") public static final String OPSTR_READ_OXYGEN_SATURATION = "android:read_oxygen_saturation";
+ field @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") public static final String OPSTR_READ_SKIN_TEMPERATURE = "android:read_skin_temperature";
field public static final String OPSTR_READ_WRITE_HEALTH_DATA = "android:read_write_health_data";
field public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO = "android:receive_ambient_trigger_audio";
field public static final String OPSTR_RECEIVE_EMERGENCY_BROADCAST = "android:receive_emergency_broadcast";
@@ -1368,6 +1368,7 @@
method @RequiresPermission("android.permission.NOTIFY_PENDING_SYSTEM_UPDATE") public void notifyPendingSystemUpdate(long, boolean);
method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public boolean packageHasActiveAdmins(String);
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, android.Manifest.permission.PROVISION_DEMO_DEVICE}) public void provisionFullyManagedDevice(@NonNull android.app.admin.FullyManagedDeviceProvisioningParams) throws android.app.admin.ProvisioningException;
+ method @FlaggedApi("android.app.admin.flags.remove_managed_profile_enabled") @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean removeManagedProfile();
method @RequiresPermission(android.Manifest.permission.TRIGGER_LOST_MODE) public void sendLostModeLocationUpdate(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>);
method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_ADMINS) public boolean setActiveProfileOwner(@NonNull android.content.ComponentName, String) throws java.lang.IllegalArgumentException;
method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS) public void setApplicationExemptions(@NonNull String, @NonNull java.util.Set<java.lang.Integer>) throws android.content.pm.PackageManager.NameNotFoundException;
@@ -18794,6 +18795,10 @@
method @Deprecated public void openFileChooser(android.webkit.ValueCallback<android.net.Uri>, String, String);
}
+ public abstract static class WebChromeClient.FileChooserParams {
+ field @FlaggedApi("android.webkit.file_system_access") public static final long ENABLE_FILE_SYSTEM_ACCESS = 364980165L; // 0x15c127c5L
+ }
+
public abstract class WebHistoryItem implements java.lang.Cloneable {
method @Deprecated public abstract int getId();
}
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index fd70f4f..8b37dbd 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -2527,12 +2527,12 @@
/** @hide Access to read oxygen saturation. */
@SystemApi
- @FlaggedApi(Flags.FLAG_PLATFORM_OXYGEN_SATURATION_ENABLED)
+ @FlaggedApi(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
public static final String OPSTR_READ_OXYGEN_SATURATION = "android:read_oxygen_saturation";
/** @hide Access to read skin temperature. */
@SystemApi
- @FlaggedApi(Flags.FLAG_PLATFORM_SKIN_TEMPERATURE_ENABLED)
+ @FlaggedApi(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
public static final String OPSTR_READ_SKIN_TEMPERATURE = "android:read_skin_temperature";
/** @hide Access to ranging */
@@ -2616,8 +2616,8 @@
OP_POST_NOTIFICATION,
// Health
Flags.replaceBodySensorPermissionEnabled() ? OP_READ_HEART_RATE : OP_NONE,
- Flags.platformSkinTemperatureEnabled() ? OP_READ_SKIN_TEMPERATURE : OP_NONE,
- Flags.platformOxygenSaturationEnabled() ? OP_READ_OXYGEN_SATURATION : OP_NONE,
+ Flags.replaceBodySensorPermissionEnabled() ? OP_READ_SKIN_TEMPERATURE : OP_NONE,
+ Flags.replaceBodySensorPermissionEnabled() ? OP_READ_OXYGEN_SATURATION : OP_NONE,
};
/**
@@ -3132,7 +3132,7 @@
.setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
new AppOpInfo.Builder(OP_READ_SKIN_TEMPERATURE, OPSTR_READ_SKIN_TEMPERATURE,
"READ_SKIN_TEMPERATURE").setPermission(
- Flags.platformSkinTemperatureEnabled()
+ Flags.replaceBodySensorPermissionEnabled()
? HealthPermissions.READ_SKIN_TEMPERATURE : null)
.setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
new AppOpInfo.Builder(OP_RANGING, OPSTR_RANGING, "RANGING")
@@ -3141,7 +3141,7 @@
.setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
new AppOpInfo.Builder(OP_READ_OXYGEN_SATURATION, OPSTR_READ_OXYGEN_SATURATION,
"READ_OXYGEN_SATURATION").setPermission(
- Flags.platformOxygenSaturationEnabled()
+ Flags.replaceBodySensorPermissionEnabled()
? HealthPermissions.READ_OXYGEN_SATURATION : null)
.setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
};
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 102540c..707ba34 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -55,6 +55,7 @@
import static android.Manifest.permission.SET_TIME_ZONE;
import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
import static android.app.admin.flags.Flags.FLAG_DEVICE_THEFT_API_ENABLED;
+import static android.app.admin.flags.Flags.FLAG_REMOVE_MANAGED_PROFILE_ENABLED;
import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled;
import static android.app.admin.flags.Flags.onboardingConsentlessBugreports;
import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM;
@@ -16962,6 +16963,30 @@
}
/**
+ * Removes a manged profile from the device only when called from a managed profile's context
+ *
+ * @param user UserHandle of the profile to be removed
+ * @return {@code true} when removal of managed profile was successful, {@code false} when
+ * removal was unsuccessful or throws IllegalArgumentException when provided user was not a
+ * managed profile
+ * @hide
+ */
+ @SystemApi
+ @UserHandleAware
+ @FlaggedApi(FLAG_REMOVE_MANAGED_PROFILE_ENABLED)
+ @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS)
+ public boolean removeManagedProfile() {
+ if (mService == null) {
+ throw new IllegalStateException("Could not find DevicePolicyManagerService");
+ }
+ try {
+ return mService.removeManagedProfile(myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Called when a managed profile has been provisioned.
*
* @throws SecurityException if the caller does not hold
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index a4e2b8f..ba97edb 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -567,6 +567,8 @@
void finalizeWorkProfileProvisioning(in UserHandle managedProfileUser, in Account migratedAccount);
+ boolean removeManagedProfile(int userId);
+
void setDeviceOwnerType(in ComponentName admin, in int deviceOwnerType);
int getDeviceOwnerType(in ComponentName admin);
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index be24bfa..5f868be 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -343,13 +343,20 @@
}
flag {
- name: "user_provisioning_same_state"
- namespace: "enterprise"
- description: "Handle exceptions while setting same provisioning state."
- bug: "326441417"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
+ name: "user_provisioning_same_state"
+ namespace: "enterprise"
+ description: "Handle exceptions while setting same provisioning state."
+ bug: "326441417"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "remove_managed_profile_enabled"
+ namespace: "enterprise"
+ description: "API that removes a given managed profile."
+ bug: "372652841"
}
flag {
diff --git a/core/java/android/app/wallpaper/WallpaperDescription.java b/core/java/android/app/wallpaper/WallpaperDescription.java
index 8ffda72..4a142bb 100644
--- a/core/java/android/app/wallpaper/WallpaperDescription.java
+++ b/core/java/android/app/wallpaper/WallpaperDescription.java
@@ -113,7 +113,7 @@
/** @return the description for this wallpaper */
@NonNull
public List<CharSequence> getDescription() {
- return new ArrayList<>();
+ return mDescription;
}
/** @return the {@link Uri} for the action associated with the wallpaper, or {@code null} if not
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index b4b5a7f..9ba5a35 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -359,3 +359,10 @@
bug: "377474232"
is_fixed_read_only: true
}
+
+flag {
+ name: "support_minor_versions_in_minsdkversion"
+ namespace: "package_manager_service"
+ description: "Block app installations that specify an incompatible minor SDK version"
+ bug: "377474232"
+}
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 96f6ad1..71b60cf 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -31,6 +31,7 @@
import static com.android.hardware.input.Flags.touchpadThreeFingerTapShortcut;
import static com.android.hardware.input.Flags.touchpadVisualizer;
import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
+import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
import static com.android.input.flags.Flags.FLAG_KEYBOARD_REPEAT_KEYS;
import static com.android.input.flags.Flags.enableInputFilterRustImpl;
import static com.android.input.flags.Flags.keyboardRepeatKeys;
@@ -1147,4 +1148,13 @@
public static boolean isCustomizableInputGesturesFeatureFlagEnabled() {
return enableCustomizableInputGestures() && useKeyGestureEventHandler();
}
+
+ /**
+ * Whether multi-key gestures are supported using {@code KeyGestureEventHandler}
+ *
+ * @hide
+ */
+ public static boolean doesKeyGestureEventHandlerSupportMultiKeyGestures() {
+ return useKeyGestureEventHandler() && useKeyGestureEventHandlerMultiPressGestures();
+ }
}
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 455b469..33040be 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -284,7 +284,7 @@
is_fixed_read_only: true
is_exported: true
namespace: "android_health_services"
- description: "This fixed read-only flag is used to enable replacing permission BODY_SENSORS (and BODY_SENSORS_BACKGROUND) with granular health permission READ_HEART_RATE (and READ_HEALTH_DATA_IN_BACKGROUND)"
+ description: "Enables replacement of BODY_SENSORS/BODY_SENSORS_BACKGROUND permissions with granular health permissions READ_HEART_RATE, READ_SKIN_TEMPERATURE, READ_OXYGEN_SATURATION, and READ_HEALTH_DATA_IN_BACKGROUND"
bug: "364638912"
}
@@ -300,24 +300,6 @@
}
flag {
- name: "platform_skin_temperature_enabled"
- is_fixed_read_only: true
- is_exported: true
- namespace: "android_health_services"
- description: "This fixed read-only flag is used to enable platform support for Skin Temperature."
- bug: "369872443"
-}
-
-flag {
- name: "platform_oxygen_saturation_enabled"
- is_fixed_read_only: true
- is_exported: true
- namespace: "android_health_services"
- description: "This fixed read-only flag is used to enable platform support for Oxygen Saturation (SpO2)."
- bug: "369873227"
-}
-
-flag {
name: "allow_host_permission_dialogs_on_virtual_devices"
is_exported: true
namespace: "permissions"
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ef35171..d2a20b6 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -1217,6 +1217,69 @@
"android.settings.REGIONAL_PREFERENCES_SETTINGS";
/**
+ * Activity Action: Show screen for allowing the region configuration.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @FlaggedApi(Flags.FLAG_SYSTEM_REGIONAL_PREFERENCES_API_ENABLED)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_REGION_SETTINGS =
+ "android.settings.REGION_SETTINGS";
+
+ /**
+ * Activity Action: Show first day of week configuration settings.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @FlaggedApi(Flags.FLAG_SYSTEM_REGIONAL_PREFERENCES_API_ENABLED)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_FIRST_DAY_OF_WEEK_SETTINGS =
+ "android.settings.FIRST_DAY_OF_WEEK_SETTINGS";
+
+ /**
+ * Activity Action: Show temperature unit configuration settings.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @FlaggedApi(Flags.FLAG_SYSTEM_REGIONAL_PREFERENCES_API_ENABLED)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_TEMPERATURE_UNIT_SETTINGS =
+ "android.settings.TEMPERATURE_UNIT_SETTINGS";
+
+ /**
+ * Activity Action: Show numbering system configuration settings.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @FlaggedApi(Flags.FLAG_SYSTEM_REGIONAL_PREFERENCES_API_ENABLED)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_NUMBERING_SYSTEM_SETTINGS =
+ "android.settings.NUMBERING_SYSTEM_SETTINGS";
+
+ /**
+ * Activity Action: Show measurement system configuration settings.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @FlaggedApi(Flags.FLAG_SYSTEM_REGIONAL_PREFERENCES_API_ENABLED)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_MEASUREMENT_SYSTEM_SETTINGS =
+ "android.settings.MEASUREMENT_SYSTEM_SETTINGS";
+
+ /**
* Activity Action: Show settings to allow configuration of lockscreen.
* <p>
* In some cases, a matching Activity may not exist, so ensure you
diff --git a/core/java/android/provider/flags.aconfig b/core/java/android/provider/flags.aconfig
index 4c63673..fff5363 100644
--- a/core/java/android/provider/flags.aconfig
+++ b/core/java/android/provider/flags.aconfig
@@ -63,3 +63,11 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "system_regional_preferences_api_enabled"
+ is_exported: true
+ namespace: "globalintl"
+ description: "Feature flag for regional preferences APIs"
+ bug: "370379000"
+}
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 0241e94..a1a9fc6 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -1786,7 +1786,12 @@
* {@code getWindowManager()} or {@code getSystemService(Context.WINDOW_SERVICE)}), the
* returned metrics provide the size of the current app window. As a result, in
* multi-window mode, the returned size can be smaller than the size of the device
- * screen.
+ * screen. System decoration handling may vary depending on API level:
+ * <ul>
+ * <li>API level 35 and above, the window size will be returned.
+ * <li>API level 34 and below, the window size minus system decoration areas and
+ * display cutout is returned.
+ * </ul>
* <li>If metrics are requested from a non-activity context (for example, the application
* context, where the WindowManager is accessed by
* {@code getApplicationContext().getSystemService(Context.WINDOW_SERVICE)}), the
diff --git a/core/java/android/view/ImeFocusController.java b/core/java/android/view/ImeFocusController.java
index 9714896..2d2f79d 100644
--- a/core/java/android/view/ImeFocusController.java
+++ b/core/java/android/view/ImeFocusController.java
@@ -23,6 +23,7 @@
import android.annotation.UiThread;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.InputMethodManager;
import com.android.internal.inputmethod.InputMethodDebug;
@@ -150,6 +151,17 @@
if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
return InputMethodManager.DISPATCH_NOT_HANDLED;
}
+ if (Flags.refactorInsetsController() && event instanceof KeyEvent keyEvent
+ && keyEvent.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+ final var insetsController = mViewRootImpl.getInsetsController();
+ if (insetsController.getAnimationType(WindowInsets.Type.ime())
+ == InsetsController.ANIMATION_TYPE_HIDE
+ || insetsController.isPredictiveBackImeHideAnimInProgress()) {
+ // if there is an ongoing hide animation, the back event should not be dispatched
+ // to the IME.
+ return InputMethodManager.DISPATCH_NOT_HANDLED;
+ }
+ }
final InputMethodManager imm =
mViewRootImpl.mContext.getSystemService(InputMethodManager.class);
if (imm == null) {
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 26ca813..b0813f3 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -1910,7 +1910,8 @@
mImeSourceConsumer.onWindowFocusLost();
}
- @VisibleForTesting
+ /** Returns the current {@link AnimationType} of an {@link InsetsType}. */
+ @VisibleForTesting(visibility = PACKAGE)
public @AnimationType int getAnimationType(@InsetsType int type) {
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 0b61c94..c71bf4b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -65,6 +65,7 @@
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP;
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION;
import static com.android.window.flags.Flags.FLAG_DELEGATE_UNHANDLED_DRAGS;
+import static com.android.window.flags.Flags.FLAG_SUPPORTS_DRAG_ASSISTANT_TO_MULTIWINDOW;
import static java.lang.Math.max;
@@ -5552,10 +5553,11 @@
/**
* Flag indicating that this drag will result in the caller activity's task to be hidden for the
- * duration of the drag, this means that the source activity will not receive drag events for
- * the current drag gesture. Only the current voice interaction service may use this flag.
- * @hide
+ * duration of the drag, which means that the source activity will not receive drag events for
+ * the current drag gesture. Only the current
+ * {@link android.service.voice.VoiceInteractionService} may use this flag.
*/
+ @FlaggedApi(FLAG_SUPPORTS_DRAG_ASSISTANT_TO_MULTIWINDOW)
public static final int DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START = 1 << 14;
/**
diff --git a/core/java/android/view/WindowMetrics.java b/core/java/android/view/WindowMetrics.java
index 8bcc9de..12af692 100644
--- a/core/java/android/view/WindowMetrics.java
+++ b/core/java/android/view/WindowMetrics.java
@@ -107,8 +107,8 @@
* and display cutout areas depending on the calling context and target SDK level. Please refer
* to {@link Display#getSize(Point)} for details.
* <p>
- * The value reported by {@link Display#getSize(Point)} excluding system decoration areas can be
- * obtained by using:
+ * The following code snippet shows how to get the bounds excluding navigation bars and display
+ * cutout:
* <pre class="prettyprint">
* final WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
* // Gets all excluding insets
diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java
index 877fa74..1baf3b8 100644
--- a/core/java/android/webkit/WebChromeClient.java
+++ b/core/java/android/webkit/WebChromeClient.java
@@ -20,6 +20,8 @@
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
@@ -552,6 +554,23 @@
* Parameters used in the {@link #onShowFileChooser} method.
*/
public static abstract class FileChooserParams {
+ /**
+ * Enable File System Access for webview.
+ *
+ * File System Access JS APIs window.showOpenFileChooser(), showDirectoryChooser(), and
+ * showSaveFilePicker() will invoke #onFileChooser(). Additional MODE_OPEN_FOLDER will be
+ * returned by #getMode(), #getIntent() will return ACTION_OPEN_DOCUMENT,
+ * ACTION_OPEN_DOCUMENT_TREE, and ACTION_CREATE_DOCUMENT rather than the current
+ * ACTION_GET_CONTENT, and new function #getPermissionMode() will be available.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @FlaggedApi(android.webkit.Flags.FLAG_FILE_SYSTEM_ACCESS)
+ @SystemApi
+ public static final long ENABLE_FILE_SYSTEM_ACCESS = 364980165L;
+
/** @hide */
@IntDef(prefix = { "MODE_" }, value = {
MODE_OPEN,
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index d9de38a..4f924a82 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -160,12 +160,21 @@
}
flag {
- name: "delegate_unhandled_drags"
- is_exported: true
- namespace: "multitasking"
- description: "Enables delegating unhandled drags to SystemUI"
- bug: "320797628"
- is_fixed_read_only: true
+ name: "delegate_unhandled_drags"
+ is_exported: true
+ namespace: "multitasking"
+ description: "Enables delegating unhandled drags to SystemUI"
+ bug: "320797628"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "supports_drag_assistant_to_multiwindow"
+ is_exported: true
+ namespace: "multitasking"
+ description: "Enables support for dragging the assistant into multiwindow"
+ bug: "371206207"
+ is_fixed_read_only: true
}
flag {
diff --git a/core/java/com/android/internal/app/AppLocaleCollector.java b/core/java/com/android/internal/app/AppLocaleCollector.java
index 56f633f..ca1fc0a 100644
--- a/core/java/com/android/internal/app/AppLocaleCollector.java
+++ b/core/java/com/android/internal/app/AppLocaleCollector.java
@@ -41,7 +41,7 @@
import java.util.stream.Collectors;
/** The Locale data collector for per-app language. */
-public class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
+public class AppLocaleCollector implements LocaleCollectorBase {
private static final String TAG = AppLocaleCollector.class.getSimpleName();
private final Context mContext;
private final String mAppPackageName;
@@ -167,8 +167,8 @@
}
@Override
- public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) {
- HashSet<String> langTagsToIgnore = new HashSet<>();
+ public Set<String> getIgnoredLocaleList(boolean translatedOnly) {
+ Set<String> langTagsToIgnore = new HashSet<>();
if (mAppCurrentLocale != null) {
langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag());
diff --git a/core/java/com/android/internal/app/LocaleCollectorBase.java b/core/java/com/android/internal/app/LocaleCollectorBase.java
new file mode 100644
index 0000000..f839077
--- /dev/null
+++ b/core/java/com/android/internal/app/LocaleCollectorBase.java
@@ -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.internal.app;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * The interface which provides the locale list.
+ */
+public interface LocaleCollectorBase {
+
+ /** Gets the ignored locale list. */
+ Set<String> getIgnoredLocaleList(boolean translatedOnly);
+
+ /** Gets the supported locale list. */
+ Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent,
+ boolean translatedOnly, boolean isForCountryMode);
+
+ /** Indicates if the class work for specific package. */
+ boolean hasSpecificPackageName();
+}
diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java
index ef4acd1..ffffefa 100644
--- a/core/java/com/android/internal/app/LocalePickerWithRegion.java
+++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java
@@ -44,7 +44,10 @@
* <p>It shows suggestions at the top, then the rest of the locales.
* Allows the user to search for locales using both their native name and their name in the
* default locale.</p>
+ *
+ * @deprecated use SettingLib's widget instead of customized UIs.
*/
+@Deprecated
public class LocalePickerWithRegion extends ListFragment implements SearchView.OnQueryTextListener {
private static final String TAG = LocalePickerWithRegion.class.getSimpleName();
private static final String PARENT_FRAGMENT_NAME = "localeListEditor";
@@ -78,21 +81,6 @@
default void onParentLocaleSelected(LocaleStore.LocaleInfo locale) {}
}
- /**
- * The interface which provides the locale list.
- */
- interface LocaleCollectorBase {
- /** Gets the ignored locale list. */
- HashSet<String> getIgnoredLocaleList(boolean translatedOnly);
-
- /** Gets the supported locale list. */
- Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent,
- boolean translatedOnly, boolean isForCountryMode);
-
- /** Indicates if the class work for specific package. */
- boolean hasSpecificPackageName();
- }
-
private static LocalePickerWithRegion createNumberingSystemPicker(
LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
boolean translatedOnly, OnActionExpandListener onActionExpandListener,
diff --git a/core/java/com/android/internal/app/SystemLocaleCollector.java b/core/java/com/android/internal/app/SystemLocaleCollector.java
index 416f510..c7931cb 100644
--- a/core/java/com/android/internal/app/SystemLocaleCollector.java
+++ b/core/java/com/android/internal/app/SystemLocaleCollector.java
@@ -24,7 +24,7 @@
import java.util.Set;
/** The Locale data collector for System language. */
-class SystemLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
+public class SystemLocaleCollector implements LocaleCollectorBase {
private final Context mContext;
private LocaleList mExplicitLocales;
@@ -32,14 +32,14 @@
this(context, null);
}
- SystemLocaleCollector(Context context, LocaleList explicitLocales) {
+ public SystemLocaleCollector(Context context, LocaleList explicitLocales) {
mContext = context;
mExplicitLocales = explicitLocales;
}
@Override
- public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) {
- HashSet<String> ignoreList = new HashSet<>();
+ public Set<String> getIgnoredLocaleList(boolean translatedOnly) {
+ Set<String> ignoreList = new HashSet<>();
if (!translatedOnly) {
final LocaleList userLocales = LocalePicker.getLocales();
final String[] langTags = userLocales.toLanguageTags().split(",");
diff --git a/core/res/res/values/stoppable_fgs_system_apps.xml b/core/res/res/values/stoppable_fgs_system_apps.xml
index 165ff61..06843f4 100644
--- a/core/res/res/values/stoppable_fgs_system_apps.xml
+++ b/core/res/res/values/stoppable_fgs_system_apps.xml
@@ -19,6 +19,7 @@
<resources>
<!-- A list of system apps whose FGS can be stopped in the task manager. -->
<string-array translatable="false" name="stoppable_fgs_system_apps">
+ <item>com.android.virtualization.terminal</item>
</string-array>
<!-- stoppable_fgs_system_apps which is supposed to be overridden by vendor -->
<string-array translatable="false" name="vendor_stoppable_fgs_system_apps">
diff --git a/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientParcelableChecker.java b/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientParcelableChecker.java
index 35b2375..cae5d8e 100644
--- a/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientParcelableChecker.java
+++ b/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientParcelableChecker.java
@@ -96,7 +96,7 @@
}
if (WRITE_PARCELABLE.matches(tree, state)) {
return buildDescription(tree)
- .setMessage("Recommended to use 'writeTypedObject()' to improve "
+ .setMessage("Recommended to use 'item.writeToParcel()' to improve "
+ "efficiency; saves overhead of Parcelable class name")
.build();
}
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index b2ac640..636e3cf 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -32,7 +32,6 @@
android:name=".desktopmode.DesktopWallpaperActivity"
android:excludeFromRecents="true"
android:launchMode="singleInstance"
- android:showForAllUsers="true"
android:theme="@style/DesktopWallpaperTheme" />
<activity
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 0200e18..c99d9ba8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -188,6 +188,8 @@
*/
private boolean mIsFirstReachabilityEducationRunning;
+ private boolean mIsInDesktopMode;
+
@NonNull
private final CompatUIStatusManager mCompatUIStatusManager;
@@ -253,18 +255,19 @@
if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()) {
mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId);
}
-
- if (taskInfo != null && taskListener != null) {
- updateActiveTaskInfo(taskInfo);
- }
-
- // We close all the Compat UI educations in case we're in desktop mode.
- if (taskInfo.configuration == null || taskListener == null
- || isInDesktopMode(taskInfo.displayId)) {
+ mIsInDesktopMode = isInDesktopMode(taskInfo);
+ // We close all the Compat UI educations in case TaskInfo has no configuration or
+ // TaskListener or in desktop mode.
+ if (taskInfo.configuration == null || taskListener == null || mIsInDesktopMode) {
// Null token means the current foreground activity is not in compatibility mode.
removeLayouts(taskInfo.taskId);
return;
}
+ if (taskInfo != null && taskListener != null) {
+ updateActiveTaskInfo(taskInfo);
+ }
+
+
// We're showing the first reachability education so we ignore incoming TaskInfo
// until the education flow has completed or we double tap. The double-tap
// basically cancel all the onboarding flow. We don't have to ignore events in case
@@ -443,7 +446,7 @@
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
CompatUIWindowManager layout = mActiveCompatLayouts.get(taskInfo.taskId);
if (layout != null) {
- if (layout.needsToBeRecreated(taskInfo, taskListener)) {
+ if (layout.needsToBeRecreated(taskInfo, taskListener) || mIsInDesktopMode) {
mActiveCompatLayouts.remove(taskInfo.taskId);
layout.release();
} else {
@@ -456,7 +459,10 @@
return;
}
}
-
+ if (mIsInDesktopMode) {
+ // Return if in desktop mode.
+ return;
+ }
// Create a new UI layout.
final Context context = getOrCreateDisplayContext(taskInfo.displayId);
if (context == null) {
@@ -494,7 +500,8 @@
private void createOrUpdateLetterboxEduLayout(@NonNull TaskInfo taskInfo,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
if (mActiveLetterboxEduLayout != null) {
- if (mActiveLetterboxEduLayout.needsToBeRecreated(taskInfo, taskListener)) {
+ if (mActiveLetterboxEduLayout.needsToBeRecreated(taskInfo, taskListener)
+ || mIsInDesktopMode) {
mActiveLetterboxEduLayout.release();
mActiveLetterboxEduLayout = null;
} else {
@@ -507,6 +514,10 @@
return;
}
}
+ if (mIsInDesktopMode) {
+ // Return if in desktop mode.
+ return;
+ }
// Create a new UI layout.
final Context context = getOrCreateDisplayContext(taskInfo.displayId);
if (context == null) {
@@ -541,7 +552,7 @@
RestartDialogWindowManager layout =
mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId);
if (layout != null) {
- if (layout.needsToBeRecreated(taskInfo, taskListener)) {
+ if (layout.needsToBeRecreated(taskInfo, taskListener) || mIsInDesktopMode) {
mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId);
layout.release();
} else {
@@ -556,6 +567,10 @@
return;
}
}
+ if (mIsInDesktopMode) {
+ // Return if in desktop mode.
+ return;
+ }
// Create a new UI layout.
final Context context = getOrCreateDisplayContext(taskInfo.displayId);
if (context == null) {
@@ -594,7 +609,8 @@
private void createOrUpdateReachabilityEduLayout(@NonNull TaskInfo taskInfo,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
if (mActiveReachabilityEduLayout != null) {
- if (mActiveReachabilityEduLayout.needsToBeRecreated(taskInfo, taskListener)) {
+ if (mActiveReachabilityEduLayout.needsToBeRecreated(taskInfo, taskListener)
+ || mIsInDesktopMode) {
mActiveReachabilityEduLayout.release();
mActiveReachabilityEduLayout = null;
} else {
@@ -608,6 +624,10 @@
return;
}
}
+ if (mIsInDesktopMode) {
+ // Return if in desktop mode.
+ return;
+ }
// Create a new UI layout.
final Context context = getOrCreateDisplayContext(taskInfo.displayId);
if (context == null) {
@@ -647,7 +667,8 @@
private void createOrUpdateUserAspectRatioSettingsLayout(@NonNull TaskInfo taskInfo,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
if (mUserAspectRatioSettingsLayout != null) {
- if (mUserAspectRatioSettingsLayout.needsToBeRecreated(taskInfo, taskListener)) {
+ if (mUserAspectRatioSettingsLayout.needsToBeRecreated(taskInfo, taskListener)
+ || mIsInDesktopMode) {
mUserAspectRatioSettingsLayout.release();
mUserAspectRatioSettingsLayout = null;
} else {
@@ -660,7 +681,10 @@
return;
}
}
-
+ if (mIsInDesktopMode) {
+ // Return if in desktop mode.
+ return;
+ }
// Create a new UI layout.
final Context context = getOrCreateDisplayContext(taskInfo.displayId);
if (context == null) {
@@ -840,8 +864,8 @@
boolean mHasShownUserAspectRatioSettingsButtonHint;
}
- private boolean isInDesktopMode(int displayId) {
- return Flags.skipCompatUiEducationInDesktopMode()
- && mInDesktopModePredicate.test(displayId);
+ private boolean isInDesktopMode(@Nullable TaskInfo taskInfo) {
+ return taskInfo != null && Flags.skipCompatUiEducationInDesktopMode()
+ && mInDesktopModePredicate.test(taskInfo.displayId);
}
}
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 223038f..7446b88 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
@@ -42,6 +42,7 @@
import android.os.Handler
import android.os.IBinder
import android.os.SystemProperties
+import android.os.UserHandle
import android.util.Size
import android.view.Display.DEFAULT_DISPLAY
import android.view.DragEvent
@@ -1174,7 +1175,11 @@
private fun addWallpaperActivity(wct: WindowContainerTransaction) {
logV("addWallpaperActivity")
- val intent = Intent(context, DesktopWallpaperActivity::class.java)
+ val userHandle = UserHandle.of(userId)
+ val userContext =
+ context.createContextAsUser(userHandle, /* flags= */ 0)
+ val intent = Intent(userContext, DesktopWallpaperActivity::class.java)
+ intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
val options =
ActivityOptions.makeBasic().apply {
launchWindowingMode = WINDOWING_MODE_FULLSCREEN
@@ -1182,11 +1187,13 @@
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
}
val pendingIntent =
- PendingIntent.getActivity(
- context,
- /* requestCode = */ 0,
+ PendingIntent.getActivityAsUser(
+ userContext,
+ /* requestCode= */ 0,
intent,
- PendingIntent.FLAG_IMMUTABLE
+ PendingIntent.FLAG_IMMUTABLE,
+ /* bundle= */ null,
+ userHandle
)
wct.sendPendingIntent(pendingIntent, intent, options.toBundle())
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt
index 1c2415c..e835b2f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt
@@ -36,7 +36,6 @@
class DesktopWallpaperActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
- ProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopWallpaperActivity: onCreate")
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index f739d65..49cf8ae 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -63,6 +63,8 @@
"Bubbles"),
WM_SHELL_COMPAT_UI(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
Consts.TAG_WM_COMPAT_UI),
+ WM_SHELL_APP_COMPAT(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+ Consts.TAG_WM_APP_COMPAT),
TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest");
private final boolean mEnabled;
@@ -131,6 +133,7 @@
private static final String TAG_WM_SPLIT_SCREEN = "ShellSplitScreen";
private static final String TAG_WM_DESKTOP_MODE = "ShellDesktopMode";
private static final String TAG_WM_COMPAT_UI = "CompatUi";
+ private static final String TAG_WM_APP_COMPAT = "AppCompat";
private static final boolean ENABLE_DEBUG = true;
private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true;
diff --git a/media/java/android/media/quality/PictureProfileHandle.aidl b/media/java/android/media/quality/PictureProfileHandle.aidl
new file mode 100644
index 0000000..5d14631
--- /dev/null
+++ b/media/java/android/media/quality/PictureProfileHandle.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.media.quality;
+
+parcelable PictureProfileHandle;
diff --git a/media/java/android/media/quality/PictureProfileHandle.java b/media/java/android/media/quality/PictureProfileHandle.java
new file mode 100644
index 0000000..2b1cda4
--- /dev/null
+++ b/media/java/android/media/quality/PictureProfileHandle.java
@@ -0,0 +1,75 @@
+/*
+ * 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.media.quality;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+// TODO(b/337330263): Expose as public API after API review
+/**
+ * A type-safe handle to a picture profile, which represents a collection of parameters used to
+ * configure picture processing hardware to enhance the quality of graphic buffers.
+ * @hide
+ */
+@FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+public final class PictureProfileHandle implements Parcelable {
+ private final long mId;
+
+ @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+ public PictureProfileHandle(long id) {
+ mId = id;
+ }
+
+ @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+ public long getId() {
+ return mId;
+ }
+
+ @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeLong(mId);
+ }
+
+ @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+ @NonNull
+ public static final Creator<PictureProfileHandle> CREATOR =
+ new Creator<PictureProfileHandle>() {
+ @Override
+ public PictureProfileHandle createFromParcel(Parcel in) {
+ return new PictureProfileHandle(in);
+ }
+
+ @Override
+ public PictureProfileHandle[] newArray(int size) {
+ return new PictureProfileHandle[size];
+ }
+ };
+
+ private PictureProfileHandle(@NonNull Parcel in) {
+ mId = in.readLong();
+ }
+}
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index 821ea21..bf419cc 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -74,16 +74,6 @@
}
flag {
- name: "volume_panel_broadcast_fix"
- namespace: "systemui"
- description: "Make the volume panel's repository listen for the new ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED broadcast instead of ACTION_NOTIFICATION_POLICY_CHANGED"
- bug: "347707024"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "volume_dialog_audio_sharing_fix"
namespace: "cross_device_experiences"
description: "Gates whether to show separate volume bars during audio sharing"
@@ -111,6 +101,14 @@
}
flag {
+ name: "write_system_preference_permission_enabled"
+ is_fixed_read_only: true
+ namespace: "android_settings"
+ description: "Enable WRITE_SYSTEM_PREFERENCE permission and appop"
+ bug: "375193223"
+}
+
+flag {
name: "asha_profile_access_profile_enabled_true"
namespace: "accessibility"
description: "Changes the return value of HearingAidProfile.accessProfileEnabled() to true"
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
index 7fdbcda..f446bb8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
@@ -93,33 +93,23 @@
IntentFilter().apply {
addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED)
addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED)
- if (Flags.volumePanelBroadcastFix() && android.app.Flags.modesApi())
+ if (android.app.Flags.modesApi())
addAction(
- NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED)
+ NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED
+ )
},
/* broadcastPermission = */ null,
- /* scheduler = */ if (Flags.volumePanelBroadcastFix()) {
- backgroundHandler
- } else {
- null
- },
+ /* scheduler = */ backgroundHandler,
)
awaitClose { context.unregisterReceiver(receiver) }
}
- .let {
- if (Flags.volumePanelBroadcastFix()) {
- // Share the flow to avoid having multiple broadcasts.
- it.flowOn(backgroundCoroutineContext)
- .shareIn(started = SharingStarted.WhileSubscribed(), scope = scope)
- } else {
- it.shareIn(started = SharingStarted.WhileSubscribed(), scope = scope)
- }
- }
+ .flowOn(backgroundCoroutineContext)
+ .shareIn(started = SharingStarted.WhileSubscribed(), scope = scope)
}
override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy {
- if (Flags.volumePanelBroadcastFix() && android.app.Flags.modesApi())
+ if (android.app.Flags.modesApi())
flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) {
// If available, get the value from extras to avoid a potential binder call.
it?.extras?.getParcelable(EXTRA_NOTIFICATION_POLICY)
@@ -161,11 +151,13 @@
contentResolver.registerContentObserver(
Settings.Global.getUriFor(Settings.Global.ZEN_MODE),
/* notifyForDescendants= */ false,
- observer)
+ observer,
+ )
contentResolver.registerContentObserver(
Settings.Global.getUriFor(Settings.Global.ZEN_MODE_CONFIG_ETAG),
/* notifyForDescendants= */ false,
- observer)
+ observer,
+ )
awaitClose { contentResolver.unregisterContentObserver(observer) }
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
index c136644..388af61 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
@@ -23,12 +23,10 @@
import android.content.Intent
import android.database.ContentObserver
import android.os.Parcelable
-import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.provider.Settings.Global
import androidx.test.filters.SmallTest
-import com.android.settingslib.flags.Flags
import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.settingslib.notification.modes.ZenMode
import com.android.settingslib.notification.modes.ZenModesBackend
@@ -93,26 +91,7 @@
)
}
- @DisableFlags(Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX)
- @Test
- fun consolidatedPolicyChanges_repositoryEmits_flagsOff() {
- testScope.runTest {
- val values = mutableListOf<NotificationManager.Policy?>()
- `when`(notificationManager.consolidatedNotificationPolicy).thenReturn(testPolicy1)
- underTest.consolidatedNotificationPolicy
- .onEach { values.add(it) }
- .launchIn(backgroundScope)
- runCurrent()
-
- `when`(notificationManager.consolidatedNotificationPolicy).thenReturn(testPolicy2)
- triggerIntent(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED)
- runCurrent()
-
- assertThat(values).containsExactly(null, testPolicy1, testPolicy2).inOrder()
- }
- }
-
- @EnableFlags(android.app.Flags.FLAG_MODES_API, Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX)
+ @EnableFlags(android.app.Flags.FLAG_MODES_API)
@Test
fun consolidatedPolicyChanges_repositoryEmits_flagsOn() {
testScope.runTest {
@@ -131,7 +110,7 @@
}
}
- @EnableFlags(android.app.Flags.FLAG_MODES_API, Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX)
+ @EnableFlags(android.app.Flags.FLAG_MODES_API)
@Test
fun consolidatedPolicyChanges_repositoryEmitsFromExtras() {
testScope.runTest {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 041cd15..c33d655 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -25,7 +25,6 @@
import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastCoerceIn
import com.android.compose.animation.scene.content.Content
-import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
import com.android.compose.nestedscroll.OnStopScope
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
@@ -36,11 +35,11 @@
internal interface DraggableHandler {
/**
- * Start a drag with the given [pointersInfo] and [overSlop].
+ * Start a drag with the given [pointersDown] and [overSlop].
*
* The returned [DragController] should be used to continue or stop the drag.
*/
- fun onDragStarted(pointersInfo: PointersInfo?, overSlop: Float): DragController
+ fun onDragStarted(pointersDown: PointersInfo.PointersDown?, overSlop: Float): DragController
}
/**
@@ -95,7 +94,7 @@
* Note: if this returns true, then [onDragStarted] will be called with overSlop equal to 0f,
* indicating that the transition should be intercepted.
*/
- internal fun shouldImmediatelyIntercept(pointersInfo: PointersInfo?): Boolean {
+ internal fun shouldImmediatelyIntercept(pointersDown: PointersInfo.PointersDown?): Boolean {
// We don't intercept the touch if we are not currently driving the transition.
val dragController = dragController
if (dragController?.isDrivingTransition != true) {
@@ -106,7 +105,7 @@
// Only intercept the current transition if one of the 2 swipes results is also a transition
// between the same pair of contents.
- val swipes = computeSwipes(pointersInfo)
+ val swipes = computeSwipes(pointersDown)
val fromContent = layoutImpl.content(swipeAnimation.currentContent)
val (upOrLeft, downOrRight) = swipes.computeSwipesResults(fromContent)
val currentScene = layoutImpl.state.currentScene
@@ -123,7 +122,10 @@
))
}
- override fun onDragStarted(pointersInfo: PointersInfo?, overSlop: Float): DragController {
+ override fun onDragStarted(
+ pointersDown: PointersInfo.PointersDown?,
+ overSlop: Float,
+ ): DragController {
if (overSlop == 0f) {
val oldDragController = dragController
check(oldDragController != null && oldDragController.isDrivingTransition) {
@@ -148,7 +150,7 @@
return updateDragController(swipes, swipeAnimation)
}
- val swipes = computeSwipes(pointersInfo)
+ val swipes = computeSwipes(pointersDown)
val fromContent = layoutImpl.contentForUserActions()
swipes.updateSwipesResults(fromContent)
@@ -194,11 +196,11 @@
)
}
- private fun computeSwipes(pointersInfo: PointersInfo?): Swipes {
- val fromSource = pointersInfo?.let { resolveSwipeSource(it.startedPosition) }
+ private fun computeSwipes(pointersDown: PointersInfo.PointersDown?): Swipes {
+ val fromSource = pointersDown?.let { resolveSwipeSource(it.startedPosition) }
return Swipes(
- upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersInfo, fromSource),
- downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersInfo, fromSource),
+ upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersDown, fromSource),
+ downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersDown, fromSource),
)
}
}
@@ -206,7 +208,7 @@
private fun resolveSwipe(
orientation: Orientation,
isUpOrLeft: Boolean,
- pointersInfo: PointersInfo?,
+ pointersDown: PointersInfo.PointersDown?,
fromSource: SwipeSource.Resolved?,
): Swipe.Resolved {
return Swipe.Resolved(
@@ -227,9 +229,9 @@
}
},
// If the number of pointers is not specified, 1 is assumed.
- pointerCount = pointersInfo?.pointersDown ?: 1,
+ pointerCount = pointersDown?.count ?: 1,
// Resolves the pointer type only if all pointers are of the same type.
- pointersType = pointersInfo?.pointersDownByType?.keys?.singleOrNull(),
+ pointersType = pointersDown?.countByType?.keys?.singleOrNull(),
fromSource = fromSource,
)
}
@@ -540,40 +542,15 @@
val connection: PriorityNestedScrollConnection = nestedScrollConnection()
- private fun resolveSwipe(isUpOrLeft: Boolean, pointersInfo: PointersInfo?): Swipe.Resolved {
- return resolveSwipe(
- orientation = draggableHandler.orientation,
- isUpOrLeft = isUpOrLeft,
- pointersInfo = pointersInfo,
- fromSource =
- pointersInfo?.let { draggableHandler.resolveSwipeSource(it.startedPosition) },
- )
- }
-
private fun nestedScrollConnection(): PriorityNestedScrollConnection {
// If we performed a long gesture before entering priority mode, we would have to avoid
// moving on to the next scene.
var canChangeScene = false
- var lastPointersInfo: PointersInfo? = null
+ var lastPointersDown: PointersInfo.PointersDown? = null
- fun hasNextScene(amount: Float): Boolean {
- val transitionState = layoutState.transitionState
- val scene = transitionState.currentScene
- val fromScene = layoutImpl.scene(scene)
- val resolvedSwipe =
- when {
- amount < 0f -> resolveSwipe(isUpOrLeft = true, lastPointersInfo)
- amount > 0f -> resolveSwipe(isUpOrLeft = false, lastPointersInfo)
- else -> null
- }
- val nextScene = resolvedSwipe?.let { fromScene.findActionResultBestMatch(it) }
- if (nextScene != null) return true
-
- if (transitionState !is TransitionState.Idle) return false
-
- val overscrollSpec = layoutImpl.state.transitions.overscrollSpec(scene, orientation)
- return overscrollSpec != null
+ fun shouldEnableSwipes(): Boolean {
+ return layoutImpl.contentForUserActions().shouldEnableSwipes(orientation)
}
var isIntercepting = false
@@ -581,14 +558,24 @@
return PriorityNestedScrollConnection(
orientation = orientation,
canStartPreScroll = { offsetAvailable, offsetBeforeStart, _ ->
- val pointersInfo = pointersInfoOwner.pointersInfo()
+ val pointersDown: PointersInfo.PointersDown? =
+ when (val info = pointersInfoOwner.pointersInfo()) {
+ PointersInfo.MouseWheel -> {
+ // Do not support mouse wheel interactions
+ return@PriorityNestedScrollConnection false
+ }
+
+ is PointersInfo.PointersDown -> info
+ null -> null
+ }
+
canChangeScene =
if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f
val canInterceptSwipeTransition =
canChangeScene &&
offsetAvailable != 0f &&
- draggableHandler.shouldImmediatelyIntercept(pointersInfo)
+ draggableHandler.shouldImmediatelyIntercept(pointersDown)
if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false
val threshold = layoutImpl.transitionInterceptionThreshold
@@ -599,11 +586,7 @@
return@PriorityNestedScrollConnection false
}
- if (pointersInfo?.isMouseWheel == true) {
- // Do not support mouse wheel interactions
- return@PriorityNestedScrollConnection false
- }
- lastPointersInfo = pointersInfo
+ lastPointersDown = pointersDown
// If the current swipe transition is *not* closed to 0f or 1f, then we want the
// scroll events to intercept the current transition to continue the scene
@@ -622,28 +605,33 @@
val isZeroOffset =
if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f
- val pointersInfo = pointersInfoOwner.pointersInfo()
- if (pointersInfo?.isMouseWheel == true) {
- // Do not support mouse wheel interactions
- return@PriorityNestedScrollConnection false
- }
- lastPointersInfo = pointersInfo
+ val pointersDown: PointersInfo.PointersDown? =
+ when (val info = pointersInfoOwner.pointersInfo()) {
+ PointersInfo.MouseWheel -> {
+ // Do not support mouse wheel interactions
+ return@PriorityNestedScrollConnection false
+ }
+
+ is PointersInfo.PointersDown -> info
+ null -> null
+ }
+ lastPointersDown = pointersDown
val canStart =
when (behavior) {
NestedScrollBehavior.EdgeNoPreview -> {
canChangeScene = isZeroOffset
- isZeroOffset && hasNextScene(offsetAvailable)
+ isZeroOffset && shouldEnableSwipes()
}
NestedScrollBehavior.EdgeWithPreview -> {
canChangeScene = isZeroOffset
- hasNextScene(offsetAvailable)
+ shouldEnableSwipes()
}
NestedScrollBehavior.EdgeAlways -> {
canChangeScene = true
- hasNextScene(offsetAvailable)
+ shouldEnableSwipes()
}
}
@@ -664,14 +652,19 @@
// We could start an overscroll animation
canChangeScene = false
- val pointersInfo = pointersInfoOwner.pointersInfo()
- if (pointersInfo?.isMouseWheel == true) {
- // Do not support mouse wheel interactions
- return@PriorityNestedScrollConnection false
- }
- lastPointersInfo = pointersInfo
+ val pointersDown: PointersInfo.PointersDown? =
+ when (val info = pointersInfoOwner.pointersInfo()) {
+ PointersInfo.MouseWheel -> {
+ // Do not support mouse wheel interactions
+ return@PriorityNestedScrollConnection false
+ }
- val canStart = behavior.canStartOnPostFling && hasNextScene(velocityAvailable)
+ is PointersInfo.PointersDown -> info
+ null -> null
+ }
+ lastPointersDown = pointersDown
+
+ val canStart = behavior.canStartOnPostFling && shouldEnableSwipes()
if (canStart) {
isIntercepting = false
}
@@ -679,11 +672,10 @@
canStart
},
onStart = { firstScroll ->
- val pointersInfo = lastPointersInfo
scrollController(
dragController =
draggableHandler.onDragStarted(
- pointersInfo = pointersInfo,
+ pointersDown = lastPointersDown,
overSlop = if (isIntercepting) 0f else firstScroll,
),
canChangeScene = canChangeScene,
@@ -701,8 +693,7 @@
): ScrollController {
return object : ScrollController {
override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float {
- val pointersInfo = pointersInfoOwner.pointersInfo()
- if (pointersInfo?.isMouseWheel == true) {
+ if (pointersInfoOwner.pointersInfo() == PointersInfo.MouseWheel) {
// Do not support mouse wheel interactions
return 0f
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index ab2324a..1603267 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -80,8 +80,8 @@
@Stable
internal fun Modifier.multiPointerDraggable(
orientation: Orientation,
- startDragImmediately: (pointersInfo: PointersInfo) -> Boolean,
- onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController,
+ startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean,
+ onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController,
onFirstPointerDown: () -> Unit = {},
swipeDetector: SwipeDetector = DefaultSwipeDetector,
dispatcher: NestedScrollDispatcher,
@@ -99,8 +99,9 @@
private data class MultiPointerDraggableElement(
private val orientation: Orientation,
- private val startDragImmediately: (pointersInfo: PointersInfo) -> Boolean,
- private val onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController,
+ private val startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean,
+ private val onDragStarted:
+ (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController,
private val onFirstPointerDown: () -> Unit,
private val swipeDetector: SwipeDetector,
private val dispatcher: NestedScrollDispatcher,
@@ -126,8 +127,8 @@
internal class MultiPointerDraggableNode(
orientation: Orientation,
- var startDragImmediately: (pointersInfo: PointersInfo) -> Boolean,
- var onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController,
+ var startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean,
+ var onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController,
var onFirstPointerDown: () -> Unit,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
private val dispatcher: NestedScrollDispatcher,
@@ -185,20 +186,27 @@
private var lastPointerEvent: PointerEvent? = null
private var startedPosition: Offset? = null
- private var pointersDown: Int = 0
+ private var countPointersDown: Int = 0
internal fun pointersInfo(): PointersInfo? {
- val startedPosition = startedPosition
- val lastPointerEvent = lastPointerEvent
- if (startedPosition == null || lastPointerEvent == null) {
- // This may be null, i.e. when the user uses TalkBack
- return null
- }
+ // This may be null, i.e. when the user uses TalkBack
+ val lastPointerEvent = lastPointerEvent ?: return null
- return PointersInfo(
+ if (lastPointerEvent.type == PointerEventType.Scroll) return PointersInfo.MouseWheel
+
+ val startedPosition = startedPosition ?: return null
+
+ return PointersInfo.PointersDown(
startedPosition = startedPosition,
- pointersDown = pointersDown,
- lastPointerEvent = lastPointerEvent,
+ count = countPointersDown,
+ countByType =
+ buildMap {
+ lastPointerEvent.changes.fastForEach { change ->
+ if (!change.pressed) return@fastForEach
+ val newValue = (get(change.type) ?: 0) + 1
+ put(change.type, newValue)
+ }
+ },
)
}
@@ -218,11 +226,11 @@
val changes = pointerEvent.changes
lastPointerEvent = pointerEvent
- pointersDown = changes.countDown()
+ countPointersDown = changes.countDown()
when {
// There are no more pointers down.
- pointersDown == 0 -> {
+ countPointersDown == 0 -> {
startedPosition = null
// In case of multiple events with 0 pointers down (not pressed) we may have
@@ -290,8 +298,8 @@
detectDragGestures(
orientation = orientation,
startDragImmediately = startDragImmediately,
- onDragStart = { pointersInfo, overSlop ->
- onDragStarted(pointersInfo, overSlop)
+ onDragStart = { pointersDown, overSlop ->
+ onDragStarted(pointersDown, overSlop)
},
onDrag = { controller, amount ->
dispatchScrollEvents(
@@ -440,8 +448,8 @@
*/
private suspend fun AwaitPointerEventScope.detectDragGestures(
orientation: Orientation,
- startDragImmediately: (pointersInfo: PointersInfo) -> Boolean,
- onDragStart: (pointersInfo: PointersInfo, overSlop: Float) -> DragController,
+ startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean,
+ onDragStart: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController,
onDrag: (controller: DragController, dragAmount: Float) -> Unit,
onDragEnd: (controller: DragController) -> Unit,
onDragCancel: (controller: DragController) -> Unit,
@@ -466,13 +474,14 @@
.first()
var overSlop = 0f
- var lastPointersInfo =
+ var lastPointersDown: PointersInfo.PointersDown =
checkNotNull(pointersInfo()) {
"We should have pointers down, last event: $currentEvent"
}
+ as PointersInfo.PointersDown
val drag =
- if (startDragImmediately(lastPointersInfo)) {
+ if (startDragImmediately(lastPointersDown)) {
consumablePointer.consume()
consumablePointer
} else {
@@ -499,10 +508,11 @@
)
} ?: return
- lastPointersInfo =
+ lastPointersDown =
checkNotNull(pointersInfo()) {
"We should have pointers down, last event: $currentEvent"
}
+ as PointersInfo.PointersDown
// Make sure that overSlop is not 0f. This can happen when the user drags by exactly
// the touch slop. However, the overSlop we pass to onDragStarted() is used to
// compute the direction we are dragging in, so overSlop should never be 0f unless
@@ -516,7 +526,7 @@
drag
}
- val controller = onDragStart(lastPointersInfo, overSlop)
+ val controller = onDragStart(lastPointersDown, overSlop)
val successful: Boolean
try {
@@ -666,48 +676,31 @@
fun pointersInfo(): PointersInfo?
}
-/**
- * Holds information about pointer interactions within a composable.
- *
- * This class stores details such as the starting position of a gesture, the number of pointers
- * down, and whether the last pointer event was a mouse wheel scroll.
- *
- * @param startedPosition The starting position of the gesture. This is the position where the first
- * pointer touched the screen, not necessarily the point where dragging begins. This may be
- * different from the initial touch position if a child composable intercepts the gesture before
- * this one.
- * @param pointersDown The number of pointers currently down.
- * @param isMouseWheel Indicates whether the last pointer event was a mouse wheel scroll.
- * @param pointersDownByType Provide a map of pointer types to the count of pointers of that type
- * currently down/pressed.
- */
-internal data class PointersInfo(
- val startedPosition: Offset,
- val pointersDown: Int,
- val isMouseWheel: Boolean,
- val pointersDownByType: Map<PointerType, Int>,
-) {
- init {
- check(pointersDown > 0) { "We should have at least 1 pointer down, $pointersDown instead" }
+internal sealed interface PointersInfo {
+ /**
+ * Holds information about pointer interactions within a composable.
+ *
+ * This class stores details such as the starting position of a gesture, the number of pointers
+ * down, and whether the last pointer event was a mouse wheel scroll.
+ *
+ * @param startedPosition The starting position of the gesture. This is the position where the
+ * first pointer touched the screen, not necessarily the point where dragging begins. This may
+ * be different from the initial touch position if a child composable intercepts the gesture
+ * before this one.
+ * @param count The number of pointers currently down.
+ * @param countByType Provide a map of pointer types to the count of pointers of that type
+ * currently down/pressed.
+ */
+ data class PointersDown(
+ val startedPosition: Offset,
+ val count: Int,
+ val countByType: Map<PointerType, Int>,
+ ) : PointersInfo {
+ init {
+ check(count > 0) { "We should have at least 1 pointer down, $count instead" }
+ }
}
-}
-private fun PointersInfo(
- startedPosition: Offset,
- pointersDown: Int,
- lastPointerEvent: PointerEvent,
-): PointersInfo {
- return PointersInfo(
- startedPosition = startedPosition,
- pointersDown = pointersDown,
- isMouseWheel = lastPointerEvent.type == PointerEventType.Scroll,
- pointersDownByType =
- buildMap {
- lastPointerEvent.changes.fastForEach { change ->
- if (!change.pressed) return@fastForEach
- val newValue = (get(change.type) ?: 0) + 1
- put(change.type, newValue)
- }
- },
- )
+ /** Indicates whether the last pointer event was a mouse wheel scroll. */
+ data object MouseWheel : PointersInfo
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 61332b6..72b29ee 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -25,17 +25,13 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transformation.SharedElementTransformation
-import com.android.compose.animation.scene.transition.link.LinkedTransition
-import com.android.compose.animation.scene.transition.link.StateLink
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
/**
@@ -236,7 +232,6 @@
canShowOverlay: (OverlayKey) -> Boolean = { true },
canHideOverlay: (OverlayKey) -> Boolean = { true },
canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
- stateLinks: List<StateLink> = emptyList(),
): MutableSceneTransitionLayoutState {
return MutableSceneTransitionLayoutStateImpl(
initialScene,
@@ -246,7 +241,6 @@
canShowOverlay,
canHideOverlay,
canReplaceOverlay,
- stateLinks,
)
}
@@ -258,10 +252,7 @@
internal val canChangeScene: (SceneKey) -> Boolean = { true },
internal val canShowOverlay: (OverlayKey) -> Boolean = { true },
internal val canHideOverlay: (OverlayKey) -> Boolean = { true },
- internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ ->
- true
- },
- private val stateLinks: List<StateLink> = emptyList(),
+ internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
) : MutableSceneTransitionLayoutState {
private val creationThread: Thread = Thread.currentThread()
@@ -364,20 +355,11 @@
checkThread()
try {
- // Keep a reference to the previous transition (if any).
- val previousTransition = currentTransition
-
// Start the transition.
startTransitionInternal(transition, chain)
- // Handle transition links.
- previousTransition?.let { cancelActiveTransitionLinks(it) }
- if (stateLinks.isNotEmpty()) {
- coroutineScope { setupTransitionLinks(transition) }
- }
-
// Run the transition until it is finished.
- transition.run()
+ transition.runInternal()
} finally {
finishTransition(transition)
}
@@ -471,42 +453,6 @@
)
}
- private fun cancelActiveTransitionLinks(transition: TransitionState.Transition) {
- transition.activeTransitionLinks.forEach { (link, linkedTransition) ->
- link.target.finishTransition(linkedTransition)
- }
- transition.activeTransitionLinks.clear()
- }
-
- private fun CoroutineScope.setupTransitionLinks(transition: TransitionState.Transition) {
- stateLinks.fastForEach { stateLink ->
- val matchingLinks =
- stateLink.transitionLinks.fastFilter { it.isMatchingLink(transition) }
- if (matchingLinks.isEmpty()) return@fastForEach
- if (matchingLinks.size > 1) error("More than one link matched.")
-
- val targetCurrentScene = stateLink.target.transitionState.currentScene
- val matchingLink = matchingLinks[0]
-
- if (!matchingLink.targetIsInValidState(targetCurrentScene)) return@fastForEach
-
- val linkedTransition =
- LinkedTransition(
- originalTransition = transition,
- fromScene = targetCurrentScene,
- toScene = matchingLink.targetTo,
- key = matchingLink.targetTransitionKey,
- )
-
- // Start with UNDISPATCHED so that startTransition is called directly and the new linked
- // transition is observable directly.
- launch(start = CoroutineStart.UNDISPATCHED) {
- stateLink.target.startTransition(linkedTransition)
- }
- transition.activeTransitionLinks[stateLink] = linkedTransition
- }
- }
-
/**
* Notify that [transition] was finished and that it settled to its
* [currentScene][TransitionState.currentScene]. This will do nothing if [transition] was
@@ -535,9 +481,6 @@
// Mark this transition as finished.
finishedTransitions.add(transition)
- // Finish all linked transitions.
- finishActiveTransitionLinks(transition)
-
// Keep a reference to the last transition, in case we remove all transitions and should
// settle to Idle.
val lastTransition = transitionStates.last()
@@ -584,13 +527,6 @@
transitionStates = listOf(TransitionState.Idle(scene, currentOverlays))
}
- private fun finishActiveTransitionLinks(transition: TransitionState.Transition) {
- for ((link, linkedTransition) in transition.activeTransitionLinks) {
- link.target.finishTransition(linkedTransition)
- }
- transition.activeTransitionLinks.clear()
- }
-
/**
* Check if a transition is in progress. If the progress value is near 0 or 1, immediately snap
* to the closest scene.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index ba5f414..a448ee4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -56,7 +56,7 @@
}
/** Whether swipe should be enabled in the given [orientation]. */
-private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
+internal fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
if (userActions.isEmpty()) {
return false
}
@@ -200,10 +200,10 @@
override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput()
- private fun startDragImmediately(pointersInfo: PointersInfo): Boolean {
+ private fun startDragImmediately(pointersDown: PointersInfo.PointersDown): Boolean {
// Immediately start the drag if the user can't swipe in the other direction and the gesture
// handler can intercept it.
- return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(pointersInfo)
+ return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(pointersDown)
}
private fun canOppositeSwipe(): Boolean {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index 7d29a68..e3118d67 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -18,6 +18,7 @@
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Stable
@@ -34,8 +35,6 @@
import com.android.compose.animation.scene.TransformationSpec
import com.android.compose.animation.scene.TransformationSpecImpl
import com.android.compose.animation.scene.TransitionKey
-import com.android.compose.animation.scene.transition.link.LinkedTransition
-import com.android.compose.animation.scene.transition.link.StateLink
import kotlinx.coroutines.launch
/** The state associated to a [SceneTransitionLayout] at some specific point in time. */
@@ -280,8 +279,8 @@
*/
private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null
- /** The map of active links that connects this transition to other transitions. */
- internal val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()
+ /** Whether this transition was already started. */
+ private var wasStarted = false
init {
check(fromContent != toContent)
@@ -328,7 +327,7 @@
}
/** Run this transition and return once it is finished. */
- abstract suspend fun run()
+ protected abstract suspend fun run()
/**
* Freeze this transition state so that neither [currentScene] nor [currentOverlays] will
@@ -341,6 +340,13 @@
*/
abstract fun freezeAndAnimateToCurrentState()
+ internal suspend fun runInternal() {
+ check(!wasStarted) { "A Transition can be started only once." }
+ wasStarted = true
+
+ run()
+ }
+
internal fun updateOverscrollSpecs(
fromSpec: OverscrollSpecImpl?,
toSpec: OverscrollSpecImpl?,
@@ -376,7 +382,7 @@
val progressSpec =
spring(
stiffness = swipeSpec.stiffness,
- dampingRatio = swipeSpec.dampingRatio,
+ dampingRatio = Spring.DampingRatioNoBouncy,
visibilityThreshold = ProgressVisibilityThreshold,
)
animatable.animateTo(0f, progressSpec)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
deleted file mode 100644
index 42ba9ba..0000000
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
+++ /dev/null
@@ -1,59 +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.compose.animation.scene.transition.link
-
-import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.TransitionKey
-import com.android.compose.animation.scene.content.state.TransitionState
-
-/** A linked transition which is driven by a [originalTransition]. */
-internal class LinkedTransition(
- private val originalTransition: TransitionState.Transition,
- fromScene: SceneKey,
- toScene: SceneKey,
- override val key: TransitionKey? = null,
-) : TransitionState.Transition.ChangeScene(fromScene, toScene) {
-
- override val currentScene: SceneKey
- get() {
- return when (originalTransition.currentScene) {
- originalTransition.fromContent -> fromScene
- originalTransition.toContent -> toScene
- else -> error("Original currentScene is neither FromScene nor ToScene")
- }
- }
-
- override val isInitiatedByUserInput: Boolean
- get() = originalTransition.isInitiatedByUserInput
-
- override val isUserInputOngoing: Boolean
- get() = originalTransition.isUserInputOngoing
-
- override val progress: Float
- get() = originalTransition.progress
-
- override val progressVelocity: Float
- get() = originalTransition.progressVelocity
-
- override suspend fun run() {
- originalTransition.run()
- }
-
- override fun freezeAndAnimateToCurrentState() {
- originalTransition.freezeAndAnimateToCurrentState()
- }
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
deleted file mode 100644
index 2aec509..0000000
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
+++ /dev/null
@@ -1,63 +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.compose.animation.scene.transition.link
-
-import com.android.compose.animation.scene.ContentKey
-import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl
-import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.SceneTransitionLayoutState
-import com.android.compose.animation.scene.TransitionKey
-import com.android.compose.animation.scene.content.state.TransitionState
-
-/** A link between a source (implicit) and [target] `SceneTransitionLayoutState`. */
-class StateLink(target: SceneTransitionLayoutState, val transitionLinks: List<TransitionLink>) {
-
- internal val target = target as MutableSceneTransitionLayoutStateImpl
-
- /**
- * Links two transitions (source and target) together.
- *
- * `null` can be passed to indicate that any SceneKey should match. e.g. passing `null`, `null`,
- * `null`, `SceneA` means that any transition at the source will trigger a transition in the
- * target to `SceneA` from any current scene.
- */
- class TransitionLink(
- val sourceFrom: ContentKey?,
- val sourceTo: ContentKey?,
- val targetFrom: SceneKey?,
- val targetTo: SceneKey,
- val targetTransitionKey: TransitionKey? = null,
- ) {
- init {
- if (
- (sourceFrom != null && sourceFrom == sourceTo) ||
- (targetFrom != null && targetFrom == targetTo)
- )
- error("From and To can't be the same")
- }
-
- internal fun isMatchingLink(transition: TransitionState.Transition): Boolean {
- return (sourceFrom == null || sourceFrom == transition.fromContent) &&
- (sourceTo == null || sourceTo == transition.toContent)
- }
-
- internal fun targetIsInValidState(targetCurrentContent: ContentKey): Boolean {
- return (targetFrom == null || targetFrom == targetCurrentContent) &&
- targetTo != targetCurrentContent
- }
- }
-}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 098673e..7e6f3a8 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -52,17 +52,15 @@
private const val SCREEN_SIZE = 100f
private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt())
-private fun pointersInfo(
+private fun pointersDown(
startedPosition: Offset = Offset.Zero,
pointersDown: Int = 1,
- isMouseWheel: Boolean = false,
pointersDownByType: Map<PointerType, Int> = mapOf(PointerType.Touch to pointersDown),
-): PointersInfo {
- return PointersInfo(
+): PointersInfo.PointersDown {
+ return PointersInfo.PointersDown(
startedPosition = startedPosition,
- pointersDown = pointersDown,
- isMouseWheel = isMouseWheel,
- pointersDownByType = pointersDownByType,
+ count = pointersDown,
+ countByType = pointersDownByType,
)
}
@@ -138,7 +136,7 @@
val draggableHandler = layoutImpl.draggableHandler(Orientation.Vertical)
val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal)
- var pointerInfoOwner: () -> PointersInfo = { pointersInfo() }
+ var pointerInfoOwner: () -> PointersInfo = { pointersDown() }
fun nestedScrollConnection(
nestedScrollBehavior: NestedScrollBehavior,
@@ -221,7 +219,7 @@
}
fun onDragStarted(
- pointersInfo: PointersInfo = pointersInfo(),
+ pointersInfo: PointersInfo.PointersDown = pointersDown(),
overSlop: Float,
expectedConsumedOverSlop: Float = overSlop,
): DragController {
@@ -235,18 +233,20 @@
)
}
- fun onDragStartedImmediately(pointersInfo: PointersInfo = pointersInfo()): DragController {
+ fun onDragStartedImmediately(
+ pointersInfo: PointersInfo.PointersDown = pointersDown()
+ ): DragController {
return onDragStarted(draggableHandler, pointersInfo, overSlop = 0f)
}
fun onDragStarted(
draggableHandler: DraggableHandler,
- pointersInfo: PointersInfo = pointersInfo(),
+ pointersInfo: PointersInfo.PointersDown = pointersDown(),
overSlop: Float = 0f,
expectedConsumedOverSlop: Float = overSlop,
): DragController {
val dragController =
- draggableHandler.onDragStarted(pointersInfo = pointersInfo, overSlop = overSlop)
+ draggableHandler.onDragStarted(pointersDown = pointersInfo, overSlop = overSlop)
// MultiPointerDraggable will always call onDelta with the initial overSlop right after
dragController.onDragDelta(pixels = overSlop, expectedConsumedOverSlop)
@@ -529,7 +529,7 @@
val dragController =
onDragStarted(
pointersInfo =
- pointersInfo(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE * 0.5f)),
+ pointersDown(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE * 0.5f)),
overSlop = up(fractionOfScreen = 0.2f),
)
assertTransition(
@@ -555,7 +555,7 @@
// Start dragging from the bottom
onDragStarted(
- pointersInfo = pointersInfo(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE)),
+ pointersInfo = pointersDown(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE)),
overSlop = up(fractionOfScreen = 0.1f),
)
assertTransition(
@@ -1052,7 +1052,7 @@
navigateToSceneC()
// Swipe up from the middle to transition to scene B.
- val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+ val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
onDragStarted(pointersInfo = middle, overSlop = up(0.1f))
assertTransition(
currentScene = SceneC,
@@ -1084,7 +1084,7 @@
// Start a new gesture from the bottom of the screen. Because swiping up from the bottom of
// C leads to scene A (and not B), the previous transitions is *not* intercepted and we
// instead animate from C to A.
- val bottom = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2, SCREEN_SIZE))
+ val bottom = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2, SCREEN_SIZE))
assertThat(draggableHandler.shouldImmediatelyIntercept(bottom)).isFalse()
onDragStarted(pointersInfo = bottom, overSlop = up(0.1f))
@@ -1103,7 +1103,7 @@
navigateToSceneC()
// Swipe up from the middle to transition to scene B.
- val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+ val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
onDragStarted(pointersInfo = middle, overSlop = up(0.1f))
assertTransition(fromScene = SceneC, toScene = SceneB, isUserInputOngoing = true)
@@ -1120,15 +1120,15 @@
@Test
fun interruptedTransitionCanNotBeImmediatelyIntercepted() = runGestureTest {
- assertThat(draggableHandler.shouldImmediatelyIntercept(pointersInfo = null)).isFalse()
+ assertThat(draggableHandler.shouldImmediatelyIntercept(pointersDown = null)).isFalse()
onDragStarted(overSlop = up(0.1f))
- assertThat(draggableHandler.shouldImmediatelyIntercept(pointersInfo = null)).isTrue()
+ assertThat(draggableHandler.shouldImmediatelyIntercept(pointersDown = null)).isTrue()
layoutState.startTransitionImmediately(
animationScope = testScope.backgroundScope,
transition(SceneA, SceneB),
)
- assertThat(draggableHandler.shouldImmediatelyIntercept(pointersInfo = null)).isFalse()
+ assertThat(draggableHandler.shouldImmediatelyIntercept(pointersDown = null)).isFalse()
}
@Test
@@ -1160,7 +1160,7 @@
assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB)
// Intercept the transition and swipe down back to scene A.
- assertThat(draggableHandler.shouldImmediatelyIntercept(pointersInfo = null)).isTrue()
+ assertThat(draggableHandler.shouldImmediatelyIntercept(pointersDown = null)).isTrue()
val dragController2 = onDragStartedImmediately()
// Block the transition when the user release their finger.
@@ -1204,7 +1204,7 @@
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
// Drag from the **top** of the screen
- pointerInfoOwner = { pointersInfo() }
+ pointerInfoOwner = { pointersDown() }
assertIdle(currentScene = SceneC)
nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
@@ -1221,7 +1221,7 @@
advanceUntilIdle()
// Drag from the **bottom** of the screen
- pointerInfoOwner = { pointersInfo(startedPosition = Offset(0f, SCREEN_SIZE)) }
+ pointerInfoOwner = { pointersDown(startedPosition = Offset(0f, SCREEN_SIZE)) }
assertIdle(currentScene = SceneC)
nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
@@ -1241,7 +1241,7 @@
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
// Use mouse wheel
- pointerInfoOwner = { pointersInfo(isMouseWheel = true) }
+ pointerInfoOwner = { PointersInfo.MouseWheel }
assertIdle(currentScene = SceneC)
nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
@@ -1251,7 +1251,7 @@
@Test
fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest {
// Swipe up from the middle to transition to scene B.
- val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+ val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.1f))
assertTransition(fromScene = SceneA, toScene = SceneB, isUserInputOngoing = true)
@@ -1265,7 +1265,7 @@
layoutState.transitions = transitions { overscrollDisabled(SceneB, Orientation.Vertical) }
// Swipe up to scene B at progress = 200%.
- val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+ val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
val dragController =
onDragStarted(
pointersInfo = middle,
@@ -1296,7 +1296,7 @@
layoutState.transitions = transitions { overscrollDisabled(SceneB, Orientation.Vertical) }
// Swipe up to scene B at progress = 200%.
- val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+ val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.99f))
assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.99f)
@@ -1342,7 +1342,7 @@
overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) }
}
- val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+ val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.5f))
val transition = assertThat(transitionState).isSceneTransition()
@@ -1374,7 +1374,7 @@
overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) }
}
- val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+ val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
val dragController = onDragStarted(pointersInfo = middle, overSlop = down(0.5f))
val transition = assertThat(transitionState).isSceneTransition()
@@ -1405,7 +1405,7 @@
overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) }
}
- val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+ val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
val dragController = onDragStarted(pointersInfo = middle, overSlop = up(1.5f))
val transition = assertThat(transitionState).isSceneTransition()
@@ -1437,7 +1437,7 @@
overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) }
}
- val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+ val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
val dragController = onDragStarted(pointersInfo = middle, overSlop = down(1.5f))
val transition = assertThat(transitionState).isSceneTransition()
@@ -1471,7 +1471,7 @@
mutableUserActionsA = mapOf(Swipe.Up to UserActionResult(SceneB))
- val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+ val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
val dragController = onDragStarted(pointersInfo = middle, overSlop = down(1f))
val transition = assertThat(transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
@@ -1504,7 +1504,7 @@
mutableUserActionsA = mapOf(Swipe.Down to UserActionResult(SceneC))
- val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+ val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
val dragController = onDragStarted(pointersInfo = middle, overSlop = up(1f))
val transition = assertThat(transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
@@ -1676,4 +1676,33 @@
assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
}
+
+ @Test
+ fun replaceOverlayNestedScroll() = runGestureTest {
+ layoutState.showOverlay(OverlayA, animationScope = testScope)
+ advanceUntilIdle()
+
+ // Initial state.
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+ assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayA)
+
+ // Swipe down to replace overlay A by overlay B.
+
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
+ nestedScroll.scroll(downOffset(0.1f))
+ val transition = assertThat(layoutState.transitionState).isReplaceOverlayTransition()
+ assertThat(transition).hasCurrentScene(SceneA)
+ assertThat(transition).hasFromOverlay(OverlayA)
+ assertThat(transition).hasToOverlay(OverlayB)
+ assertThat(transition).hasCurrentOverlays(OverlayA)
+ assertThat(transition).hasProgress(0.1f)
+
+ nestedScroll.preFling(Velocity(0f, velocityThreshold))
+ advanceUntilIdle()
+ // Commit the gesture. The overlays are instantly swapped in the set of current overlays.
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+ assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index a2b263b..2b70908 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -26,10 +26,8 @@
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
-import com.android.compose.animation.scene.TestScenes.SceneD
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.subjects.assertThat
-import com.android.compose.animation.scene.transition.link.StateLink
import com.android.compose.animation.scene.transition.seekToScene
import com.android.compose.test.MonotonicClockTestScope
import com.android.compose.test.TestSceneTransition
@@ -42,6 +40,7 @@
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows
import org.junit.Rule
@@ -132,147 +131,6 @@
assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB))
}
- private fun setupLinkedStates(
- parentInitialScene: SceneKey = SceneC,
- childInitialScene: SceneKey = SceneA,
- sourceFrom: SceneKey? = SceneA,
- sourceTo: SceneKey? = SceneB,
- targetFrom: SceneKey? = SceneC,
- targetTo: SceneKey = SceneD,
- ): Pair<MutableSceneTransitionLayoutStateImpl, MutableSceneTransitionLayoutStateImpl> {
- val parentState = MutableSceneTransitionLayoutState(parentInitialScene)
- val link =
- listOf(
- StateLink(
- parentState,
- listOf(StateLink.TransitionLink(sourceFrom, sourceTo, targetFrom, targetTo)),
- )
- )
- val childState = MutableSceneTransitionLayoutState(childInitialScene, stateLinks = link)
- return Pair(
- parentState as MutableSceneTransitionLayoutStateImpl,
- childState as MutableSceneTransitionLayoutStateImpl,
- )
- }
-
- @Test
- fun linkedTransition_startsLinkAndFinishesLinkInToState() = runTest {
- val (parentState, childState) = setupLinkedStates()
-
- val childTransition = transition(SceneA, SceneB)
-
- val job =
- childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
- assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
- assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
-
- childTransition.finish()
- job.join()
- assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
- assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD))
- }
-
- @Test
- fun linkedTransition_transitiveLink() = runTest {
- val parentParentState =
- MutableSceneTransitionLayoutState(SceneB) as MutableSceneTransitionLayoutStateImpl
- val parentLink =
- listOf(
- StateLink(
- parentParentState,
- listOf(StateLink.TransitionLink(SceneC, SceneD, SceneB, SceneC)),
- )
- )
- val parentState =
- MutableSceneTransitionLayoutState(SceneC, stateLinks = parentLink)
- as MutableSceneTransitionLayoutStateImpl
- val link =
- listOf(
- StateLink(
- parentState,
- listOf(StateLink.TransitionLink(SceneA, SceneB, SceneC, SceneD)),
- )
- )
- val childState =
- MutableSceneTransitionLayoutState(SceneA, stateLinks = link)
- as MutableSceneTransitionLayoutStateImpl
-
- val childTransition = transition(SceneA, SceneB)
-
- val job =
- childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
- assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
- assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
- assertThat(parentParentState.isTransitioning(SceneB, SceneC)).isTrue()
-
- childTransition.finish()
- job.join()
- assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
- assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD))
- assertThat(parentParentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
- }
-
- @Test
- fun linkedTransition_linkProgressIsEqual() = runTest {
- val (parentState, childState) = setupLinkedStates()
-
- var progress = 0f
- val childTransition = transition(SceneA, SceneB, progress = { progress })
-
- childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
- assertThat(parentState.currentTransition?.progress).isEqualTo(0f)
-
- progress = .5f
- assertThat(parentState.currentTransition?.progress).isEqualTo(.5f)
- }
-
- @Test
- fun linkedTransition_reverseTransitionIsNotLinked() = runTest {
- val (parentState, childState) = setupLinkedStates()
-
- val childTransition = transition(SceneB, SceneA, current = { SceneB })
-
- val job =
- childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
- assertThat(childState.isTransitioning(SceneB, SceneA)).isTrue()
- assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
-
- childTransition.finish()
- job.join()
- assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
- assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
- }
-
- @Test
- fun linkedTransition_startsLinkAndFinishesLinkInFromState() = runTest {
- val (parentState, childState) = setupLinkedStates()
-
- val childTransition = transition(SceneA, SceneB, current = { SceneA })
- val job =
- childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
-
- childTransition.finish()
- job.join()
- assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneA))
- assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
- }
-
- @Test
- fun linkedTransition_startsLinkButLinkedStateIsTakenOver() = runTest {
- val (parentState, childState) = setupLinkedStates()
-
- val childTransition = transition(SceneA, SceneB)
- val parentTransition = transition(SceneC, SceneA)
- val job =
- childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
- parentState.startTransitionImmediately(animationScope = backgroundScope, parentTransition)
-
- childTransition.finish()
- job.join()
- assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
- assertThat(parentState.transitionState).isEqualTo(parentTransition)
- }
-
@Test
fun setTargetScene_withTransitionKey() = runMonotonicClockTest {
val transitionkey = TransitionKey(debugName = "foo")
@@ -393,51 +251,6 @@
assertThat(state.isTransitioning()).isTrue()
}
- @Test
- fun linkedTransition_fuzzyLinksAreMatchedAndStarted() = runTest {
- val (parentState, childState) = setupLinkedStates(SceneC, SceneA, null, null, null, SceneD)
- val childTransition = transition(SceneA, SceneB)
-
- val job =
- childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
- assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
- assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
-
- childTransition.finish()
- job.join()
- assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
- assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD))
- }
-
- @Test
- fun linkedTransition_fuzzyLinksAreMatchedAndResetToProperPreviousScene() = runTest {
- val (parentState, childState) =
- setupLinkedStates(SceneC, SceneA, SceneA, null, null, SceneD)
-
- val childTransition = transition(SceneA, SceneB, current = { SceneA })
-
- val job =
- childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
- assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
- assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
-
- childTransition.finish()
- job.join()
- assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneA))
- assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
- }
-
- @Test
- fun linkedTransition_fuzzyLinksAreNotMatched() = runTest {
- val (parentState, childState) =
- setupLinkedStates(SceneC, SceneA, SceneB, null, SceneC, SceneD)
- val childTransition = transition(SceneA, SceneB)
-
- childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
- assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
- assertThat(parentState.isTransitioning(SceneC, SceneD)).isFalse()
- }
-
private fun MonotonicClockTestScope.startOverscrollableTransistionFromAtoB(
progress: () -> Float,
sceneTransitions: SceneTransitions,
@@ -777,4 +590,15 @@
assertThat(transition.progressTo(SceneA)).isEqualTo(1f - 0.2f)
assertThrows(IllegalArgumentException::class.java) { transition.progressTo(SceneC) }
}
+
+ @Test
+ fun transitionCanBeStartedOnlyOnce() = runTest {
+ val state = MutableSceneTransitionLayoutState(SceneA)
+ val transition = transition(from = SceneA, to = SceneB)
+
+ state.startTransitionImmediately(backgroundScope, transition)
+ assertThrows(IllegalStateException::class.java) {
+ runBlocking { state.startTransition(transition) }
+ }
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 3b2ee98..97a96a4 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -574,7 +574,7 @@
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
SceneTransitionLayout(layoutState, Modifier.size(LayoutWidth, LayoutHeight)) {
- scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
+ scene(SceneA, userActions = mapOf(Swipe.Up to SceneB, Swipe.Down to SceneB)) {
Box(
Modifier.fillMaxSize()
// A scrollable that does not consume the scroll gesture
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
deleted file mode 100644
index dd58ea7..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2021 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.keyguard;
-
-import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.hardware.display.DisplayManagerGlobal;
-import android.testing.TestableLooper;
-import android.view.Display;
-import android.view.DisplayInfo;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.navigationbar.NavigationBarController;
-import com.android.systemui.settings.FakeDisplayTracker;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.concurrent.Executor;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-@TestableLooper.RunWithLooper
-public class KeyguardDisplayManagerTest extends SysuiTestCase {
-
- @Mock
- private NavigationBarController mNavigationBarController;
- @Mock
- private ConnectedDisplayKeyguardPresentation.Factory
- mConnectedDisplayKeyguardPresentationFactory;
- @Mock
- private ConnectedDisplayKeyguardPresentation mConnectedDisplayKeyguardPresentation;
- @Mock
- private KeyguardDisplayManager.DeviceStateHelper mDeviceStateHelper;
- @Mock
- private KeyguardStateController mKeyguardStateController;
-
- private Executor mMainExecutor = Runnable::run;
- private Executor mBackgroundExecutor = Runnable::run;
- private KeyguardDisplayManager mManager;
- private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
- // The default and secondary displays are both in the default group
- private Display mDefaultDisplay;
- private Display mSecondaryDisplay;
-
- // This display is in a different group from the default and secondary displays.
- private Display mAlwaysUnlockedDisplay;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mManager = spy(new KeyguardDisplayManager(mContext, () -> mNavigationBarController,
- mDisplayTracker, mMainExecutor, mBackgroundExecutor, mDeviceStateHelper,
- mKeyguardStateController, mConnectedDisplayKeyguardPresentationFactory));
- doReturn(mConnectedDisplayKeyguardPresentation).when(
- mConnectedDisplayKeyguardPresentationFactory).create(any());
- doReturn(mConnectedDisplayKeyguardPresentation).when(mManager)
- .createPresentation(any());
- mDefaultDisplay = new Display(DisplayManagerGlobal.getInstance(), Display.DEFAULT_DISPLAY,
- new DisplayInfo(), DEFAULT_DISPLAY_ADJUSTMENTS);
- mSecondaryDisplay = new Display(DisplayManagerGlobal.getInstance(),
- Display.DEFAULT_DISPLAY + 1,
- new DisplayInfo(), DEFAULT_DISPLAY_ADJUSTMENTS);
-
- DisplayInfo alwaysUnlockedDisplayInfo = new DisplayInfo();
- alwaysUnlockedDisplayInfo.displayId = Display.DEFAULT_DISPLAY + 2;
- alwaysUnlockedDisplayInfo.flags = Display.FLAG_ALWAYS_UNLOCKED;
- mAlwaysUnlockedDisplay = new Display(DisplayManagerGlobal.getInstance(),
- Display.DEFAULT_DISPLAY,
- alwaysUnlockedDisplayInfo, DEFAULT_DISPLAY_ADJUSTMENTS);
- }
-
- @Test
- public void testShow_defaultDisplayOnly() {
- mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay});
- mManager.show();
- verify(mManager, never()).createPresentation(any());
- }
-
- @Test
- public void testShow_includeSecondaryDisplay() {
- mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mSecondaryDisplay});
- mManager.show();
- verify(mManager, times(1)).createPresentation(eq(mSecondaryDisplay));
- }
-
- @Test
- public void testShow_includeAlwaysUnlockedDisplay() {
- mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mAlwaysUnlockedDisplay});
-
- mManager.show();
- verify(mManager, never()).createPresentation(any());
- }
-
- @Test
- public void testShow_includeSecondaryAndAlwaysUnlockedDisplays() {
- mDisplayTracker.setAllDisplays(
- new Display[]{mDefaultDisplay, mSecondaryDisplay, mAlwaysUnlockedDisplay});
-
- mManager.show();
- verify(mManager, times(1)).createPresentation(eq(mSecondaryDisplay));
- }
-
- @Test
- public void testShow_concurrentDisplayActive_occluded() {
- mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mSecondaryDisplay});
-
- when(mDeviceStateHelper.isConcurrentDisplayActive(mSecondaryDisplay)).thenReturn(true);
- when(mKeyguardStateController.isOccluded()).thenReturn(true);
- verify(mManager, never()).createPresentation(eq(mSecondaryDisplay));
- }
-
- @Test
- public void testShow_presentationCreated() {
- when(mManager.createPresentation(any())).thenCallRealMethod();
- mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mSecondaryDisplay});
-
- mManager.show();
-
- verify(mConnectedDisplayKeyguardPresentationFactory).create(eq(mSecondaryDisplay));
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt
new file mode 100644
index 0000000..57a6797
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt
@@ -0,0 +1,225 @@
+/*
+ * 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.keyguard
+
+import android.hardware.display.DisplayManagerGlobal
+import android.platform.test.annotations.EnableFlags
+import android.testing.TestableLooper.RunWithLooper
+import android.view.Display
+import android.view.DisplayAdjustments
+import android.view.DisplayInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardDisplayManager.DeviceStateHelper
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.navigationbar.NavigationBarController
+import com.android.systemui.settings.FakeDisplayTracker
+import com.android.systemui.shade.data.repository.FakeShadePositionRepository
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import java.util.concurrent.Executor
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+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.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+@OptIn(ExperimentalCoroutinesApi::class)
+class KeyguardDisplayManagerTest : SysuiTestCase() {
+ @Mock private val navigationBarController = mock(NavigationBarController::class.java)
+ @Mock
+ private val presentationFactory = mock(ConnectedDisplayKeyguardPresentation.Factory::class.java)
+ @Mock
+ private val connectedDisplayKeyguardPresentation =
+ mock(ConnectedDisplayKeyguardPresentation::class.java)
+ @Mock private val deviceStateHelper = mock(DeviceStateHelper::class.java)
+ @Mock private val keyguardStateController = mock(KeyguardStateController::class.java)
+ private val shadePositionRepository = FakeShadePositionRepository()
+
+ private val mainExecutor = Executor { it.run() }
+ private val backgroundExecutor = Executor { it.run() }
+ private lateinit var manager: KeyguardDisplayManager
+ private val displayTracker = FakeDisplayTracker(mContext)
+ // The default and secondary displays are both in the default group
+ private lateinit var defaultDisplay: Display
+ private lateinit var secondaryDisplay: Display
+
+ private val testScope = TestScope(UnconfinedTestDispatcher())
+
+ // This display is in a different group from the default and secondary displays.
+ private lateinit var alwaysUnlockedDisplay: Display
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ manager =
+ KeyguardDisplayManager(
+ mContext,
+ { navigationBarController },
+ displayTracker,
+ mainExecutor,
+ backgroundExecutor,
+ deviceStateHelper,
+ keyguardStateController,
+ presentationFactory,
+ { shadePositionRepository },
+ testScope.backgroundScope,
+ )
+ whenever(presentationFactory.create(any())).doReturn(connectedDisplayKeyguardPresentation)
+
+ defaultDisplay =
+ Display(
+ DisplayManagerGlobal.getInstance(),
+ Display.DEFAULT_DISPLAY,
+ DisplayInfo(),
+ DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS,
+ )
+ secondaryDisplay =
+ Display(
+ DisplayManagerGlobal.getInstance(),
+ Display.DEFAULT_DISPLAY + 1,
+ DisplayInfo(),
+ DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS,
+ )
+
+ val alwaysUnlockedDisplayInfo = DisplayInfo()
+ alwaysUnlockedDisplayInfo.displayId = Display.DEFAULT_DISPLAY + 2
+ alwaysUnlockedDisplayInfo.flags = Display.FLAG_ALWAYS_UNLOCKED
+ alwaysUnlockedDisplay =
+ Display(
+ DisplayManagerGlobal.getInstance(),
+ Display.DEFAULT_DISPLAY,
+ alwaysUnlockedDisplayInfo,
+ DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS,
+ )
+ }
+
+ @Test
+ fun testShow_defaultDisplayOnly() {
+ displayTracker.allDisplays = arrayOf(defaultDisplay)
+ manager.show()
+ verify(presentationFactory, never()).create(any())
+ }
+
+ @Test
+ fun testShow_includeSecondaryDisplay() {
+ displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay)
+ manager.show()
+ verify(presentationFactory).create(eq(secondaryDisplay))
+ }
+
+ @Test
+ fun testShow_includeAlwaysUnlockedDisplay() {
+ displayTracker.allDisplays = arrayOf(defaultDisplay, alwaysUnlockedDisplay)
+
+ manager.show()
+ verify(presentationFactory, never()).create(any())
+ }
+
+ @Test
+ fun testShow_includeSecondaryAndAlwaysUnlockedDisplays() {
+ displayTracker.allDisplays =
+ arrayOf(defaultDisplay, secondaryDisplay, alwaysUnlockedDisplay)
+
+ manager.show()
+ verify(presentationFactory).create(eq(secondaryDisplay))
+ }
+
+ @Test
+ fun testShow_concurrentDisplayActive_occluded() {
+ displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay)
+
+ whenever(deviceStateHelper.isConcurrentDisplayActive(secondaryDisplay)).thenReturn(true)
+ whenever(keyguardStateController.isOccluded).thenReturn(true)
+ verify(presentationFactory, never()).create(eq(secondaryDisplay))
+ }
+
+ @Test
+ fun testShow_presentationCreated() {
+ displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay)
+
+ manager.show()
+
+ verify(presentationFactory).create(eq(secondaryDisplay))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+ fun show_shadeMovesDisplay_newPresentationCreated() {
+ displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay)
+ // Shade in the default display, we expect the presentation to be in the secondary only
+ shadePositionRepository.setDisplayId(defaultDisplay.displayId)
+
+ manager.show()
+
+ verify(presentationFactory).create(eq(secondaryDisplay))
+ verify(presentationFactory, never()).create(eq(defaultDisplay))
+ reset(presentationFactory)
+ whenever(presentationFactory.create(any())).thenReturn(connectedDisplayKeyguardPresentation)
+
+ // Let's move it to the secondary display. We expect it will be added in the default
+ // one.
+ shadePositionRepository.setDisplayId(secondaryDisplay.displayId)
+ testScope.advanceUntilIdle()
+
+ verify(presentationFactory).create(eq(defaultDisplay))
+ reset(presentationFactory)
+ whenever(presentationFactory.create(any())).thenReturn(connectedDisplayKeyguardPresentation)
+
+ // Let's move it back! it should be re-created (it means it was removed before)
+ shadePositionRepository.setDisplayId(defaultDisplay.displayId)
+ testScope.advanceUntilIdle()
+
+ verify(presentationFactory).create(eq(secondaryDisplay))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+ fun show_shadeInSecondaryDisplay_defaultOneHasPresentation() {
+ displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay)
+ shadePositionRepository.setDisplayId(secondaryDisplay.displayId)
+
+ manager.show()
+
+ verify(presentationFactory).create(eq(defaultDisplay))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+ fun show_shadeInDefaultDisplay_secondaryOneHasPresentation() {
+ displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay)
+ shadePositionRepository.setDisplayId(defaultDisplay.displayId)
+
+ manager.show()
+
+ verify(presentationFactory).create(eq(secondaryDisplay))
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
new file mode 100644
index 0000000..c646d8f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
@@ -0,0 +1,291 @@
+/*
+ * 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.keyboard.shortcut.data.repository
+
+import android.content.Context
+import android.content.Context.INPUT_SERVICE
+import android.hardware.input.InputGestureData
+import android.hardware.input.InputGestureData.createKeyTrigger
+import android.hardware.input.KeyGestureEvent
+import android.hardware.input.fakeInputManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.KeyEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.hardware.input.Flags.FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES
+import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyboard.shortcut.customShortcutCategoriesRepository
+import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.settings.userTracker
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() {
+
+ private val mockUserContext: Context = mock()
+ private val kosmos =
+ testKosmos().also {
+ it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
+ }
+
+ private val fakeInputManager = kosmos.fakeInputManager
+ private val testScope = kosmos.testScope
+ private val helper = kosmos.shortcutHelperTestHelper
+ private val repo = kosmos.customShortcutCategoriesRepository
+
+ @Before
+ fun setup() {
+ whenever(mockUserContext.getSystemService(INPUT_SERVICE))
+ .thenReturn(fakeInputManager.inputManager)
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+ fun categories_emitsCorrectlyConvertedShortcutCategories() {
+ testScope.runTest {
+ whenever(
+ fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
+ )
+ .thenReturn(customizableInputGesturesWithSimpleShortcutCombinations)
+
+ helper.toggle(deviceId = 123)
+ val categories by collectLastValue(repo.categories)
+
+ assertThat(categories)
+ .containsExactlyElementsIn(expectedShortcutCategoriesWithSimpleShortcutCombination)
+ }
+ }
+
+ @Test
+ @DisableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+ fun categories_emitsEmptyListWhenFlagIsDisabled() {
+ testScope.runTest {
+ whenever(
+ fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
+ )
+ .thenReturn(customizableInputGesturesWithSimpleShortcutCombinations)
+
+ helper.toggle(deviceId = 123)
+ val categories by collectLastValue(repo.categories)
+
+ assertThat(categories).isEmpty()
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+ fun categories_ignoresUnknownKeyGestureTypes() {
+ testScope.runTest {
+ whenever(
+ fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
+ )
+ .thenReturn(customizableInputGestureWithUnknownKeyGestureType)
+
+ helper.toggle(deviceId = 123)
+ val categories by collectLastValue(repo.categories)
+
+ assertThat(categories).isEmpty()
+ }
+ }
+
+ private fun simpleInputGestureData(
+ keyCode: Int = KeyEvent.KEYCODE_A,
+ modifiers: Int = KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON,
+ keyGestureType: Int,
+ ): InputGestureData {
+ val builder = InputGestureData.Builder()
+ builder.setKeyGestureType(keyGestureType)
+ builder.setTrigger(createKeyTrigger(keyCode, modifiers))
+ return builder.build()
+ }
+
+ private fun simpleShortcutCategory(
+ category: ShortcutCategoryType,
+ subcategoryLabel: String,
+ shortcutLabel: String,
+ ): ShortcutCategory {
+ return ShortcutCategory(
+ type = category,
+ subCategories =
+ listOf(
+ ShortcutSubCategory(
+ label = subcategoryLabel,
+ shortcuts = listOf(simpleShortcut(shortcutLabel)),
+ )
+ ),
+ )
+ }
+
+ private fun simpleShortcut(label: String) =
+ Shortcut(
+ label = label,
+ commands =
+ listOf(
+ ShortcutCommand(
+ isCustom = true,
+ keys =
+ listOf(
+ ShortcutKey.Text("Ctrl"),
+ ShortcutKey.Text("Alt"),
+ ShortcutKey.Text("A"),
+ ),
+ )
+ ),
+ )
+
+ private val customizableInputGestureWithUnknownKeyGestureType =
+ // These key gesture events are currently not supported by shortcut helper customizer
+ listOf(
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS
+ ),
+ simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_MEDIA_KEY),
+ )
+
+ private val expectedShortcutCategoriesWithSimpleShortcutCombination =
+ listOf(
+ simpleShortcutCategory(System, "System apps", "Open assistant"),
+ simpleShortcutCategory(System, "System controls", "Go to home screen"),
+ simpleShortcutCategory(System, "System apps", "Open settings"),
+ simpleShortcutCategory(System, "System controls", "Lock screen"),
+ simpleShortcutCategory(System, "System controls", "View notifications"),
+ simpleShortcutCategory(System, "System apps", "Take a note"),
+ simpleShortcutCategory(System, "System controls", "Take screenshot"),
+ simpleShortcutCategory(System, "System controls", "Go back"),
+ simpleShortcutCategory(
+ MultiTasking,
+ "Split screen",
+ "Switch from split screen to full screen",
+ ),
+ simpleShortcutCategory(
+ MultiTasking,
+ "Split screen",
+ "Use split screen with current app on the left",
+ ),
+ simpleShortcutCategory(
+ MultiTasking,
+ "Split screen",
+ "Switch to app on left or above while using split screen",
+ ),
+ simpleShortcutCategory(
+ MultiTasking,
+ "Split screen",
+ "Use split screen with current app on the right",
+ ),
+ simpleShortcutCategory(
+ MultiTasking,
+ "Split screen",
+ "Switch to app on right or below while using split screen",
+ ),
+ simpleShortcutCategory(System, "System controls", "Show shortcuts"),
+ simpleShortcutCategory(System, "System controls", "View recent apps"),
+ simpleShortcutCategory(AppCategories, "Applications", "Calculator"),
+ simpleShortcutCategory(AppCategories, "Applications", "Calendar"),
+ simpleShortcutCategory(AppCategories, "Applications", "Chrome"),
+ simpleShortcutCategory(AppCategories, "Applications", "Contacts"),
+ simpleShortcutCategory(AppCategories, "Applications", "Gmail"),
+ simpleShortcutCategory(AppCategories, "Applications", "Maps"),
+ simpleShortcutCategory(AppCategories, "Applications", "Messages"),
+ simpleShortcutCategory(MultiTasking, "Recent apps", "Cycle forward through recent apps"),
+ )
+
+ private val customizableInputGesturesWithSimpleShortcutCombinations =
+ listOf(
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT
+ ),
+ simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS
+ ),
+ simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL
+ ),
+ simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT
+ ),
+ simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_BACK),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION
+ ),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT
+ ),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT
+ ),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT
+ ),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT
+ ),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER
+ ),
+ simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR
+ ),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR
+ ),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER
+ ),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS
+ ),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL
+ ),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS
+ ),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING
+ ),
+ simpleInputGestureData(
+ keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER
+ ),
+ )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt
similarity index 98%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt
index 620b8b6..f90ab1f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt
@@ -40,6 +40,7 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
+import com.android.systemui.keyboard.shortcut.defaultShortcutCategoriesRepository
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
@@ -47,7 +48,6 @@
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
-import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesRepository
import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
@@ -71,7 +71,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
-class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() {
+class DefaultShortcutCategoriesRepositoryTest : SysuiTestCase() {
private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()
@@ -87,7 +87,7 @@
it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
}
- private val repo = kosmos.shortcutHelperCategoriesRepository
+ private val repo = kosmos.defaultShortcutCategoriesRepository
private val helper = kosmos.shortcutHelperTestHelper
private val testScope = kosmos.testScope
private val fakeInputManager = kosmos.fakeInputManager
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
index a862fdf..778e822 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
@@ -19,6 +19,8 @@
import android.content.Context
import android.content.res.Configuration
import android.graphics.Rect
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.view.Display
import android.view.DisplayCutout
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -29,6 +31,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.commandline.CommandRegistry
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.leak.RotationUtils
import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
@@ -1051,7 +1054,8 @@
}
@Test
- fun onMaxBoundsChanged_beforeStart_listenerNotNotified() {
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun onMaxBoundsChanged_beforeStart_flagEnabled_listenerNotNotified() {
// Start out with an existing configuration with bounds
configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100)
configurationController.onConfigurationChanged(configuration)
@@ -1083,7 +1087,41 @@
}
@Test
- fun onDensityOrFontScaleChanged_beforeStart_listenerNotNotified() {
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun onMaxBoundsChanged_beforeStart_flagDisabled_listenerNotNotified() {
+ // Start out with an existing configuration with bounds
+ configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100)
+ configurationController.onConfigurationChanged(configuration)
+ val provider =
+ StatusBarContentInsetsProviderImpl(
+ contextMock,
+ configurationController,
+ mock<DumpManager>(),
+ mock<CommandRegistry>(),
+ mock<SysUICutoutProvider>(),
+ )
+ val listener =
+ object : StatusBarContentInsetsChangedListener {
+ var triggered = false
+
+ override fun onStatusBarContentInsetsChanged() {
+ triggered = true
+ }
+ }
+ provider.addCallback(listener)
+
+ // WHEN the config is updated with new bounds
+ // but provider is not started
+ configuration.windowConfiguration.setMaxBounds(0, 0, 456, 789)
+ configurationController.onConfigurationChanged(configuration)
+
+ // THEN the listener is notified
+ assertThat(listener.triggered).isTrue()
+ }
+
+ @Test
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun onDensityOrFontScaleChanged_beforeStart_flagEnabled_listenerNotNotified() {
configuration.densityDpi = 12
val provider =
StatusBarContentInsetsProviderImpl(
@@ -1112,6 +1150,36 @@
}
@Test
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun onDensityOrFontScaleChanged_beforeStart_flagDisabled_listenerNotified() {
+ configuration.densityDpi = 12
+ val provider =
+ StatusBarContentInsetsProviderImpl(
+ contextMock,
+ configurationController,
+ mock<DumpManager>(),
+ mock<CommandRegistry>(),
+ mock<SysUICutoutProvider>(),
+ )
+ val listener =
+ object : StatusBarContentInsetsChangedListener {
+ var triggered = false
+
+ override fun onStatusBarContentInsetsChanged() {
+ triggered = true
+ }
+ }
+ provider.addCallback(listener)
+
+ // WHEN the config is updated, but the provider is not started
+ configuration.densityDpi = 20
+ configurationController.onConfigurationChanged(configuration)
+
+ // THEN the listener is notified
+ assertThat(listener.triggered).isTrue()
+ }
+
+ @Test
fun onDensityOrFontScaleChanged_afterStart_listenerNotified() {
configuration.densityDpi = 12
val provider =
@@ -1169,7 +1237,8 @@
}
@Test
- fun onThemeChanged_beforeStart_listenerNotNotified() {
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun onThemeChanged_beforeStart_flagEnabled_listenerNotNotified() {
val provider =
StatusBarContentInsetsProviderImpl(
contextMock,
@@ -1193,6 +1262,32 @@
assertThat(listener.triggered).isFalse()
}
+ @Test
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun onThemeChanged_beforeStart_flagDisabled_listenerNotified() {
+ val provider =
+ StatusBarContentInsetsProviderImpl(
+ contextMock,
+ configurationController,
+ mock<DumpManager>(),
+ mock<CommandRegistry>(),
+ mock<SysUICutoutProvider>(),
+ )
+ val listener =
+ object : StatusBarContentInsetsChangedListener {
+ var triggered = false
+
+ override fun onStatusBarContentInsetsChanged() {
+ triggered = true
+ }
+ }
+ provider.addCallback(listener)
+
+ configurationController.notifyThemeChanged()
+
+ assertThat(listener.triggered).isTrue()
+ }
+
private fun assertRects(
expected: Rect,
actual: Rect,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewBinder.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewBinder.kt
index cdc7aa2..9888574 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewBinder.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewBinder.kt
@@ -32,6 +32,8 @@
override fun bind(
view: View,
viewModel: HomeStatusBarViewModel,
+ systemEventChipAnimateIn: ((View) -> Unit)?,
+ systemEventChipAnimateOut: ((View) -> Unit)?,
listener: StatusBarVisibilityChangeListener,
) {
this.listener = listener
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
index 02c1540..eef5753 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
@@ -19,6 +19,7 @@
import android.view.View
import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -53,11 +54,14 @@
)
)
- override val isSystemInfoVisible =
+ override val systemInfoCombinedVis =
MutableStateFlow(
- HomeStatusBarViewModel.VisibilityModel(
- visibility = View.GONE,
- shouldAnimateChange = false,
+ HomeStatusBarViewModel.SystemInfoCombinedVisibilityModel(
+ HomeStatusBarViewModel.VisibilityModel(
+ visibility = View.GONE,
+ shouldAnimateChange = false,
+ ),
+ Idle,
)
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
index b3a73d8..c4d2569 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
@@ -60,11 +60,16 @@
import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.events.data.repository.systemStatusEventAnimationRepository
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
@@ -90,6 +95,7 @@
private val activeNotificationListRepository = kosmos.activeNotificationListRepository
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
private val disableFlagsRepository = kosmos.fakeDisableFlagsRepository
+ private val systemStatusEventAnimationRepository = kosmos.systemStatusEventAnimationRepository
private lateinit var underTest: HomeStatusBarViewModel
@@ -546,25 +552,50 @@
@Test
fun isSystemInfoVisible_allowedByDisableFlags_visible() =
testScope.runTest {
- val latest by collectLastValue(underTest.isSystemInfoVisible)
+ val latest by collectLastValue(underTest.systemInfoCombinedVis)
transitionKeyguardToGone()
disableFlagsRepository.disableFlags.value =
DisableFlagsModel(DISABLE_NONE, DISABLE2_NONE)
- assertThat(latest!!.visibility).isEqualTo(View.VISIBLE)
+ assertThat(latest!!.baseVisibility.visibility).isEqualTo(View.VISIBLE)
}
@Test
fun isSystemInfoVisible_notAllowedByDisableFlags_gone() =
testScope.runTest {
- val latest by collectLastValue(underTest.isSystemInfoVisible)
+ val latest by collectLastValue(underTest.systemInfoCombinedVis)
transitionKeyguardToGone()
disableFlagsRepository.disableFlags.value =
DisableFlagsModel(DISABLE_SYSTEM_INFO, DISABLE2_NONE)
- assertThat(latest!!.visibility).isEqualTo(View.GONE)
+ assertThat(latest!!.baseVisibility.visibility).isEqualTo(View.GONE)
+ }
+
+ @Test
+ fun systemInfoCombineVis_animationsPassThrough() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.systemInfoCombinedVis)
+ transitionKeyguardToGone()
+
+ assertThat(latest!!.baseVisibility)
+ .isEqualTo(VisibilityModel(visibility = View.VISIBLE, shouldAnimateChange = false))
+ assertThat(latest!!.animationState).isEqualTo(Idle)
+
+ // WHEN the animation state changes, but the visibility state doesn't change
+ systemStatusEventAnimationRepository.animationState.value = AnimatingIn
+
+ // THEN the visibility is the same
+ assertThat(latest!!.baseVisibility)
+ .isEqualTo(VisibilityModel(visibility = View.VISIBLE, shouldAnimateChange = false))
+ // THEN the animation state updates
+ assertThat(latest!!.animationState).isEqualTo(AnimatingIn)
+
+ systemStatusEventAnimationRepository.animationState.value = AnimatingOut
+ assertThat(latest!!.baseVisibility)
+ .isEqualTo(VisibilityModel(visibility = View.VISIBLE, shouldAnimateChange = false))
+ assertThat(latest!!.animationState).isEqualTo(AnimatingOut)
}
@Test
@@ -573,7 +604,7 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
keyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.GONE,
@@ -583,7 +614,7 @@
assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
}
@Test
@@ -592,13 +623,13 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
}
@Test
@@ -607,7 +638,7 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
keyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
@@ -617,7 +648,7 @@
assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
}
@Test
@@ -626,13 +657,13 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
kosmos.sceneContainerRepository.snapToScene(Scenes.Bouncer)
assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
}
@Test
@@ -641,7 +672,7 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
keyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
@@ -651,7 +682,7 @@
assertThat(clockVisible!!.visibility).isEqualTo(View.VISIBLE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.VISIBLE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.VISIBLE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.VISIBLE)
}
@Test
@@ -660,14 +691,14 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, taskInfo = null)
assertThat(clockVisible!!.visibility).isEqualTo(View.VISIBLE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.VISIBLE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.VISIBLE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.VISIBLE)
}
@Test
@@ -676,13 +707,13 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
transitionKeyguardToGone()
assertThat(clockVisible!!.visibility).isEqualTo(View.VISIBLE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.VISIBLE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.VISIBLE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.VISIBLE)
}
@Test
@@ -691,14 +722,14 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
transitionKeyguardToGone()
kosmos.shadeTestUtil.setShadeExpansion(0f)
assertThat(clockVisible!!.visibility).isEqualTo(View.VISIBLE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.VISIBLE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.VISIBLE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.VISIBLE)
}
@Test
@@ -707,13 +738,13 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
kosmos.sceneContainerRepository.snapToScene(Scenes.Gone)
assertThat(clockVisible!!.visibility).isEqualTo(View.VISIBLE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.VISIBLE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.VISIBLE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.VISIBLE)
}
@Test
@@ -722,14 +753,14 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
transitionKeyguardToGone()
kosmos.shadeTestUtil.setShadeExpansion(1f)
assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
}
@Test
@@ -738,14 +769,14 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
transitionKeyguardToGone()
kosmos.sceneContainerRepository.snapToScene(Scenes.Shade)
assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
}
@Test
@@ -754,7 +785,7 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
// Secure camera is an occluding activity
keyguardTransitionRepository.sendTransitionSteps(
@@ -766,7 +797,7 @@
assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
}
@Test
@@ -775,7 +806,7 @@
testScope.runTest {
val clockVisible by collectLastValue(underTest.isClockVisible)
val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
- val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+ val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
// Secure camera is an occluding activity
@@ -784,7 +815,7 @@
assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
- assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+ assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
}
private fun activeNotificationsStore(notifications: List<ActiveNotificationModel>) =
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index e90f055f..f187ce6 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -13,50 +13,58 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
- android:id="@+id/volume_dialog_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="right"
- android:divider="@drawable/volume_dialog_floating_sliders_spacer"
- android:orientation="horizontal"
- android:showDividers="middle|end|beginning"
- android:theme="@style/volume_dialog_theme">
+ android:id="@+id/volume_dialog_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
<LinearLayout
- android:id="@+id/volume_dialog_floating_sliders_container"
+ android:id="@+id/volume_dialog_container"
android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:divider="@drawable/volume_dialog_floating_sliders_spacer"
- android:gravity="bottom"
- android:orientation="horizontal"
- android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
- android:showDividers="middle" />
-
- <LinearLayout
- android:id="@+id/volume_dialog"
- android:layout_width="@dimen/volume_dialog_width"
android:layout_height="wrap_content"
- android:background="@drawable/volume_dialog_background"
- android:divider="@drawable/volume_dialog_spacer"
- android:paddingVertical="@dimen/volume_dialog_vertical_padding"
- android:gravity="center_horizontal"
- android:orientation="vertical"
- android:showDividers="middle">
+ android:layout_gravity="center_vertical|end"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:orientation="horizontal"
+ android:showDividers="middle|end|beginning">
- <include layout="@layout/volume_ringer_drawer" />
+ <LinearLayout
+ android:id="@+id/volume_dialog_floating_sliders_container"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:gravity="bottom"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
+ android:showDividers="middle" />
- <include layout="@layout/volume_dialog_slider" />
+ <LinearLayout
+ android:id="@+id/volume_dialog"
+ android:layout_width="@dimen/volume_dialog_width"
+ android:layout_height="wrap_content"
+ android:background="@drawable/volume_dialog_background"
+ android:clipChildren="false"
+ android:clipToOutline="false"
+ android:clipToPadding="false"
+ android:divider="@drawable/volume_dialog_spacer"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:paddingVertical="@dimen/volume_dialog_vertical_padding"
+ android:showDividers="middle">
- <ImageButton
- android:id="@+id/volume_dialog_settings"
- android:layout_width="@dimen/volume_dialog_button_size"
- android:layout_height="@dimen/volume_dialog_button_size"
- android:background="@drawable/ripple_drawable_20dp"
- android:contentDescription="@string/accessibility_volume_settings"
- android:soundEffectsEnabled="false"
- android:src="@drawable/horizontal_ellipsis"
- android:tint="?androidprv:attr/materialColorPrimary" />
+ <include layout="@layout/volume_ringer_drawer" />
+
+ <include layout="@layout/volume_dialog_slider" />
+
+ <ImageButton
+ android:id="@+id/volume_dialog_settings"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size"
+ android:background="@drawable/ripple_drawable_20dp"
+ android:contentDescription="@string/accessibility_volume_settings"
+ android:soundEffectsEnabled="false"
+ android:src="@drawable/horizontal_ellipsis"
+ android:tint="?androidprv:attr/materialColorPrimary" />
+ </LinearLayout>
</LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 7d071cd..c69b98c 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -557,6 +557,16 @@
<item name="android:showWhenLocked">true</item>
</style>
+ <style name="Theme.SystemUI.Dialog.Volume">
+ <item name="android:backgroundDimEnabled">false</item>
+ <item name="android:showWhenLocked">true</item>
+ <item name="android:windowBackground">@color/transparent</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowIsFloating">false</item>
+ <item name="android:windowNoTitle">true</item>
+ </style>
+
<style name="SystemUI.Material3.Slider.Volume">
<item name="trackHeight">40dp</item>
<item name="thumbHeight">52dp</item>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
index 1342dd0..95830b5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
@@ -15,6 +15,8 @@
*/
package com.android.keyguard;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+
import android.annotation.NonNull;
import android.app.Presentation;
import android.content.Context;
@@ -36,18 +38,24 @@
import androidx.annotation.Nullable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.UiBackground;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.views.NavigationBarView;
import com.android.systemui.settings.DisplayTracker;
+import com.android.systemui.shade.data.repository.ShadePositionRepository;
+import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import dagger.Lazy;
+import kotlinx.coroutines.CoroutineScope;
+
import java.util.concurrent.Executor;
import javax.inject.Inject;
+import javax.inject.Provider;
@SysUISingleton
public class KeyguardDisplayManager {
@@ -58,6 +66,7 @@
private final DisplayManager mDisplayService;
private final DisplayTracker mDisplayTracker;
private final Lazy<NavigationBarController> mNavigationBarControllerLazy;
+ private final Provider<ShadePositionRepository> mShadePositionRepositoryProvider;
private final ConnectedDisplayKeyguardPresentation.Factory
mConnectedDisplayKeyguardPresentationFactory;
private final Context mContext;
@@ -102,9 +111,12 @@
DeviceStateHelper deviceStateHelper,
KeyguardStateController keyguardStateController,
ConnectedDisplayKeyguardPresentation.Factory
- connectedDisplayKeyguardPresentationFactory) {
+ connectedDisplayKeyguardPresentationFactory,
+ Provider<ShadePositionRepository> shadePositionRepositoryProvider,
+ @Application CoroutineScope appScope) {
mContext = context;
mNavigationBarControllerLazy = navigationBarControllerLazy;
+ mShadePositionRepositoryProvider = shadePositionRepositoryProvider;
uiBgExecutor.execute(() -> mMediaRouter = mContext.getSystemService(MediaRouter.class));
mDisplayService = mContext.getSystemService(DisplayManager.class);
mDisplayTracker = displayTracker;
@@ -112,6 +124,17 @@
mDeviceStateHelper = deviceStateHelper;
mKeyguardStateController = keyguardStateController;
mConnectedDisplayKeyguardPresentationFactory = connectedDisplayKeyguardPresentationFactory;
+ if (ShadeWindowGoesAround.isEnabled()) {
+ collectFlow(appScope, shadePositionRepositoryProvider.get().getDisplayId(),
+ (id) -> onShadeWindowMovedToDisplayId(id));
+ }
+ }
+
+ private void onShadeWindowMovedToDisplayId(int shadeDisplayId) {
+ if (mShowing) {
+ hidePresentation(shadeDisplayId);
+ updateDisplays(/* showing= */ true);
+ }
}
private boolean isKeyguardShowable(Display display) {
@@ -119,9 +142,20 @@
if (DEBUG) Log.i(TAG, "Cannot show Keyguard on null display");
return false;
}
- if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) {
- if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display");
- return false;
+ if (ShadeWindowGoesAround.isEnabled()) {
+ int shadeDisplayId = mShadePositionRepositoryProvider.get().getDisplayId().getValue();
+ if (display.getDisplayId() == shadeDisplayId) {
+ if (DEBUG) {
+ Log.i(TAG,
+ "Do not show KeyguardPresentation on the shade window display");
+ }
+ return false;
+ }
+ } else {
+ if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) {
+ if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display");
+ return false;
+ }
}
display.getDisplayInfo(mTmpDisplayInfo);
if ((mTmpDisplayInfo.flags & Display.FLAG_PRIVATE) != 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt
new file mode 100644
index 0000000..8c393e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.keyboard.shortcut.data.model
+
+import android.graphics.drawable.Icon
+import android.hardware.input.InputGestureData
+import android.view.KeyboardShortcutGroup
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesUtils
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
+
+/**
+ * Internal Keyboard Shortcut models to use with [ShortcutCategoriesUtils.fetchShortcutCategory]
+ * when converting API models to Shortcut Helper Model [ShortcutCategory]. These Internal Models
+ * bridge the Gap between [InputGestureData] from custom shortcuts API and [KeyboardShortcutGroup]
+ * from default shortcuts API allowing us to have a single Utility Class that converts API models to
+ * Shortcut Helper models
+ *
+ * @param label Equivalent to shortcut helper's subcategory label
+ * @param items Keyboard Shortcuts received from API
+ * @param packageName package name of current app shortcut if available.
+ */
+data class InternalKeyboardShortcutGroup(
+ val label: String,
+ val items: List<InternalKeyboardShortcutInfo>,
+ val packageName: String? = null,
+)
+
+/**
+ * @param label Shortcut label
+ * @param keycode Key to trigger shortcut
+ * @param modifiers Mask of shortcut modifiers
+ * @param baseCharacter Key to trigger shortcut if is a character
+ * @param icon Shortcut icon if available - often used for app launch shortcuts
+ * @param isCustomShortcut If Shortcut is user customized or system defined.
+ */
+data class InternalKeyboardShortcutInfo(
+ val label: String,
+ val keycode: Int,
+ val modifiers: Int,
+ val baseCharacter: Char = Char.MIN_VALUE,
+ val icon: Icon? = null,
+ val isCustomShortcut: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
new file mode 100644
index 0000000..7f8fbb5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
@@ -0,0 +1,175 @@
+/*
+ * 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.keyboard.shortcut.data.repository
+
+import android.content.Context
+import android.content.Context.INPUT_SERVICE
+import android.hardware.input.InputGestureData
+import android.hardware.input.InputGestureData.KeyTrigger
+import android.hardware.input.InputManager
+import android.hardware.input.InputSettings
+import android.hardware.input.KeyGestureEvent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutGroup
+import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutInfo
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
+import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class CustomShortcutCategoriesRepository
+@Inject
+constructor(
+ stateRepository: ShortcutHelperStateRepository,
+ private val userTracker: UserTracker,
+ @Background private val backgroundScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val shortcutCategoriesUtils: ShortcutCategoriesUtils,
+) : ShortcutCategoriesRepository {
+
+ private val userContext: Context
+ get() = userTracker.createCurrentUserContext(userTracker.userContext)
+
+ // Input manager created with user context to provide correct user id when requesting custom
+ // shortcut
+ private val inputManager: InputManager
+ get() = userContext.getSystemService(INPUT_SERVICE) as InputManager
+
+ private val activeInputDevice =
+ stateRepository.state.map {
+ if (it is Active) {
+ withContext(backgroundDispatcher) { inputManager.getInputDevice(it.deviceId) }
+ } else {
+ null
+ }
+ }
+
+ override val categories: Flow<List<ShortcutCategory>> =
+ activeInputDevice
+ .map { inputDevice ->
+ if (inputDevice == null) {
+ emptyList()
+ } else {
+ val customInputGesturesForUser: List<InputGestureData> =
+ if (InputSettings.isCustomizableInputGesturesFeatureFlagEnabled()) {
+ inputManager.getCustomInputGestures(/* filter= */ null)
+ } else emptyList()
+ val sources = toInternalGroupSources(customInputGesturesForUser)
+ val supportedKeyCodes =
+ shortcutCategoriesUtils.fetchSupportedKeyCodes(
+ inputDevice.id,
+ sources.map { it.groups },
+ )
+
+ val result =
+ sources.mapNotNull { source ->
+ shortcutCategoriesUtils.fetchShortcutCategory(
+ type = source.type,
+ groups = source.groups,
+ inputDevice = inputDevice,
+ supportedKeyCodes = supportedKeyCodes,
+ )
+ }
+ result
+ }
+ }
+ .stateIn(
+ scope = backgroundScope,
+ initialValue = emptyList(),
+ started = SharingStarted.Lazily,
+ )
+
+ private fun toInternalGroupSources(
+ inputGestures: List<InputGestureData>
+ ): List<InternalGroupsSource> {
+ val ungroupedInternalGroupSources =
+ inputGestures.mapNotNull { gestureData ->
+ val keyTrigger = gestureData.trigger as KeyTrigger
+ val keyGestureType = gestureData.action.keyGestureType()
+ fetchGroupLabelByGestureType(keyGestureType)?.let { groupLabel ->
+ toInternalKeyboardShortcutInfo(keyGestureType, keyTrigger)?.let {
+ internalKeyboardShortcutInfo ->
+ val group =
+ InternalKeyboardShortcutGroup(
+ label = groupLabel,
+ items = listOf(internalKeyboardShortcutInfo),
+ )
+
+ fetchShortcutCategoryTypeByGestureType(keyGestureType)?.let {
+ InternalGroupsSource(groups = listOf(group), type = it)
+ }
+ }
+ }
+ }
+
+ return ungroupedInternalGroupSources
+ }
+
+ private fun toInternalKeyboardShortcutInfo(
+ keyGestureType: Int,
+ keyTrigger: KeyTrigger,
+ ): InternalKeyboardShortcutInfo? {
+ fetchShortcutInfoLabelByGestureType(keyGestureType)?.let {
+ return InternalKeyboardShortcutInfo(
+ label = it,
+ keycode = keyTrigger.keycode,
+ modifiers = keyTrigger.modifierState,
+ isCustomShortcut = true,
+ )
+ }
+ return null
+ }
+
+ private fun fetchGroupLabelByGestureType(
+ @KeyGestureEvent.KeyGestureType keyGestureType: Int
+ ): String? {
+ return InputGestures.gestureToInternalKeyboardShortcutGroupLabelMap.getOrDefault(
+ keyGestureType,
+ null,
+ )
+ }
+
+ private fun fetchShortcutInfoLabelByGestureType(
+ @KeyGestureEvent.KeyGestureType keyGestureType: Int
+ ): String? {
+ return InputGestures.gestureToInternalKeyboardShortcutInfoLabelMap.getOrDefault(
+ keyGestureType,
+ null,
+ )
+ }
+
+ private fun fetchShortcutCategoryTypeByGestureType(
+ @KeyGestureEvent.KeyGestureType keyGestureType: Int
+ ): ShortcutCategoryType? {
+ return InputGestures.gestureToShortcutCategoryTypeMap.getOrDefault(keyGestureType, null)
+ }
+
+ private data class InternalGroupsSource(
+ val groups: List<InternalKeyboardShortcutGroup>,
+ val type: ShortcutCategoryType,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt
new file mode 100644
index 0000000..5bb7cdd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.keyboard.shortcut.data.repository
+
+import android.hardware.input.InputManager
+import android.view.KeyboardShortcutGroup
+import android.view.KeyboardShortcutInfo
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutGroup
+import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutInfo
+import com.android.systemui.keyboard.shortcut.data.source.KeyboardShortcutGroupsSource
+import com.android.systemui.keyboard.shortcut.qualifiers.AppCategoriesShortcuts
+import com.android.systemui.keyboard.shortcut.qualifiers.CurrentAppShortcuts
+import com.android.systemui.keyboard.shortcut.qualifiers.InputShortcuts
+import com.android.systemui.keyboard.shortcut.qualifiers.MultitaskingShortcuts
+import com.android.systemui.keyboard.shortcut.qualifiers.SystemShortcuts
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.CurrentApp
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.InputMethodEditor
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class DefaultShortcutCategoriesRepository
+@Inject
+constructor(
+ @Background private val backgroundScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ @SystemShortcuts private val systemShortcutsSource: KeyboardShortcutGroupsSource,
+ @MultitaskingShortcuts private val multitaskingShortcutsSource: KeyboardShortcutGroupsSource,
+ @AppCategoriesShortcuts private val appCategoriesShortcutsSource: KeyboardShortcutGroupsSource,
+ @InputShortcuts private val inputShortcutsSource: KeyboardShortcutGroupsSource,
+ @CurrentAppShortcuts private val currentAppShortcutsSource: KeyboardShortcutGroupsSource,
+ private val inputManager: InputManager,
+ stateRepository: ShortcutHelperStateRepository,
+ shortcutCategoriesUtils: ShortcutCategoriesUtils,
+) : ShortcutCategoriesRepository {
+
+ private val sources =
+ listOf(
+ InternalGroupsSource(source = systemShortcutsSource, typeProvider = { System }),
+ InternalGroupsSource(
+ source = multitaskingShortcutsSource,
+ typeProvider = { MultiTasking },
+ ),
+ InternalGroupsSource(
+ source = appCategoriesShortcutsSource,
+ typeProvider = { AppCategories },
+ ),
+ InternalGroupsSource(
+ source = inputShortcutsSource,
+ typeProvider = { InputMethodEditor },
+ ),
+ InternalGroupsSource(
+ source = currentAppShortcutsSource,
+ typeProvider = { groups -> getCurrentAppShortcutCategoryType(groups) },
+ ),
+ )
+
+ private val activeInputDevice =
+ stateRepository.state.map {
+ if (it is Active) {
+ withContext(backgroundDispatcher) { inputManager.getInputDevice(it.deviceId) }
+ } else {
+ null
+ }
+ }
+
+ override val categories: Flow<List<ShortcutCategory>> =
+ activeInputDevice
+ .map { inputDevice ->
+ if (inputDevice == null) {
+ return@map emptyList()
+ }
+ val groupsFromAllSources =
+ sources.map {
+ toInternalKeyboardShortcutGroups(it.source.shortcutGroups(inputDevice.id))
+ }
+ val supportedKeyCodes =
+ shortcutCategoriesUtils.fetchSupportedKeyCodes(
+ inputDevice.id,
+ groupsFromAllSources,
+ )
+ return@map sources.mapIndexedNotNull { index, internalGroupsSource ->
+ shortcutCategoriesUtils.fetchShortcutCategory(
+ internalGroupsSource.typeProvider(groupsFromAllSources[index]),
+ groupsFromAllSources[index],
+ inputDevice,
+ supportedKeyCodes,
+ )
+ }
+ }
+ .stateIn(
+ scope = backgroundScope,
+ started = SharingStarted.Lazily,
+ initialValue = emptyList(),
+ )
+
+ private fun toInternalKeyboardShortcutGroups(
+ keyboardShortcutGroups: List<KeyboardShortcutGroup>
+ ): List<InternalKeyboardShortcutGroup> {
+ return keyboardShortcutGroups.map { group ->
+ InternalKeyboardShortcutGroup(
+ label = group.label.toString(),
+ items = group.items.map { toInternalKeyboardShortcutInfo(it) },
+ packageName = group.packageName?.toString(),
+ )
+ }
+ }
+
+ private fun toInternalKeyboardShortcutInfo(
+ keyboardShortcutInfo: KeyboardShortcutInfo
+ ): InternalKeyboardShortcutInfo {
+ return InternalKeyboardShortcutInfo(
+ label = keyboardShortcutInfo.label!!.toString(),
+ keycode = keyboardShortcutInfo.keycode,
+ modifiers = keyboardShortcutInfo.modifiers,
+ baseCharacter = keyboardShortcutInfo.baseCharacter,
+ icon = keyboardShortcutInfo.icon,
+ )
+ }
+
+ private fun getCurrentAppShortcutCategoryType(
+ shortcutGroups: List<InternalKeyboardShortcutGroup>
+ ): ShortcutCategoryType? {
+ val packageName = shortcutGroups.firstOrNull()?.packageName ?: return null
+ return CurrentApp(packageName)
+ }
+
+ private class InternalGroupsSource(
+ val source: KeyboardShortcutGroupsSource,
+ val typeProvider: (groups: List<InternalKeyboardShortcutGroup>) -> ShortcutCategoryType?,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt
new file mode 100644
index 0000000..28134db
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt
@@ -0,0 +1,156 @@
+/*
+ * 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.keyboard.shortcut.data.repository
+
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_BACK
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
+
+object InputGestures {
+ val gestureToShortcutCategoryTypeMap =
+ mapOf(
+ // System Category
+ KEY_GESTURE_TYPE_HOME to System,
+ KEY_GESTURE_TYPE_RECENT_APPS to System,
+ KEY_GESTURE_TYPE_BACK to System,
+ KEY_GESTURE_TYPE_TAKE_SCREENSHOT to System,
+ KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER to System,
+ KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL to System,
+ KEY_GESTURE_TYPE_LOCK_SCREEN to System,
+ KEY_GESTURE_TYPE_OPEN_NOTES to System,
+ KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to System,
+ KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to System,
+ KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to System,
+ KEY_GESTURE_TYPE_ALL_APPS to System,
+
+ // Multitasking Category
+ KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to MultiTasking,
+ KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to MultiTasking,
+ KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT to MultiTasking,
+ KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to MultiTasking,
+ KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT to MultiTasking,
+ KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to MultiTasking,
+
+ // App Category
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR to AppCategories,
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR to AppCategories,
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER to AppCategories,
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS to AppCategories,
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL to AppCategories,
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS to AppCategories,
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to AppCategories,
+ )
+
+ // TODO move all string to to resources use the same resources as the original shortcuts
+ // - that way when the strings are translated there are no discrepancies
+ val gestureToInternalKeyboardShortcutGroupLabelMap =
+ mapOf(
+ // System Category
+ KEY_GESTURE_TYPE_HOME to "System controls",
+ KEY_GESTURE_TYPE_RECENT_APPS to "System controls",
+ KEY_GESTURE_TYPE_BACK to "System controls",
+ KEY_GESTURE_TYPE_TAKE_SCREENSHOT to "System controls",
+ KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER to "System controls",
+ KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL to "System controls",
+ KEY_GESTURE_TYPE_LOCK_SCREEN to "System controls",
+ KEY_GESTURE_TYPE_ALL_APPS to "System controls",
+ KEY_GESTURE_TYPE_OPEN_NOTES to "System apps",
+ KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to "System apps",
+ KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to "System apps",
+ KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to "System apps",
+
+ // Multitasking Category
+ KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to "Recent apps",
+ KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to "Split screen",
+ KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT to "Split screen",
+ KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to "Split screen",
+ KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT to "Split screen",
+ KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to "Split screen",
+
+ // App Category
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR to "Applications",
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR to "Applications",
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER to "Applications",
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS to "Applications",
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL to "Applications",
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS to "Applications",
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to "Applications",
+ )
+
+ val gestureToInternalKeyboardShortcutInfoLabelMap =
+ mapOf(
+ // System Category
+ KEY_GESTURE_TYPE_HOME to "Go to home screen",
+ KEY_GESTURE_TYPE_RECENT_APPS to "View recent apps",
+ KEY_GESTURE_TYPE_BACK to "Go back",
+ KEY_GESTURE_TYPE_TAKE_SCREENSHOT to "Take screenshot",
+ KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER to "Show shortcuts",
+ KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL to "View notifications",
+ KEY_GESTURE_TYPE_LOCK_SCREEN to "Lock screen",
+ KEY_GESTURE_TYPE_ALL_APPS to "Open apps list",
+ KEY_GESTURE_TYPE_OPEN_NOTES to "Take a note",
+ KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to "Open settings",
+ KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to "Open assistant",
+ KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to "Open assistant",
+
+ // Multitasking Category
+ KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to "Cycle forward through recent apps",
+ KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to
+ "Use split screen with current app on the left",
+ KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT to
+ "Use split screen with current app on the right",
+ KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to "Switch from split screen to full screen",
+ KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT to
+ "Switch to app on left or above while using split screen",
+ KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to
+ "Switch to app on right or below while using split screen",
+
+ // App Category
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR to "Calculator",
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR to "Calendar",
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER to "Chrome",
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS to "Contacts",
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL to "Gmail",
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS to "Maps",
+ KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to "Messages",
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesRepository.kt
new file mode 100644
index 0000000..2e8cc00
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesRepository.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.keyboard.shortcut.data.repository
+
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
+import kotlinx.coroutines.flow.Flow
+
+interface ShortcutCategoriesRepository {
+ val categories: Flow<List<ShortcutCategory>>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
similarity index 60%
rename from packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
rename to packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
index 12dd581..899fd15 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
@@ -24,123 +24,34 @@
import android.view.InputDevice
import android.view.KeyCharacterMap
import android.view.KeyEvent
-import android.view.KeyboardShortcutGroup
-import android.view.KeyboardShortcutInfo
import com.android.systemui.Flags.shortcutHelperKeyGlyph
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.keyboard.shortcut.data.source.KeyboardShortcutGroupsSource
-import com.android.systemui.keyboard.shortcut.qualifiers.AppCategoriesShortcuts
-import com.android.systemui.keyboard.shortcut.qualifiers.CurrentAppShortcuts
-import com.android.systemui.keyboard.shortcut.qualifiers.InputShortcuts
-import com.android.systemui.keyboard.shortcut.qualifiers.MultitaskingShortcuts
-import com.android.systemui.keyboard.shortcut.qualifiers.SystemShortcuts
+import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutGroup
+import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutInfo
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.CurrentApp
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.InputMethodEditor
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
+import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.withContext
-@SysUISingleton
-class ShortcutHelperCategoriesRepository
+class ShortcutCategoriesUtils
@Inject
constructor(
private val context: Context,
- @Background private val backgroundScope: CoroutineScope,
- @Background private val backgroundDispatcher: CoroutineDispatcher,
- @SystemShortcuts private val systemShortcutsSource: KeyboardShortcutGroupsSource,
- @MultitaskingShortcuts private val multitaskingShortcutsSource: KeyboardShortcutGroupsSource,
- @AppCategoriesShortcuts private val appCategoriesShortcutsSource: KeyboardShortcutGroupsSource,
- @InputShortcuts private val inputShortcutsSource: KeyboardShortcutGroupsSource,
- @CurrentAppShortcuts private val currentAppShortcutsSource: KeyboardShortcutGroupsSource,
+ @Background private val backgroundCoroutineContext: CoroutineContext,
private val inputManager: InputManager,
- stateRepository: ShortcutHelperStateRepository,
) {
-
- private val sources =
- listOf(
- InternalGroupsSource(
- source = systemShortcutsSource,
- isTrusted = true,
- typeProvider = { System },
- ),
- InternalGroupsSource(
- source = multitaskingShortcutsSource,
- isTrusted = true,
- typeProvider = { MultiTasking },
- ),
- InternalGroupsSource(
- source = appCategoriesShortcutsSource,
- isTrusted = true,
- typeProvider = { AppCategories },
- ),
- InternalGroupsSource(
- source = inputShortcutsSource,
- isTrusted = false,
- typeProvider = { InputMethodEditor },
- ),
- InternalGroupsSource(
- source = currentAppShortcutsSource,
- isTrusted = false,
- typeProvider = { groups -> getCurrentAppShortcutCategoryType(groups) },
- ),
- )
-
- private val activeInputDevice =
- stateRepository.state.map {
- if (it is Active) {
- withContext(backgroundDispatcher) { inputManager.getInputDevice(it.deviceId) }
- } else {
- null
- }
- }
-
- val categories: Flow<List<ShortcutCategory>> =
- activeInputDevice
- .map { inputDevice ->
- if (inputDevice == null) {
- return@map emptyList()
- }
- val groupsFromAllSources = sources.map { it.source.shortcutGroups(inputDevice.id) }
- val supportedKeyCodes = fetchSupportedKeyCodes(inputDevice.id, groupsFromAllSources)
- return@map sources.mapIndexedNotNull { index, internalGroupsSource ->
- fetchShortcutCategory(
- internalGroupsSource,
- groupsFromAllSources[index],
- inputDevice,
- supportedKeyCodes,
- )
- }
- }
- .stateIn(
- scope = backgroundScope,
- started = SharingStarted.Lazily,
- initialValue = emptyList(),
- )
-
- private fun fetchShortcutCategory(
- internalGroupsSource: InternalGroupsSource,
- groups: List<KeyboardShortcutGroup>,
+ fun fetchShortcutCategory(
+ type: ShortcutCategoryType?,
+ groups: List<InternalKeyboardShortcutGroup>,
inputDevice: InputDevice,
supportedKeyCodes: Set<Int>,
): ShortcutCategory? {
- val type = internalGroupsSource.typeProvider(groups)
return if (type == null) {
null
} else {
@@ -151,27 +62,17 @@
inputDevice.keyCharacterMap,
type,
groups,
- internalGroupsSource.isTrusted,
+ type.isTrusted,
supportedKeyCodes,
)
}
}
- private fun getCurrentAppShortcutCategoryType(
- shortcutGroups: List<KeyboardShortcutGroup>
- ): ShortcutCategoryType? {
- return if (shortcutGroups.isEmpty()) {
- null
- } else {
- CurrentApp(packageName = shortcutGroups[0].packageName.toString())
- }
- }
-
private fun toShortcutCategory(
keyGlyphMap: KeyGlyphMap?,
keyCharacterMap: KeyCharacterMap,
type: ShortcutCategoryType,
- shortcutGroups: List<KeyboardShortcutGroup>,
+ shortcutGroups: List<InternalKeyboardShortcutGroup>,
keepIcons: Boolean,
supportedKeyCodes: Set<Int>,
): ShortcutCategory? {
@@ -179,7 +80,7 @@
shortcutGroups
.map { shortcutGroup ->
ShortcutSubCategory(
- shortcutGroup.label.toString(),
+ shortcutGroup.label,
toShortcuts(
keyGlyphMap,
keyCharacterMap,
@@ -201,7 +102,7 @@
private fun toShortcuts(
keyGlyphMap: KeyGlyphMap?,
keyCharacterMap: KeyCharacterMap,
- infoList: List<KeyboardShortcutInfo>,
+ infoList: List<InternalKeyboardShortcutInfo>,
keepIcons: Boolean,
supportedKeyCodes: Set<Int>,
) =
@@ -216,13 +117,13 @@
private fun toShortcut(
keyGlyphMap: KeyGlyphMap?,
keyCharacterMap: KeyCharacterMap,
- shortcutInfo: KeyboardShortcutInfo,
+ shortcutInfo: InternalKeyboardShortcutInfo,
keepIcon: Boolean,
): Shortcut? {
val shortcutCommand =
toShortcutCommand(keyGlyphMap, keyCharacterMap, shortcutInfo) ?: return null
return Shortcut(
- label = shortcutInfo.label!!.toString(),
+ label = shortcutInfo.label,
icon = toShortcutIcon(keepIcon, shortcutInfo),
commands = listOf(shortcutCommand),
)
@@ -230,7 +131,7 @@
private fun toShortcutIcon(
keepIcon: Boolean,
- shortcutInfo: KeyboardShortcutInfo,
+ shortcutInfo: InternalKeyboardShortcutInfo,
): ShortcutIcon? {
if (!keepIcon) {
return null
@@ -247,7 +148,7 @@
private fun toShortcutCommand(
keyGlyphMap: KeyGlyphMap?,
keyCharacterMap: KeyCharacterMap,
- info: KeyboardShortcutInfo,
+ info: InternalKeyboardShortcutInfo,
): ShortcutCommand? {
val keys = mutableListOf<ShortcutKey>()
var remainingModifiers = info.modifiers
@@ -272,7 +173,7 @@
Log.wtf(TAG, "No keys for $info")
return null
}
- return ShortcutCommand(keys)
+ return ShortcutCommand(keys = keys, isCustom = info.isCustomShortcut)
}
private fun toShortcutModifierKey(keyGlyphMap: KeyGlyphMap?, modifierMask: Int): ShortcutKey? {
@@ -325,11 +226,11 @@
return null
}
- private suspend fun fetchSupportedKeyCodes(
+ suspend fun fetchSupportedKeyCodes(
deviceId: Int,
- groupsFromAllSources: List<List<KeyboardShortcutGroup>>,
+ groupsFromAllSources: List<List<InternalKeyboardShortcutGroup>>,
): Set<Int> =
- withContext(backgroundDispatcher) {
+ withContext(backgroundCoroutineContext) {
val allUsedKeyCodes =
groupsFromAllSources
.flatMap { groups -> groups.flatMap { group -> group.items } }
@@ -342,14 +243,8 @@
.toSet()
}
- private class InternalGroupsSource(
- val source: KeyboardShortcutGroupsSource,
- val isTrusted: Boolean,
- val typeProvider: (groups: List<KeyboardShortcutGroup>) -> ShortcutCategoryType?,
- )
-
companion object {
- private const val TAG = "SHCategoriesRepo"
+ private const val TAG = "ShortcutCategoriesUtils"
private val SUPPORTED_MODIFIERS =
listOf(
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
index 6f19561..39fc27d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
@@ -17,7 +17,7 @@
package com.android.systemui.keyboard.shortcut.domain.interactor
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCategoriesRepository
+import com.android.systemui.keyboard.shortcut.data.repository.DefaultShortcutCategoriesRepository
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
@@ -28,9 +28,7 @@
@SysUISingleton
class ShortcutHelperCategoriesInteractor
@Inject
-constructor(
- categoriesRepository: ShortcutHelperCategoriesRepository,
-) {
+constructor(categoriesRepository: DefaultShortcutCategoriesRepository) {
val shortcutCategories: Flow<List<ShortcutCategory>> =
categoriesRepository.categories.map { categories ->
@@ -42,12 +40,12 @@
shortcutCategory.subCategories.map {
ShortcutSubCategory(
label = it.label,
- shortcuts = groupShortcutsInSubcategory(it.shortcuts)
+ shortcuts = groupShortcutsInSubcategory(it.shortcuts),
)
}
return ShortcutCategory(
type = shortcutCategory.type,
- subCategories = subCategoriesWithGroupedShortcuts
+ subCategories = subCategoriesWithGroupedShortcuts,
)
}
@@ -59,7 +57,7 @@
Shortcut(
label = commonLabel,
icon = groupedShortcuts.firstOrNull()?.icon,
- commands = groupedShortcuts.flatMap { it.commands }
+ commands = groupedShortcuts.flatMap { it.commands },
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
index c89ef15..813a1fca 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
@@ -17,24 +17,36 @@
package com.android.systemui.keyboard.shortcut.shared.model
sealed interface ShortcutCategoryType {
- data object System : ShortcutCategoryType
+ val isTrusted: Boolean
- data object MultiTasking : ShortcutCategoryType
+ data object System : ShortcutCategoryType {
+ override val isTrusted: Boolean = true
+ }
- data object InputMethodEditor : ShortcutCategoryType
+ data object MultiTasking : ShortcutCategoryType {
+ override val isTrusted: Boolean = true
+ }
- data object AppCategories : ShortcutCategoryType
+ data object InputMethodEditor : ShortcutCategoryType {
+ override val isTrusted: Boolean = false
+ }
- data class CurrentApp(val packageName: String) : ShortcutCategoryType
+ data object AppCategories : ShortcutCategoryType {
+ override val isTrusted: Boolean = true
+ }
+
+ data class CurrentApp(val packageName: String) : ShortcutCategoryType {
+ override val isTrusted: Boolean = false
+ }
}
data class ShortcutCategory(
val type: ShortcutCategoryType,
- val subCategories: List<ShortcutSubCategory>
+ val subCategories: List<ShortcutSubCategory>,
) {
constructor(
type: ShortcutCategoryType,
- vararg subCategories: ShortcutSubCategory
+ vararg subCategories: ShortcutSubCategory,
) : this(type, subCategories.asList())
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt
index 28451ae..c7e6b43 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt
@@ -18,10 +18,11 @@
import androidx.annotation.DrawableRes
-data class ShortcutCommand(val keys: List<ShortcutKey>)
+data class ShortcutCommand(val keys: List<ShortcutKey>, val isCustom: Boolean = false)
class ShortcutCommandBuilder {
private val keys = mutableListOf<ShortcutKey>()
+ private var isCustom = false
fun key(text: String) {
keys += ShortcutKey.Text(text)
@@ -31,7 +32,11 @@
keys += ShortcutKey.Icon.ResIdIcon(drawableResId)
}
- fun build() = ShortcutCommand(keys)
+ fun isCustom(isCustom: Boolean) {
+ this.isCustom = isCustom
+ }
+
+ fun build() = ShortcutCommand(keys, isCustom)
}
fun shortcutCommand(block: ShortcutCommandBuilder.() -> Unit) =
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
index 807c70b..10a201e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
@@ -27,6 +27,7 @@
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
@@ -36,6 +37,7 @@
import com.android.systemui.keyboard.shortcut.ui.composable.getWidth
import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import com.android.systemui.statusbar.phone.createBottomSheet
import javax.inject.Inject
@@ -86,6 +88,7 @@
},
)
dialog.setOnDismissListener { shortcutHelperViewModel.onViewClosed() }
+ dialog.setTitle(stringResource(R.string.shortcut_helper_title))
},
maxWidth = ShortcutHelperBottomSheet.LargeScreenWidthLandscape,
)
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/UserAwareConnection.kt b/packages/SystemUI/src/com/android/systemui/recordissue/UserAwareConnection.kt
index 6aaa27d..eca4051 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/UserAwareConnection.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/UserAwareConnection.kt
@@ -69,7 +69,11 @@
@WorkerThread
fun doUnBind() {
if (shouldUnBind) {
- userContextProvider.userContext.unbindService(this)
+ try {
+ userContextProvider.userContext.unbindService(this)
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "Can't disconnect because service wasn't connected anyways.", e)
+ }
shouldUnBind = false
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index 63510b8..e15830e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -157,7 +157,6 @@
@SysUISingleton
@Provides
- @ShadeDisplayAware
fun provideShadePositionRepository(impl: ShadePositionRepositoryImpl): ShadePositionRepository {
ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
return impl
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadePositionRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadePositionRepository.kt
new file mode 100644
index 0000000..37210b9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadePositionRepository.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.shade.data.repository
+
+import android.view.Display
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class FakeShadePositionRepository : ShadePositionRepository {
+ private val _displayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
+
+ override fun setDisplayId(displayId: Int) {
+ _displayId.value = displayId
+ }
+
+ override val displayId: StateFlow<Int>
+ get() = _displayId
+
+ override fun resetDisplayId() {
+ _displayId.value = Display.DEFAULT_DISPLAY
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt
index 6f492cf..c23ff53 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt
@@ -32,7 +32,7 @@
/** Is the refactor enabled */
@JvmStatic
- inline val isEnabled
+ inline val isEnabled: Boolean
get() = Flags.shadeWindowGoesAround()
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
index 564d52a..1cb4c44 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
@@ -27,7 +27,10 @@
interface SystemStatusAnimationScheduler :
CallbackController<SystemStatusAnimationCallback>, Dumpable {
- /** StateFlow holding the current [SystemEventAnimationState] at any time. */
+ /**
+ * The current state of the animation. This can be used from compose functions to coordinate
+ * their animations with the chip
+ */
val animationState: StateFlow<SystemEventAnimationState>
fun onStatusEvent(event: StatusEvent)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/data/repository/SystemStatusEventAnimationRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/data/repository/SystemStatusEventAnimationRepository.kt
new file mode 100644
index 0000000..971f5d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/data/repository/SystemStatusEventAnimationRepository.kt
@@ -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.systemui.statusbar.events.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+/** Repository to expose the [SystemStatusAnimationScheduler] state via flows */
+interface SystemStatusEventAnimationRepository {
+ val animationState: StateFlow<SystemEventAnimationState>
+}
+
+@SysUISingleton
+class SystemStatusEventAnimationRepositoryImpl
+@Inject
+constructor(scheduler: SystemStatusAnimationScheduler) : SystemStatusEventAnimationRepository {
+ override val animationState = scheduler.animationState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/domain/interactor/SystemStatusEventAnimationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/domain/interactor/SystemStatusEventAnimationInteractor.kt
new file mode 100644
index 0000000..3e30642
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/domain/interactor/SystemStatusEventAnimationInteractor.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.events.domain.interactor
+
+import android.view.View
+import androidx.core.animation.Animator
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.events.data.repository.SystemStatusEventAnimationRepository
+import com.android.systemui.statusbar.phone.fragment.StatusBarSystemEventDefaultAnimator
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Interactor for dealing with system status event animations. This class can be used to monitor the
+ * current [animationState], and defines some common animation functions that an handle hiding
+ * system chrome in order to make space for the event chips
+ */
+@SysUISingleton
+class SystemStatusEventAnimationInteractor
+@Inject
+constructor(
+ repo: SystemStatusEventAnimationRepository,
+ configurationInteractor: ConfigurationInteractor,
+ @Application scope: CoroutineScope,
+) {
+ private val chipAnimateInTranslationX =
+ configurationInteractor
+ .dimensionPixelSize(R.dimen.ongoing_appops_chip_animation_in_status_bar_translation_x)
+ .stateIn(scope, SharingStarted.Eagerly, 0)
+
+ private val chipAnimateOutTranslationX =
+ configurationInteractor
+ .dimensionPixelSize(R.dimen.ongoing_appops_chip_animation_out_status_bar_translation_x)
+ .stateIn(scope, SharingStarted.Eagerly, 0)
+
+ val animationState = repo.animationState
+
+ private fun getDefaultStatusBarAnimationForChipEnter(
+ setX: (Float) -> Unit,
+ setAlpha: (Float) -> Unit,
+ ): Animator {
+ return StatusBarSystemEventDefaultAnimator.getDefaultStatusBarAnimationForChipEnter(
+ chipAnimateInTranslationX.value,
+ setX,
+ setAlpha,
+ )
+ }
+
+ private fun getDefaultStatusBarAnimationForChipExit(
+ setX: (Float) -> Unit,
+ setAlpha: (Float) -> Unit,
+ ): Animator {
+ return StatusBarSystemEventDefaultAnimator.getDefaultStatusBarAnimationForChipExit(
+ chipAnimateOutTranslationX.value,
+ setX,
+ setAlpha,
+ )
+ }
+
+ fun animateStatusBarContentForChipEnter(v: View) {
+ getDefaultStatusBarAnimationForChipEnter(setX = v::setTranslationX, setAlpha = v::setAlpha)
+ .start()
+ }
+
+ fun animateStatusBarContentForChipExit(v: View) {
+ v.translationX = chipAnimateOutTranslationX.value.toFloat()
+ getDefaultStatusBarAnimationForChipExit(setX = v::setTranslationX, setAlpha = v::setAlpha)
+ .start()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
index d991b1d..41db5f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
@@ -174,6 +174,14 @@
private val dumpableName = TAG + nameSuffix
private val commandName = StatusBarInsetsCommand.NAME + nameSuffix
+ init {
+ if (!StatusBarConnectedDisplays.isEnabled) {
+ // Call start(), since it is not called when the flag is disabled, to keep the old
+ // behavior as it was.
+ start()
+ }
+ }
+
override fun start() {
configurationController.addCallback(this)
dumpManager.registerDumpable(dumpableName, this)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index c55a63c..23b4b65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -376,7 +376,11 @@
mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
mHomeStatusBarViewBinder.bind(
- mStatusBar, mHomeStatusBarViewModel, mStatusBarVisibilityChangeListener);
+ mStatusBar,
+ mHomeStatusBarViewModel,
+ /* systemEventChipAnimateIn */ null,
+ /* systemEventChipAnimateOut */ null,
+ mStatusBarVisibilityChangeListener);
}
private String getDumpableName() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt
index 1f9ea08..fd7bce0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt
@@ -71,49 +71,87 @@
override fun onSystemEventAnimationBegin(): Animator {
isAnimationRunning = true
- val moveOut =
- ValueAnimator.ofFloat(0f, 1f).apply {
- duration = 23.frames
- interpolator = STATUS_BAR_X_MOVE_OUT
- addUpdateListener {
- onTranslationXChanged(-(translationXIn * animatedValue as Float))
- }
- }
- val alphaOut =
- ValueAnimator.ofFloat(1f, 0f).apply {
- duration = 8.frames
- interpolator = null
- addUpdateListener { onAlphaChanged(animatedValue as Float) }
- }
-
- val animSet = AnimatorSet()
- animSet.playTogether(moveOut, alphaOut)
- return animSet
+ return getDefaultStatusBarAnimationForChipEnter(
+ translationXIn,
+ onTranslationXChanged,
+ onAlphaChanged,
+ )
}
override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator {
onTranslationXChanged(translationXOut.toFloat())
- val moveIn =
- ValueAnimator.ofFloat(1f, 0f).apply {
- duration = 23.frames
- startDelay = 7.frames
- interpolator = STATUS_BAR_X_MOVE_IN
- addUpdateListener {
- onTranslationXChanged(translationXOut * animatedValue as Float)
- }
- }
- val alphaIn =
- ValueAnimator.ofFloat(0f, 1f).apply {
- duration = 5.frames
- startDelay = 11.frames
- interpolator = null
- addUpdateListener { onAlphaChanged(animatedValue as Float) }
- }
+ val anim =
+ getDefaultStatusBarAnimationForChipExit(
+ translationXOut,
+ onTranslationXChanged,
+ onAlphaChanged,
+ )
+ anim.doOnEnd { isAnimationRunning = false }
+ anim.doOnCancel { isAnimationRunning = false }
- val animatorSet = AnimatorSet()
- animatorSet.playTogether(moveIn, alphaIn)
- animatorSet.doOnEnd { isAnimationRunning = false }
- animatorSet.doOnCancel { isAnimationRunning = false }
- return animatorSet
+ return anim
+ }
+
+ /** Static definition of these animations so we can use them more easily from view binders */
+ companion object {
+ /**
+ * Chip: coming in. Animated view: going out.
+ *
+ * Implements the exact spec for animating any status bar elements OUT to make space for the
+ * chip IN animation.
+ */
+ fun getDefaultStatusBarAnimationForChipEnter(
+ targetTranslation: Int,
+ setX: (Float) -> Unit,
+ setAlpha: (Float) -> Unit,
+ ): Animator {
+ val moveOut =
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = 23.frames
+ interpolator = STATUS_BAR_X_MOVE_OUT
+ addUpdateListener { setX(-(targetTranslation * animatedValue as Float)) }
+ }
+ val alphaOut =
+ ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = 8.frames
+ interpolator = null
+ addUpdateListener { setAlpha(animatedValue as Float) }
+ }
+
+ val animSet = AnimatorSet()
+ animSet.playTogether(moveOut, alphaOut)
+ return animSet
+ }
+
+ /**
+ * Chip: going out. Animated view: coming in.
+ *
+ * Implements the exact spec for animating any status bar elements IN as the chip is
+ * animating OUT
+ */
+ fun getDefaultStatusBarAnimationForChipExit(
+ targetTranslation: Int,
+ setX: (Float) -> Unit,
+ setAlpha: (Float) -> Unit,
+ ): Animator {
+ val moveIn =
+ ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = 23.frames
+ startDelay = 7.frames
+ interpolator = STATUS_BAR_X_MOVE_IN
+ addUpdateListener { setX(targetTranslation * animatedValue as Float) }
+ }
+ val alphaIn =
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = 5.frames
+ startDelay = 11.frames
+ interpolator = null
+ addUpdateListener { setAlpha(animatedValue as Float) }
+ }
+
+ val animatorSet = AnimatorSet()
+ animatorSet.playTogether(moveIn, alphaIn)
+ return animatorSet
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 935b101..bfdc8bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -23,6 +23,8 @@
import com.android.systemui.log.LogBufferFactory
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.statusbar.events.data.repository.SystemStatusEventAnimationRepository
+import com.android.systemui.statusbar.events.data.repository.SystemStatusEventAnimationRepositoryImpl
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
@@ -85,6 +87,11 @@
abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
@Binds
+ abstract fun systemStatusEventAnimationRepository(
+ impl: SystemStatusEventAnimationRepositoryImpl
+ ): SystemStatusEventAnimationRepository
+
+ @Binds
abstract fun realDeviceBasedSatelliteRepository(
impl: DeviceBasedSatelliteRepositoryImpl
): RealDeviceBasedSatelliteRepository
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
index 2177e02..72df027 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
@@ -32,6 +32,10 @@
import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipBinder
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.RunningChipAnim
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel
@@ -46,10 +50,16 @@
/**
* Binds the view to the view-model. [listener] will be notified whenever an event that may
* change the status bar visibility occurs.
+ *
+ * Null chip animations are used when [StatusBarRootModernization] is off (i.e., when we are
+ * binding from the fragment). If non-null, they control the animation of the system icon area
+ * to support the chip animations.
*/
fun bind(
view: View,
viewModel: HomeStatusBarViewModel,
+ systemEventChipAnimateIn: ((View) -> Unit)?,
+ systemEventChipAnimateOut: ((View) -> Unit)?,
listener: StatusBarVisibilityChangeListener,
)
}
@@ -59,6 +69,8 @@
override fun bind(
view: View,
viewModel: HomeStatusBarViewModel,
+ systemEventChipAnimateIn: ((View) -> Unit)?,
+ systemEventChipAnimateOut: ((View) -> Unit)?,
listener: StatusBarVisibilityChangeListener,
) {
view.repeatWhenAttached {
@@ -95,6 +107,7 @@
when (primaryChipModel) {
is OngoingActivityChipModel.Shown ->
primaryChipView.show(shouldAnimateChange = true)
+
is OngoingActivityChipModel.Hidden ->
primaryChipView.hide(
state = View.GONE,
@@ -109,6 +122,7 @@
hasSecondaryOngoingActivity = false,
shouldAnimate = true,
)
+
is OngoingActivityChipModel.Hidden ->
listener.onOngoingActivityStatusChanged(
hasPrimaryOngoingActivity = false,
@@ -175,10 +189,30 @@
view.requireViewById<View>(R.id.status_bar_end_side_content)
// TODO(b/364360986): Also handle operator name view.
launch {
- viewModel.isSystemInfoVisible.collect {
- systemInfoView.adjustVisibility(it)
- // TODO(b/364360986): The system info view has a custom alpha controller
- // in CollapsedStatusBarFragment.
+ viewModel.systemInfoCombinedVis.collect { (baseVis, animState) ->
+ // Broadly speaking, the baseVis controls the view.visibility, and
+ // the animation state uses only alpha to achieve its effect. This
+ // means that we can always modify the visibility, and if we're
+ // animating we can use the animState to handle it. If we are not
+ // animating, then we can use the baseVis default animation
+ if (animState.isAnimatingChip()) {
+ // Just apply the visibility of the view, but don't animate
+ systemInfoView.visibility = baseVis.visibility
+ // Now apply the animation state, with its animator
+ when (animState) {
+ AnimatingIn -> {
+ systemEventChipAnimateIn?.invoke(systemInfoView)
+ }
+ AnimatingOut -> {
+ systemEventChipAnimateOut?.invoke(systemInfoView)
+ }
+ else -> {
+ // Nothing to do here
+ }
+ }
+ } else {
+ systemInfoView.adjustVisibility(baseVis)
+ }
}
}
}
@@ -186,6 +220,14 @@
}
}
+ private fun SystemEventAnimationState.isAnimatingChip() =
+ when (this) {
+ AnimatingIn,
+ AnimatingOut,
+ RunningChipAnim -> true
+ else -> false
+ }
+
private fun OngoingActivityChipModel.toVisibilityModel(): VisibilityModel {
return VisibilityModel(
visibility = if (this is OngoingActivityChipModel.Shown) View.VISIBLE else View.GONE,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
index a9c2f72..1faa9f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
@@ -35,6 +35,7 @@
import com.android.systemui.plugins.DarkIconDispatcher
import com.android.systemui.res.R
import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore
+import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder
import com.android.systemui.statusbar.phone.NotificationIconContainer
import com.android.systemui.statusbar.phone.PhoneStatusBarView
@@ -59,6 +60,7 @@
private val iconController: StatusBarIconController,
private val ongoingCallController: OngoingCallController,
private val darkIconDispatcherStore: DarkIconDispatcherStore,
+ private val eventAnimationInteractor: SystemStatusEventAnimationInteractor,
) {
fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView {
val composeView = ComposeView(root.context)
@@ -73,6 +75,7 @@
iconController = iconController,
ongoingCallController = ongoingCallController,
darkIconDispatcher = darkIconDispatcherStore.forDisplay(root.context.displayId),
+ eventAnimationInteractor = eventAnimationInteractor,
onViewCreated = andThen,
)
}
@@ -102,6 +105,7 @@
iconController: StatusBarIconController,
ongoingCallController: OngoingCallController,
darkIconDispatcher: DarkIconDispatcher,
+ eventAnimationInteractor: SystemStatusEventAnimationInteractor,
onViewCreated: (ViewGroup) -> Unit,
) {
// None of these methods are used when [StatusBarRootModernization] is on.
@@ -181,6 +185,8 @@
statusBarViewBinder.bind(
phoneStatusBarView,
statusBarViewModel,
+ eventAnimationInteractor::animateStatusBarContentForChipEnter,
+ eventAnimationInteractor::animateStatusBarContentForChipExit,
nopVisibilityChangeListener,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
index 4277a8b..6a9b43c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
@@ -35,6 +35,9 @@
import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel
+import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.phone.domain.interactor.LightsOutInteractor
@@ -95,7 +98,12 @@
val isClockVisible: Flow<VisibilityModel>
val isNotificationIconContainerVisible: Flow<VisibilityModel>
- val isSystemInfoVisible: Flow<VisibilityModel>
+ /**
+ * Pair of (system info visibility, event animation state). The animation state can be used to
+ * respond to the system event chip animations. In all cases, system info visibility correctly
+ * models the View.visibility for the system info area
+ */
+ val systemInfoCombinedVis: StateFlow<SystemInfoCombinedVisibilityModel>
/**
* Apps can request a low profile mode [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE] where
@@ -114,6 +122,12 @@
/** True if a visibility change should be animated. */
val shouldAnimateChange: Boolean,
)
+
+ /** The combined visibility + animation state for the system info status bar area */
+ data class SystemInfoCombinedVisibilityModel(
+ val baseVisibility: VisibilityModel,
+ val animationState: SystemEventAnimationState,
+ )
}
@SysUISingleton
@@ -129,6 +143,7 @@
sceneContainerOcclusionInteractor: SceneContainerOcclusionInteractor,
shadeInteractor: ShadeInteractor,
ongoingActivityChipsViewModel: OngoingActivityChipsViewModel,
+ animations: SystemStatusEventAnimationInteractor,
@Application coroutineScope: CoroutineScope,
) : HomeStatusBarViewModel {
override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> =
@@ -228,7 +243,7 @@
visibilityViaDisableFlags.animate,
)
}
- override val isSystemInfoVisible: Flow<VisibilityModel> =
+ private val isSystemInfoVisible =
combine(
shouldHomeStatusBarBeVisible,
collapsedStatusBarInteractor.visibilityViaDisableFlags,
@@ -238,6 +253,22 @@
VisibilityModel(showSystemInfo.toVisibilityInt(), visibilityViaDisableFlags.animate)
}
+ override val systemInfoCombinedVis =
+ combine(isSystemInfoVisible, animations.animationState) { sysInfoVisible, animationState ->
+ HomeStatusBarViewModel.SystemInfoCombinedVisibilityModel(
+ sysInfoVisible,
+ animationState,
+ )
+ }
+ .stateIn(
+ coroutineScope,
+ SharingStarted.WhileSubscribed(),
+ HomeStatusBarViewModel.SystemInfoCombinedVisibilityModel(
+ VisibilityModel(View.VISIBLE, false),
+ Idle,
+ ),
+ )
+
@View.Visibility
private fun Boolean.toVisibilityInt(): Int {
return if (this) View.VISIBLE else View.GONE
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java
index b79d39e..36d64a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java
@@ -39,7 +39,7 @@
import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController;
/**
- * Status bar view.
+ * Status bar view
* We now extend WindowRootView so that we can host Compose views
*/
public class StatusBarWindowView extends FrameLayout {
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 4fc9a7c..5c0cc81 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
@@ -18,9 +18,13 @@
import android.app.Dialog
import android.content.Context
+import android.graphics.PixelFormat
import android.os.Bundle
import android.view.MotionEvent
+import android.view.ViewGroup
+import android.view.WindowManager
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.VolumeDialogViewBinder
@@ -32,10 +36,34 @@
@Application context: Context,
private val viewBinder: VolumeDialogViewBinder,
private val visibilityInteractor: VolumeDialogVisibilityInteractor,
-) : Dialog(context) {
+) : Dialog(context, R.style.Theme_SystemUI_Dialog_Volume) {
+
+ init {
+ with(window!!) {
+ 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)
+
+ 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.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
+ }
+ setCanceledOnTouchOutside(true)
+ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ setContentView(R.layout.volume_dialog)
viewBinder.bind(this)
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
index 78eabb2..d9a945c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
@@ -17,21 +17,22 @@
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.graphics.Rect
+import android.graphics.Region
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
-import android.view.Window
-import android.view.WindowManager
+import android.view.ViewTreeObserver
+import android.view.ViewTreeObserver.InternalInsetsInfo
+import android.widget.FrameLayout
+import androidx.annotation.GravityInt
import com.android.internal.view.RotationPolicy
import com.android.systemui.lifecycle.WindowLifecycleState
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.lifecycle.viewModel
import com.android.systemui.res.R
+import com.android.systemui.util.children
import com.android.systemui.volume.SystemUIInterpolators
-import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder
import com.android.systemui.volume.dialog.settings.ui.binder.VolumeDialogSettingsButtonViewBinder
@@ -52,6 +53,8 @@
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
/** Binds the root view of the Volume Dialog. */
@OptIn(ExperimentalCoroutinesApi::class)
@@ -61,65 +64,41 @@
constructor(
private val volumeResources: VolumeDialogResources,
private val gravityViewModel: VolumeDialogGravityViewModel,
- private val viewModelFactory: VolumeDialogViewModel.Factory,
+ private val dialogViewModelFactory: VolumeDialogViewModel.Factory,
private val jankListenerFactory: JankListenerFactory,
private val tracer: VolumeTracer,
- @VolumeDialog private val coroutineScope: CoroutineScope,
private val volumeDialogRingerViewBinder: VolumeDialogRingerViewBinder,
private val slidersViewBinder: VolumeDialogSlidersViewBinder,
private val settingsButtonViewBinder: VolumeDialogSettingsButtonViewBinder,
) {
fun bind(dialog: Dialog) {
- setupDialog(dialog)
- val view: View = dialog.requireViewById(R.id.volume_dialog_container)
- view.alpha = 0f
- view.repeatWhenAttached {
- view.viewModel(
+ // Root view of the Volume Dialog.
+ val root: ViewGroup = dialog.requireViewById(R.id.volume_dialog_root)
+ // Volume Dialog container view that contains the dialog itself without the floating sliders
+ val container: View = root.requireViewById(R.id.volume_dialog_container)
+ container.alpha = 0f
+ container.repeatWhenAttached {
+ root.viewModel(
traceName = "VolumeDialogViewBinder",
minWindowLifecycleState = WindowLifecycleState.ATTACHED,
- factory = { viewModelFactory.create() },
+ factory = { dialogViewModelFactory.create() },
) { viewModel ->
- viewModel.dialogTitle.onEach { dialog.window?.setTitle(it) }.launchIn(this)
+ animateVisibility(container, dialog, viewModel.dialogVisibilityModel)
- animateVisibility(view, dialog, viewModel.dialogVisibilityModel)
+ viewModel.dialogTitle.onEach { dialog.window?.setTitle(it) }.launchIn(this)
+ gravityViewModel.dialogGravity
+ .onEach { container.setLayoutGravity(it) }
+ .launchIn(this)
+
+ launch { root.viewTreeObserver.computeInternalInsetsListener(root) }
awaitCancellation()
}
}
- volumeDialogRingerViewBinder.bind(view)
- slidersViewBinder.bind(view)
- settingsButtonViewBinder.bind(view)
- }
-
- /** Configures [Window] for the [Dialog]. */
- private fun setupDialog(dialog: Dialog) {
- with(dialog.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 { setGravity(it) }.launchIn(coroutineScope)
- }
- dialog.setContentView(R.layout.volume_dialog)
- dialog.setCanceledOnTouchOutside(true)
+ volumeDialogRingerViewBinder.bind(root)
+ slidersViewBinder.bind(root)
+ settingsButtonViewBinder.bind(root)
}
private fun CoroutineScope.animateVisibility(
@@ -209,4 +188,33 @@
}
animator.suspendAnimate(jankListenerFactory.dismiss(this, duration))
}
+
+ private suspend fun ViewTreeObserver.computeInternalInsetsListener(viewGroup: ViewGroup) =
+ suspendCancellableCoroutine<Unit> { continuation ->
+ val listener =
+ ViewTreeObserver.OnComputeInternalInsetsListener { inoutInfo ->
+ viewGroup.fillTouchableBounds(inoutInfo)
+ }
+ addOnComputeInternalInsetsListener(listener)
+ continuation.invokeOnCancellation { removeOnComputeInternalInsetsListener(listener) }
+ }
+
+ private fun ViewGroup.fillTouchableBounds(internalInsetsInfo: InternalInsetsInfo) {
+ for (child in children) {
+ val boundsRect = Rect()
+ internalInsetsInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION)
+
+ child.getBoundsInWindow(boundsRect, false)
+ internalInsetsInfo.touchableRegion.op(boundsRect, Region.Op.UNION)
+ }
+ val boundsRect = Rect()
+ getBoundsInWindow(boundsRect, false)
+ }
+
+ private fun View.setLayoutGravity(@GravityInt newGravity: Int) {
+ val frameLayoutParams =
+ layoutParams as? FrameLayout.LayoutParams
+ ?: error("View must be a child of a FrameLayout")
+ layoutParams = frameLayoutParams.apply { gravity = newGravity }
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index 8022e6e..f52f039 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -22,7 +22,9 @@
import android.hardware.input.fakeInputManager
import android.view.windowManager
import com.android.systemui.broadcast.broadcastDispatcher
-import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCategoriesRepository
+import com.android.systemui.keyboard.shortcut.data.repository.CustomShortcutCategoriesRepository
+import com.android.systemui.keyboard.shortcut.data.repository.DefaultShortcutCategoriesRepository
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesUtils
import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper
import com.android.systemui.keyboard.shortcut.data.source.AppCategoriesShortcutsSource
@@ -40,6 +42,7 @@
import com.android.systemui.keyguard.data.repository.fakeCommandQueue
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.backgroundCoroutineContext
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.model.sysUiState
@@ -73,10 +76,18 @@
var Kosmos.shortcutHelperCurrentAppShortcutsSource: KeyboardShortcutGroupsSource by
Kosmos.Fixture { CurrentAppShortcutsSource(windowManager) }
-val Kosmos.shortcutHelperCategoriesRepository by
+val Kosmos.shortcutCategoriesUtils by
Kosmos.Fixture {
- ShortcutHelperCategoriesRepository(
+ ShortcutCategoriesUtils(
applicationContext,
+ backgroundCoroutineContext,
+ fakeInputManager.inputManager,
+ )
+ }
+
+val Kosmos.defaultShortcutCategoriesRepository by
+ Kosmos.Fixture {
+ DefaultShortcutCategoriesRepository(
applicationCoroutineScope,
testDispatcher,
shortcutHelperSystemShortcutsSource,
@@ -86,6 +97,18 @@
shortcutHelperCurrentAppShortcutsSource,
fakeInputManager.inputManager,
shortcutHelperStateRepository,
+ shortcutCategoriesUtils,
+ )
+ }
+
+val Kosmos.customShortcutCategoriesRepository by
+ Kosmos.Fixture {
+ CustomShortcutCategoriesRepository(
+ shortcutHelperStateRepository,
+ userTracker,
+ applicationCoroutineScope,
+ testDispatcher,
+ shortcutCategoriesUtils,
)
}
@@ -112,7 +135,7 @@
}
val Kosmos.shortcutHelperCategoriesInteractor by
- Kosmos.Fixture { ShortcutHelperCategoriesInteractor(shortcutHelperCategoriesRepository) }
+ Kosmos.Fixture { ShortcutHelperCategoriesInteractor(defaultShortcutCategoriesRepository) }
val Kosmos.shortcutHelperViewModel by
Kosmos.Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/data/repository/SystemStatusEventAnimationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/data/repository/SystemStatusEventAnimationRepositoryKosmos.kt
new file mode 100644
index 0000000..92eeef9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/data/repository/SystemStatusEventAnimationRepositoryKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.events.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
+import kotlinx.coroutines.flow.MutableStateFlow
+
+val Kosmos.systemStatusEventAnimationRepository: FakeSystemStatusEventAnimationRepository by
+ Kosmos.Fixture { FakeSystemStatusEventAnimationRepository() }
+
+class FakeSystemStatusEventAnimationRepository : SystemStatusEventAnimationRepository {
+ override val animationState = MutableStateFlow(SystemEventAnimationState.Idle)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/domain/interactor/SystemStatusEventAnimationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/domain/interactor/SystemStatusEventAnimationInteractorKosmos.kt
new file mode 100644
index 0000000..7513fea
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/domain/interactor/SystemStatusEventAnimationInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.events.domain.interactor
+
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.events.data.repository.systemStatusEventAnimationRepository
+
+val Kosmos.systemStatusEventAnimationInteractor by
+ Kosmos.Fixture {
+ SystemStatusEventAnimationInteractor(
+ repo = systemStatusEventAnimationRepository,
+ configurationInteractor = configurationInteractor,
+ scope = applicationCoroutineScope,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
index 3a7ada2..03e4c89 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
@@ -24,6 +24,7 @@
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
+import com.android.systemui.statusbar.events.domain.interactor.systemStatusEventAnimationInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.phone.domain.interactor.lightsOutInteractor
import com.android.systemui.statusbar.pipeline.shared.domain.interactor.collapsedStatusBarInteractor
@@ -40,6 +41,7 @@
sceneContainerOcclusionInteractor,
shadeInteractor,
ongoingActivityChipsViewModel,
+ systemStatusEventAnimationInteractor,
applicationCoroutineScope,
)
}
diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh
index 3726ca9..b389a67 100755
--- a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh
+++ b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh
@@ -30,7 +30,7 @@
EOF
}
-source "${0%/*}"/../../common.sh
+source "${0%/*}"/../common.sh
SCRIPT_NAME="${0##*/}"
@@ -61,7 +61,6 @@
done
shift $(($OPTIND - 1))
-
# Build the dump files, which are the input of this test.
run m dump-jar tiny-framework-dump-test
diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
index e8fa417..afdc0c0 100644
--- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java
+++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
@@ -119,9 +119,16 @@
== BiometricManager.Authenticators.IDENTITY_CHECK;
boolean isMandatoryBiometricsAuthentication = false;
+ final int effectiveUserId;
+ if (Flags.effectiveUserBp()) {
+ effectiveUserId = userManager.getCredentialOwnerProfile(userId);
+ } else {
+ effectiveUserId = userId;
+ }
+
if (dropCredentialFallback(promptInfo.getAuthenticators(),
settingObserver.getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(
- userId), trustManager)) {
+ effectiveUserId), trustManager)) {
isMandatoryBiometricsAuthentication = true;
promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
if (promptInfo.getNegativeButtonText() == null) {
@@ -145,13 +152,6 @@
final List<BiometricSensor> eligibleSensors = new ArrayList<>();
final List<Pair<BiometricSensor, Integer>> ineligibleSensors = new ArrayList<>();
- final int effectiveUserId;
- if (Flags.effectiveUserBp()) {
- effectiveUserId = userManager.getCredentialOwnerProfile(userId);
- } else {
- effectiveUserId = userId;
- }
-
if (biometricRequested) {
for (BiometricSensor sensor : sensors) {
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index 97f4a5c..eeac260 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -32,6 +32,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.net.Uri;
import android.os.Binder;
@@ -75,6 +76,7 @@
private final ChangeReporter mChangeReporter;
private final CompatConfig mCompatConfig;
private final AndroidBuildClassifier mBuildClassifier;
+ private Boolean mIsWear;
public PlatformCompat(Context context) {
super(PermissionEnforcer.fromContext(context));
@@ -511,9 +513,16 @@
}
private ApplicationInfo fixTargetSdk(ApplicationInfo appInfo, int uid) {
+
+ // mIsWear doesn't need to be locked, ok if executes twice
+ if (mIsWear == null) {
+ mIsWear = mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+ }
+
// b/282922910 - we don't want apps sharing system uid and targeting
// older target sdk to impact all system uid apps
- if (Flags.systemUidTargetSystemSdk() && uid == Process.SYSTEM_UID) {
+ if (Flags.systemUidTargetSystemSdk() && !mIsWear &&
+ uid == Process.SYSTEM_UID) {
appInfo.targetSdkVersion = Build.VERSION.SDK_INT;
}
return appInfo;
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 0124e25..bb0b190 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -21,8 +21,6 @@
import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE;
import static com.android.hardware.input.Flags.enableNew25q2Keycodes;
-import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
-import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
import android.annotation.BinderThread;
import android.annotation.MainThread;
@@ -51,7 +49,6 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
-import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.IndentingPrintWriter;
@@ -215,7 +212,7 @@
}
private void initKeyCombinationRules() {
- if (!useKeyGestureEventHandler() || !useKeyGestureEventHandlerMultiPressGestures()) {
+ if (!InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()) {
return;
}
// TODO(b/358569822): Handle Power, Back key properly since key combination gesture is
@@ -441,7 +438,8 @@
public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
- if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
+ if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()
+ && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
return mKeyCombinationManager.interceptKey(event, interactive);
}
return false;
@@ -457,15 +455,18 @@
final long keyConsumed = -1;
final long keyNotConsumed = 0;
- if (mKeyCombinationManager.isKeyConsumed(event)) {
- return keyConsumed;
- }
+ if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()) {
+ if (mKeyCombinationManager.isKeyConsumed(event)) {
+ return keyConsumed;
+ }
- if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
- final long now = SystemClock.uptimeMillis();
- final long interceptTimeout = mKeyCombinationManager.getKeyInterceptTimeout(keyCode);
- if (now < interceptTimeout) {
- return interceptTimeout - now;
+ if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
+ final long now = SystemClock.uptimeMillis();
+ final long interceptTimeout = mKeyCombinationManager.getKeyInterceptTimeout(
+ keyCode);
+ if (now < interceptTimeout) {
+ return interceptTimeout - now;
+ }
}
}
diff --git a/services/core/java/com/android/server/integrity/model/BitInputStream.java b/services/core/java/com/android/server/integrity/model/BitInputStream.java
deleted file mode 100644
index e7cc81e..0000000
--- a/services/core/java/com/android/server/integrity/model/BitInputStream.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2019 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.integrity.model;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/** A wrapper class for reading a stream of bits.
- *
- * <p>Note: this class reads from underlying stream byte-by-byte. It is advised to apply buffering
- * to underlying streams.
- */
-public class BitInputStream {
-
- private long mBitsRead;
-
- private InputStream mInputStream;
-
- private byte mCurrentByte;
-
- public BitInputStream(InputStream inputStream) {
- mInputStream = inputStream;
- }
-
- /**
- * Read the next number of bits from the stream.
- *
- * @param numOfBits The number of bits to read.
- * @return The value read from the stream.
- */
- public int getNext(int numOfBits) throws IOException {
- int component = 0;
- int count = 0;
-
- while (count++ < numOfBits) {
- if (mBitsRead % 8 == 0) {
- mCurrentByte = getNextByte();
- }
- int offset = 7 - (int) (mBitsRead % 8);
-
- component <<= 1;
- component |= (mCurrentByte >>> offset) & 1;
-
- mBitsRead++;
- }
-
- return component;
- }
-
- /** Check if there are bits left in the stream. */
- public boolean hasNext() throws IOException {
- return mInputStream.available() > 0;
- }
-
- private byte getNextByte() throws IOException {
- return (byte) mInputStream.read();
- }
-}
diff --git a/services/core/java/com/android/server/integrity/model/BitOutputStream.java b/services/core/java/com/android/server/integrity/model/BitOutputStream.java
deleted file mode 100644
index 14b35fd..0000000
--- a/services/core/java/com/android/server/integrity/model/BitOutputStream.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2019 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.integrity.model;
-
-import static com.android.server.integrity.model.ComponentBitSize.BYTE_BITS;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Arrays;
-
-/** A wrapper class for writing a stream of bits. */
-public class BitOutputStream {
-
- private static final int BUFFER_SIZE = 4 * 1024;
-
- private int mNextBitIndex;
-
- private final OutputStream mOutputStream;
- private final byte[] mBuffer;
-
- public BitOutputStream(OutputStream outputStream) {
- mBuffer = new byte[BUFFER_SIZE];
- mNextBitIndex = 0;
- mOutputStream = outputStream;
- }
-
- /**
- * Set the next number of bits in the stream to value.
- *
- * @param numOfBits The number of bits used to represent the value.
- * @param value The value to convert to bits.
- */
- public void setNext(int numOfBits, int value) throws IOException {
- if (numOfBits <= 0) {
- return;
- }
-
- // optional: we can do some clever size checking to "OR" an entire segment of bits instead
- // of setting bits one by one, but it is probably not worth it.
- int nextBitMask = 1 << (numOfBits - 1);
- while (numOfBits-- > 0) {
- setNext((value & nextBitMask) != 0);
- nextBitMask >>>= 1;
- }
- }
-
- /**
- * Set the next bit in the stream to value.
- *
- * @param value The value to set the bit to
- */
- public void setNext(boolean value) throws IOException {
- int byteToWrite = mNextBitIndex / BYTE_BITS;
- if (byteToWrite == BUFFER_SIZE) {
- mOutputStream.write(mBuffer);
- reset();
- byteToWrite = 0;
- }
- if (value) {
- mBuffer[byteToWrite] |= 1 << (BYTE_BITS - 1 - (mNextBitIndex % BYTE_BITS));
- }
- mNextBitIndex++;
- }
-
- /** Set the next bit in the stream to true. */
- public void setNext() throws IOException {
- setNext(/* value= */ true);
- }
-
- /**
- * Flush the data written to the underlying {@link java.io.OutputStream}. Any unfinished bytes
- * will be padded with 0.
- */
- public void flush() throws IOException {
- int endByte = mNextBitIndex / BYTE_BITS;
- if (mNextBitIndex % BYTE_BITS != 0) {
- // If next bit is not the first bit of a byte, then mNextBitIndex / BYTE_BITS would be
- // the byte that includes already written bits. We need to increment it so this byte
- // gets written.
- endByte++;
- }
- mOutputStream.write(mBuffer, 0, endByte);
- reset();
- }
-
- /** Reset this output stream to start state. */
- private void reset() {
- mNextBitIndex = 0;
- Arrays.fill(mBuffer, (byte) 0);
- }
-}
diff --git a/services/core/java/com/android/server/integrity/model/ByteTrackedOutputStream.java b/services/core/java/com/android/server/integrity/model/ByteTrackedOutputStream.java
deleted file mode 100644
index ceed054..0000000
--- a/services/core/java/com/android/server/integrity/model/ByteTrackedOutputStream.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2020 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.integrity.model;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/**
- * An output stream that tracks the total number written bytes since construction and allows
- * querying this value any time during the execution.
- *
- * <p>This class is used for constructing the rule indexing.
- */
-public class ByteTrackedOutputStream extends OutputStream {
-
- private int mWrittenBytesCount;
- private final OutputStream mOutputStream;
-
- public ByteTrackedOutputStream(OutputStream outputStream) {
- mWrittenBytesCount = 0;
- mOutputStream = outputStream;
- }
-
- @Override
- public void write(int b) throws IOException {
- mWrittenBytesCount++;
- mOutputStream.write(b);
- }
-
- /**
- * Writes the given bytes into the output stream provided in constructor and updates the total
- * number of written bytes.
- */
- @Override
- public void write(byte[] bytes) throws IOException {
- write(bytes, 0, bytes.length);
- }
-
- @Override
- public void write(byte[] b, int off, int len) throws IOException {
- mWrittenBytesCount += len;
- mOutputStream.write(b, off, len);
- }
-
- /** Returns the total number of bytes written into the output stream at the requested time. */
- public int getWrittenBytesCount() {
- return mWrittenBytesCount;
- }
-}
diff --git a/services/core/java/com/android/server/integrity/model/ComponentBitSize.java b/services/core/java/com/android/server/integrity/model/ComponentBitSize.java
deleted file mode 100644
index 94e6708..0000000
--- a/services/core/java/com/android/server/integrity/model/ComponentBitSize.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2019 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.integrity.model;
-
-import android.content.integrity.Rule;
-
-/**
- * A helper class containing information about the binary representation of different {@link Rule}
- * components.
- */
-public final class ComponentBitSize {
- public static final int FORMAT_VERSION_BITS = 8;
-
- public static final int EFFECT_BITS = 3;
- public static final int KEY_BITS = 4;
- public static final int OPERATOR_BITS = 3;
- public static final int CONNECTOR_BITS = 2;
- public static final int SEPARATOR_BITS = 3;
- public static final int VALUE_SIZE_BITS = 8;
- public static final int IS_HASHED_BITS = 1;
-
- public static final int ATOMIC_FORMULA_START = 0;
- public static final int COMPOUND_FORMULA_START = 1;
- public static final int COMPOUND_FORMULA_END = 2;
- public static final int INSTALLER_ALLOWED_BY_MANIFEST_START = 3;
-
- public static final int DEFAULT_FORMAT_VERSION = 1;
- public static final int SIGNAL_BIT = 1;
-
- public static final int BYTE_BITS = 8;
-}
diff --git a/services/core/java/com/android/server/integrity/model/IndexingFileConstants.java b/services/core/java/com/android/server/integrity/model/IndexingFileConstants.java
deleted file mode 100644
index 0c4052a..0000000
--- a/services/core/java/com/android/server/integrity/model/IndexingFileConstants.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2020 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.integrity.model;
-
-/** A helper class containing special indexing file constants. */
-public final class IndexingFileConstants {
- // We empirically experimented with different block sizes and identified that 50 is in the
- // optimal range of efficient computation.
- public static final int INDEXING_BLOCK_SIZE = 50;
-
- public static final String START_INDEXING_KEY = "START_KEY";
- public static final String END_INDEXING_KEY = "END_KEY";
-}
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index acf62dc..7469c92 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -64,6 +64,7 @@
import android.util.Xml;
import com.android.internal.R;
+import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
@@ -1632,6 +1633,14 @@
continue;
}
+ // If the trunkstable feature flag is disabled for this
+ // exception, skip the tag.
+ if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(
+ /* pkg= */ null, parser, /* allowNoNamespace= */ true)) {
+ XmlUtils.skipCurrentTag(parser);
+ continue;
+ }
+
final boolean fixed =
parser.getAttributeBoolean(null, ATTR_FIXED, false);
final boolean whitelisted =
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 8d039f1..dda5bcf 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -88,7 +88,6 @@
import static com.android.hardware.input.Flags.keyboardA11yShortcutControl;
import static com.android.hardware.input.Flags.modifierShortcutDump;
import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
-import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
import static com.android.server.flags.Flags.modifierShortcutManagerMultiuser;
import static com.android.server.flags.Flags.newBugreportKeyboardShortcut;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY;
@@ -2496,7 +2495,7 @@
private void initKeyCombinationRules() {
mKeyCombinationManager = new KeyCombinationManager(mHandler);
- if (useKeyGestureEventHandler() && useKeyGestureEventHandlerMultiPressGestures()) {
+ if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()) {
return;
}
final boolean screenshotChordEnabled = mContext.getResources().getBoolean(
@@ -3442,7 +3441,7 @@
+ keyguardOn() + " canceled=" + event.isCanceled());
}
- if (!useKeyGestureEventHandler()) {
+ if (!InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()) {
if (mKeyCombinationManager.isKeyConsumed(event)) {
return keyConsumed;
}
@@ -5720,7 +5719,8 @@
}
private void handleKeyGesture(KeyEvent event, boolean interactive, boolean defaultDisplayOn) {
- if (mKeyCombinationManager.interceptKey(event, interactive)) {
+ if (!InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()
+ && mKeyCombinationManager.interceptKey(event, interactive)) {
// handled by combo keys manager.
mSingleKeyGestureDetector.reset();
return;
diff --git a/services/core/java/com/android/server/stats/pull/psi/OWNERS b/services/core/java/com/android/server/stats/pull/psi/OWNERS
new file mode 100644
index 0000000..f72fd7c
--- /dev/null
+++ b/services/core/java/com/android/server/stats/pull/psi/OWNERS
@@ -0,0 +1,9 @@
+jackrichardson@google.com
+dbrotikovskaya@google.com
+ivokay@google.com
+gagapov@google.com
+yigitfiliz@google.com
+rswang@google.com
+evaleriano@google.com
+igorstepanov@google.com
+iyou@google.com
diff --git a/services/core/java/com/android/server/stats/pull/psi/PsiData.java b/services/core/java/com/android/server/stats/pull/psi/PsiData.java
new file mode 100644
index 0000000..d1cbf74
--- /dev/null
+++ b/services/core/java/com/android/server/stats/pull/psi/PsiData.java
@@ -0,0 +1,106 @@
+/*
+ * 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.stats.pull.psi;
+
+/**
+ * Wraps PSI (Pressure Stall Information) corresponding to a system resource. See more details about
+ * PSI, see https://docs.kernel.org/accounting/psi.html#psi-pressure-stall-information.
+ */
+public class PsiData {
+ public enum ResourceType {
+ CPU,
+ MEMORY,
+ IO
+ }
+
+ static class AppsStallInfo {
+
+ /** Past 10s average % of wasted CPU cycles when apps tasks are stalled on mResourceType.*/
+ private final float mAvg10SecPercentage;
+
+ /** Past 60s average % of wasted CPU cycles when apps tasks are stalled on mResourceType.*/
+ private final float mAvg60SecPercentage;
+
+ /** Past 300s average % of wasted CPU cycles when apps tasks are stalled on mResourceType.*/
+ private final float mAvg300SecPercentage;
+
+ /** Total number of microseconds that apps tasks are stalled on mResourceType.*/
+ private final long mTotalUsec;
+
+ AppsStallInfo(
+ float avg10SecPercentage, float avg60SecPercentage,
+ float avg300SecPercentage, long totalUsec) {
+ mAvg10SecPercentage = avg10SecPercentage;
+ mAvg60SecPercentage = avg60SecPercentage;
+ mAvg300SecPercentage = avg300SecPercentage;
+ mTotalUsec = totalUsec;
+ }
+ }
+
+ /** The system resource type of this {@code PsiData}. */
+ private final ResourceType mResourceType;
+
+ /** Info on some tasks are stalled on mResourceType. */
+ private final AppsStallInfo mSomeAppsStallInfo;
+
+ /**
+ * Info on all non-idle tasks are stalled on mResourceType. For the CPU ResourceType,
+ * all fields will always be 0 as it's undefined.
+ */
+ private final AppsStallInfo mFullAppsStallInfo;
+
+ PsiData(
+ ResourceType resourceType,
+ AppsStallInfo someAppsStallInfo,
+ AppsStallInfo fullAppsStallInfo) {
+ mResourceType = resourceType;
+ mSomeAppsStallInfo = someAppsStallInfo;
+ mFullAppsStallInfo = fullAppsStallInfo;
+ }
+
+ public ResourceType getResourceType() {
+ return mResourceType;
+ }
+
+ public float getSomeAvg10SecPercentage() {
+ return mSomeAppsStallInfo.mAvg10SecPercentage; }
+
+ public float getSomeAvg60SecPercentage() {
+ return mSomeAppsStallInfo.mAvg60SecPercentage; }
+
+ public float getSomeAvg300SecPercentage() {
+ return mSomeAppsStallInfo.mAvg300SecPercentage; }
+
+ public long getSomeTotalUsec() {
+ return mSomeAppsStallInfo.mTotalUsec;
+ }
+
+ public float getFullAvg10SecPercentage() {
+ return mFullAppsStallInfo.mAvg10SecPercentage;
+ }
+
+ public float getFullAvg60SecPercentage() {
+ return mFullAppsStallInfo.mAvg60SecPercentage;
+ }
+
+ public float getFullAvg300SecPercentage() {
+ return mFullAppsStallInfo.mAvg300SecPercentage; }
+
+ public long getFullTotalUsec() {
+ return mFullAppsStallInfo.mTotalUsec;
+ }
+}
diff --git a/services/core/java/com/android/server/stats/pull/psi/PsiExtractor.java b/services/core/java/com/android/server/stats/pull/psi/PsiExtractor.java
new file mode 100644
index 0000000..5d0d7e1
--- /dev/null
+++ b/services/core/java/com/android/server/stats/pull/psi/PsiExtractor.java
@@ -0,0 +1,161 @@
+/*
+ * 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.stats.pull.psi;
+
+import static java.util.stream.Collectors.joining;
+
+import android.annotation.Nullable;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.text.MessageFormat;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class PsiExtractor {
+ private static final String TAG = "PsiExtractor";
+
+ // Paths for PSI files are guarded by SELinux policy. PCS needs to be explicitly
+ // allowlisted to access these files.
+ private static final String PSI_MEMORY_PATH = "/proc/pressure/memory";
+ private static final String PSI_IO_PATH = "/proc/pressure/io";
+ private static final String PSI_CPU_PATH = "/proc/pressure/cpu";
+
+ // The patterns matching a line of PSI output such as
+ // "some avg10=0.12 avg60=0.34 avg300=0.56 total=123456" or
+ // "full avg10=0.12 avg60=0.34 avg300=0.56 total=123456" to extract the stalling percentage
+ // values for "some" and "full" line of PSI output respectively.
+ private static final String PSI_PATTERN_TEMPLATE =
+ ".*{0} avg10=(\\d+.\\d+) avg60=(\\d+.\\d+) avg300=(\\d+.\\d+) total=(\\d+).*";
+ private static final String SOME = "some";
+ private static final String FULL = "full";
+ private final PsiReader mPsiReader;
+
+ public PsiExtractor() {
+ mPsiReader = new PsiReader();
+ }
+ public PsiExtractor(PsiReader psiReader) {
+ mPsiReader = psiReader;
+ }
+
+ /**
+ * Parses /pressure/proc/{resourceType} kernel file to extract the Pressure Stall Information
+ * (PSI), more information: can be found at https://docs.kernel.org/accounting/psi.html.
+ *
+ * @param resourceType (Memory/CPU/IO) to get the PSI for.
+ */
+ @Nullable
+ public PsiData getPsiData(PsiData.ResourceType resourceType) {
+ String psiFileData;
+ if (resourceType == PsiData.ResourceType.MEMORY) {
+ psiFileData = mPsiReader.read(PSI_MEMORY_PATH);
+ } else if (resourceType == PsiData.ResourceType.IO) {
+ psiFileData = mPsiReader.read(PSI_IO_PATH);
+ } else if (resourceType == PsiData.ResourceType.CPU) {
+ psiFileData = mPsiReader.read(PSI_CPU_PATH);
+ } else {
+ Log.w(TAG, "PsiExtractor failure: cannot read kernel source file, returning null");
+ return null;
+ }
+ return parsePsiData(psiFileData, resourceType);
+ }
+
+ @Nullable
+ private static PsiData.AppsStallInfo parsePsiString(
+ String psiFileData, String appType, PsiData.ResourceType resourceType) {
+ // There is an extra case of file content: the CPU full is undefined and isn't reported for
+ // earlier versions. It should be always propagated as 0, but for the current logic purposes
+ // we will report atom only if at least one value (some/full) is presented. Thus, hardcoding
+ // the "full" line as 0 only when the "some" line is presented.
+ if (appType == FULL && resourceType == PsiData.ResourceType.CPU) {
+ if (psiFileData.contains(SOME) && !psiFileData.contains(FULL)) {
+ return new PsiData.AppsStallInfo((float) 0.0, (float) 0.0, (float) 0.0, 0);
+ }
+ }
+
+ Pattern psiStringPattern = Pattern.compile(
+ MessageFormat.format(PSI_PATTERN_TEMPLATE, appType));
+ Matcher psiLineMatcher = psiStringPattern.matcher(psiFileData);
+
+ // Parsing the line starts with "some" in the expected output.
+ // The line for "some" should always be present in PSI output. The output must be somehow
+ // malformed if the line cannot be matched.
+ if (!psiLineMatcher.find()) {
+ Log.w(TAG,
+ "Returning null: the line \"" + appType + "\" is not in expected pattern.");
+ return null;
+ }
+ try {
+ return new PsiData.AppsStallInfo(
+ Float.parseFloat(psiLineMatcher.group(1)),
+ Float.parseFloat(psiLineMatcher.group(2)),
+ Float.parseFloat(psiLineMatcher.group(3)),
+ Long.parseLong(psiLineMatcher.group(4)));
+ } catch (NumberFormatException e) {
+ Log.w(TAG,
+ "Returning null: some value in line \"" + appType
+ + "\" cannot be parsed as numeric.");
+ return null;
+ }
+ }
+
+ @Nullable
+ private static PsiData parsePsiData(
+ String psiFileData, PsiData.ResourceType resourceType) {
+ PsiData.AppsStallInfo someAppsStallInfo = parsePsiString(psiFileData, SOME, resourceType);
+ PsiData.AppsStallInfo fullAppsStallInfo = parsePsiString(psiFileData, FULL, resourceType);
+
+ if (someAppsStallInfo == null && fullAppsStallInfo == null) {
+ Log.w(TAG, "Returning empty PSI: some or full line are failed to parse");
+ return null;
+ } else if (someAppsStallInfo == null) {
+ Log.d(TAG, "Replacing some info with empty PSI record for the resource type "
+ + resourceType);
+ someAppsStallInfo = new PsiData.AppsStallInfo(
+ (float) -1.0, (float) -1.0, (float) -1.0, -1);
+ } else if (fullAppsStallInfo == null) {
+ Log.d(TAG, "Replacing full info with empty PSI record for the resource type "
+ + resourceType);
+ fullAppsStallInfo = new PsiData.AppsStallInfo(
+ (float) -1.0, (float) -1.0, (float) -1.0, -1);
+ }
+ return new PsiData(resourceType, someAppsStallInfo, fullAppsStallInfo);
+ }
+
+ /** Dependency class */
+ public static class PsiReader {
+ /**
+ * Reads file from provided path and returns its content if the file found, null otherwise.
+ *
+ * @param filePath file path to read.
+ */
+ @Nullable
+ public String read(String filePath) {
+ try (BufferedReader br =
+ new BufferedReader(new InputStreamReader(
+ new FileInputStream(filePath)))) {
+ return br.lines().collect(joining(System.lineSeparator()));
+ } catch (IOException e) {
+ Log.w(TAG, "Cannot read file " + filePath);
+ return null;
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b27a2a5..1c11c67 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8896,6 +8896,7 @@
mAppCompatController.getAppCompatSizeCompatModePolicy();
if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()
+ && mAppCompatDisplayInsets != null
&& !mAppCompatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) {
// App prefers to keep its original size.
// If the size compat is from previous fixed orientation letterboxing, we may want to
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
index fa2c716..e8eae4f 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
@@ -70,15 +70,8 @@
mAppCompatAspectRatioState.reset();
}
- float getDesiredAspectRatio(@NonNull Configuration newParentConfig,
+ private float getDesiredAspectRatio(@NonNull Configuration newParentConfig,
@NonNull Rect parentBounds) {
- // If in camera compat mode, aspect ratio from the camera compat policy has priority over
- // default letterbox aspect ratio.
- if (AppCompatCameraPolicy.shouldCameraCompatControlAspectRatio(
- mActivityRecord)) {
- return AppCompatCameraPolicy.getCameraCompatAspectRatio(mActivityRecord);
- }
-
final float letterboxAspectRatioOverride =
mAppCompatOverrides.getAppCompatAspectRatioOverrides()
.getFixedOrientationLetterboxAspectRatio(newParentConfig);
@@ -120,7 +113,16 @@
if (mTransparentPolicy.isRunning()) {
return mTransparentPolicy.getInheritedMinAspectRatio();
}
+
final ActivityInfo info = mActivityRecord.info;
+
+ // If in camera compat mode, aspect ratio from the camera compat policy has priority over
+ // the default aspect ratio.
+ if (AppCompatCameraPolicy.shouldCameraCompatControlAspectRatio(mActivityRecord)) {
+ return Math.max(AppCompatCameraPolicy.getCameraCompatMinAspectRatio(mActivityRecord),
+ info.getMinAspectRatio());
+ }
+
final AppCompatAspectRatioOverrides aspectRatioOverrides =
mAppCompatOverrides.getAppCompatAspectRatioOverrides();
if (aspectRatioOverrides.shouldApplyUserMinAspectRatioOverride()) {
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index 8c5689c1..8be66cc 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -234,7 +234,7 @@
}
// TODO(b/369070416): have policies implement the same interface.
- static float getCameraCompatAspectRatio(@NonNull ActivityRecord activity) {
+ static float getCameraCompatMinAspectRatio(@NonNull ActivityRecord activity) {
final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
if (cameraPolicy == null) {
return 1.0f;
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 2664d8c..6091b83 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -771,20 +771,21 @@
mTopTurnScreenOnActivity.setCurrentLaunchCanTurnScreenOn(false);
}
- boolean hasChange = false;
- if (!lastKeyguardGoingAway && mKeyguardGoingAway) {
+ final boolean startedGoingAway = (!lastKeyguardGoingAway && mKeyguardGoingAway);
+ final boolean occludedChanged = (lastOccluded != mOccluded);
+
+ if (startedGoingAway) {
writeEventLog("dismissIfInsecure");
controller.handleDismissInsecureKeyguard(display);
controller.scheduleGoingAwayTimeout(mDisplayId);
- hasChange = true;
- } else if (lastOccluded != mOccluded) {
+ }
+ if (occludedChanged && (reduceKeyguardTransitions() || !startedGoingAway)) {
controller.handleOccludedChanged(mDisplayId, mTopOccludesActivity);
- hasChange = true;
}
// Collect the participants for shell transition, so that transition won't happen too
// early since the transition was set ready.
- if (hasChange && top != null && (mOccluded || mKeyguardGoingAway)) {
+ if (top != null && (startedGoingAway || (occludedChanged && mOccluded))) {
display.mTransitionController.collect(top);
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index c653038..50f3f39 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -21030,6 +21030,27 @@
}
@Override
+ public boolean removeManagedProfile(int userId) {
+ Preconditions.checkCallAuthorization(
+ hasCallingOrSelfPermission(MANAGE_PROFILE_AND_DEVICE_OWNERS));
+
+ if (!isManagedProfile(userId)){
+ throw new IllegalArgumentException("Cannot remove user as it is not a managed profile");
+ }
+
+ boolean success = false;
+ final long identity = Binder.clearCallingIdentity();
+ try{
+ success = mUserManager.removeUserEvenWhenDisallowed(userId);
+ } catch (Exception e) {
+ Slogf.e(LOG_TAG, "Remove managed profile failed due to: ", e);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ return success;
+ }
+
+ @Override
public UserHandle createAndProvisionManagedProfile(
@NonNull ManagedProfileProvisioningParams provisioningParams,
@NonNull String callerPackage) {
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java b/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
index fead05b..5df9dd5 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
@@ -35,6 +35,14 @@
public abstract boolean isSupervisionEnabledForUser(@UserIdInt int userId);
/**
+ * Set whether supervision is enabled for the specified user.
+ *
+ * @param userId The user to set the supervision state for
+ * @param enabled Whether or not the user should be supervised
+ */
+ public abstract void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled);
+
+ /**
* Sets whether the supervision lock screen should be shown for the specified user
*
* @param userId The user set the superivision state for
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java
index 4c515c1..67e2547 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionService.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java
@@ -19,7 +19,9 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.app.admin.DevicePolicyManagerInternal;
import android.app.supervision.ISupervisionManager;
+import android.content.ComponentName;
import android.content.Context;
import android.content.pm.UserInfo;
import android.os.Bundle;
@@ -28,19 +30,19 @@
import android.os.ShellCallback;
import android.util.SparseArray;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalServices;
import com.android.server.SystemService;
+import com.android.server.SystemService.TargetUser;
import com.android.server.pm.UserManagerInternal;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-/**
- * Service for handling system supervision.
- */
+/** Service for handling system supervision. */
public class SupervisionService extends ISupervisionManager.Stub {
private static final String LOG_TAG = "SupervisionService";
@@ -52,14 +54,25 @@
@GuardedBy("getLockObject()")
private final SparseArray<SupervisionUserData> mUserData = new SparseArray<>();
+ private final DevicePolicyManagerInternal mDpmInternal;
private final UserManagerInternal mUserManagerInternal;
public SupervisionService(Context context) {
mContext = context.createAttributionContext(LOG_TAG);
+ mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class);
mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
mUserManagerInternal.addUserLifecycleListener(new UserLifecycleListener());
}
+ void syncStateWithDevicePolicyManager(TargetUser user) {
+ if (user.isPreCreated()) return;
+
+ // Ensure that supervision is enabled when supervision app is the profile owner.
+ if (android.app.admin.flags.Flags.enableSupervisionServiceSync() && isProfileOwner(user)) {
+ setSupervisionEnabledForUser(user.getUserIdentifier(), true);
+ }
+ }
+
@Override
public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
synchronized (getLockObject()) {
@@ -74,14 +87,15 @@
@Nullable FileDescriptor err,
@NonNull String[] args,
@Nullable ShellCallback callback,
- @NonNull ResultReceiver resultReceiver) throws RemoteException {
+ @NonNull ResultReceiver resultReceiver)
+ throws RemoteException {
new SupervisionServiceShellCommand(this)
.exec(this, in, out, err, args, callback, resultReceiver);
}
@Override
- protected void dump(@NonNull FileDescriptor fd,
- @NonNull PrintWriter printWriter, @Nullable String[] args) {
+ protected void dump(
+ @NonNull FileDescriptor fd, @NonNull PrintWriter printWriter, @Nullable String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, printWriter)) return;
try (var pw = new IndentingPrintWriter(printWriter, " ")) {
@@ -120,6 +134,17 @@
}
}
+ /** Returns whether the supervision app has profile owner status. */
+ private boolean isProfileOwner(TargetUser user) {
+ ComponentName profileOwner = mDpmInternal.getProfileOwnerAsUser(user.getUserIdentifier());
+ if (profileOwner == null) {
+ return false;
+ }
+
+ String configPackage = mContext.getResources().getString(R.string.config_systemSupervision);
+ return profileOwner.getPackageName().equals(configPackage);
+ }
+
public static class Lifecycle extends SystemService {
private final SupervisionService mSupervisionService;
@@ -133,13 +158,24 @@
publishLocalService(SupervisionManagerInternal.class, mSupervisionService.mInternal);
publishBinderService(Context.SUPERVISION_SERVICE, mSupervisionService);
}
+
+ @Override
+ public void onUserStarting(@NonNull TargetUser user) {
+ mSupervisionService.syncStateWithDevicePolicyManager(user);
+ }
}
- final SupervisionManagerInternal mInternal = new SupervisionManagerInternal() {
+ final SupervisionManagerInternal mInternal = new SupervisionManagerInternalImpl();
+
+ private final class SupervisionManagerInternalImpl extends SupervisionManagerInternal {
+ @Override
public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
- synchronized (getLockObject()) {
- return getUserDataLocked(userId).supervisionEnabled;
- }
+ return SupervisionService.this.isSupervisionEnabledForUser(userId);
+ }
+
+ @Override
+ public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) {
+ SupervisionService.this.setSupervisionEnabledForUser(userId, enabled);
}
@Override
@@ -151,7 +187,7 @@
data.supervisionLockScreenOptions = options;
}
}
- };
+ }
private final class UserLifecycleListener implements UserManagerInternal.UserLifecycleListener {
@Override
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
index c418151..96c6cbc 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
@@ -123,7 +123,7 @@
Assert.assertEquals("mic mute reporting wrong value",
muted, mAudioService.isMicrophoneMuted());
// verify the intent for mic mute changed is supposed to be fired
- Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
+ mTestLooper.dispatchAll();
verify(mSpySystemServer, times(1))
.sendMicrophoneMuteChangedIntent();
reset(mSpySystemServer);
@@ -148,7 +148,7 @@
Assert.assertEquals("mic mute reporting wrong value",
!muted, mAudioService.isMicrophoneMuted());
// verify the intent for mic mute changed is supposed to be fired
- Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
+ mTestLooper.dispatchAll();
verify(mSpySystemServer, times(1))
.sendMicrophoneMuteChangedIntent();
reset(mSpySystemServer);
@@ -159,8 +159,7 @@
public void testRingNotifAlias() throws Exception {
Log.i(TAG, "running testRingNotifAlias");
Assert.assertNotNull(mAudioService);
- // TODO add initialization message that can be caught here instead of sleeping
- Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS); // wait for full AudioService initialization
+ mTestLooper.dispatchAll(); // wait for full AudioService initialization
// test with aliasing RING and NOTIFICATION
mAudioService.setNotifAliasRingForTest(true);
@@ -171,7 +170,7 @@
mAudioService.setStreamVolume(AudioSystem.STREAM_NOTIFICATION,
ringVol, 0, "bla");
mAudioService.setStreamVolume(AudioSystem.STREAM_RING, ringMaxVol, 0, "bla");
- Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
+ mTestLooper.dispatchAll();
Assert.assertEquals(ringMaxVol,
mAudioService.getStreamVolume(AudioSystem.STREAM_NOTIFICATION));
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
index cf628ad..b81bf3c 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
@@ -332,6 +332,28 @@
assertThat(preAuthInfo.callingUserId).isEqualTo(USER_ID);
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_EFFECTIVE_USER_BP)
+ public void testCredentialOwnerIdAsUserId_forMandatoryBiometrics() throws Exception {
+ when(mUserManager.getCredentialOwnerProfile(USER_ID)).thenReturn(OWNER_ID);
+ when(mSettingObserver.getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(
+ OWNER_ID)).thenReturn(true);
+ when(mSettingObserver.getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(
+ USER_ID)).thenReturn(false);
+ when(mTrustManager.isInSignificantPlace()).thenReturn(false);
+
+ final BiometricSensor sensor = getFaceSensor();
+ final PromptInfo promptInfo = new PromptInfo();
+ promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
+ promptInfo.setNegativeButtonText(TEST_PACKAGE_NAME);
+ final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
+ mSettingObserver, List.of(sensor), USER_ID , promptInfo, TEST_PACKAGE_NAME,
+ false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager,
+ mUserManager);
+
+ assertThat(preAuthInfo.getIsMandatoryBiometricsAuthentication()).isTrue();
+ }
+
private BiometricSensor getFingerprintSensor() {
BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FINGERPRINT,
TYPE_FINGERPRINT, BiometricManager.Authenticators.BIOMETRIC_STRONG,
diff --git a/services/tests/servicestests/src/com/android/server/integrity/model/ByteTrackedOutputStreamTest.java b/services/tests/servicestests/src/com/android/server/integrity/model/ByteTrackedOutputStreamTest.java
deleted file mode 100644
index 57274bf..0000000
--- a/services/tests/servicestests/src/com/android/server/integrity/model/ByteTrackedOutputStreamTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2020 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.integrity.model;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.ByteArrayOutputStream;
-
-@RunWith(JUnit4.class)
-public class ByteTrackedOutputStreamTest {
-
- @Test
- public void testConstructorStartsWithZeroBytesWritten() {
- ByteTrackedOutputStream byteTrackedOutputStream =
- new ByteTrackedOutputStream(new ByteArrayOutputStream());
-
- assertThat(byteTrackedOutputStream.getWrittenBytesCount()).isEqualTo(0);
- }
-
- @Test
- public void testSuccessfulWriteAndValidateWrittenBytesCount_directFromByteArray()
- throws Exception {
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- ByteTrackedOutputStream byteTrackedOutputStream = new ByteTrackedOutputStream(outputStream);
-
- byte[] outputContent = "This is going to be outputed for tests.".getBytes();
- byteTrackedOutputStream.write(outputContent);
-
- assertThat(byteTrackedOutputStream.getWrittenBytesCount()).isEqualTo(outputContent.length);
- assertThat(outputStream.toByteArray().length).isEqualTo(outputContent.length);
- }
-
- @Test
- public void testSuccessfulWriteAndValidateWrittenBytesCount_fromBitStream() throws Exception {
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- ByteTrackedOutputStream byteTrackedOutputStream = new ByteTrackedOutputStream(outputStream);
-
- BitOutputStream bitOutputStream = new BitOutputStream(byteTrackedOutputStream);
- bitOutputStream.setNext(/* numOfBits= */5, /* value= */1);
- bitOutputStream.flush();
-
- // Even though we wrote 5 bits, this will complete to 1 byte.
- assertThat(byteTrackedOutputStream.getWrittenBytesCount()).isEqualTo(1);
-
- // Add a bit less than 2 bytes (10 bits).
- bitOutputStream.setNext(/* numOfBits= */10, /* value= */1);
- bitOutputStream.flush();
- assertThat(byteTrackedOutputStream.getWrittenBytesCount()).isEqualTo(3);
-
- assertThat(outputStream.toByteArray().length).isEqualTo(3);
- }
-}
diff --git a/services/tests/servicestests/src/com/android/server/stats/pull/psi/OWNERS b/services/tests/servicestests/src/com/android/server/stats/pull/psi/OWNERS
new file mode 100644
index 0000000..e068a84
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/stats/pull/psi/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/stats/pull/psi/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/stats/pull/psi/PsiExtractorTest.java b/services/tests/servicestests/src/com/android/server/stats/pull/psi/PsiExtractorTest.java
new file mode 100644
index 0000000..b563c08
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/stats/pull/psi/PsiExtractorTest.java
@@ -0,0 +1,305 @@
+/*
+ * 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.stats.pull.psi;
+
+import static org.testng.AssertJUnit.assertEquals;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+
+public class PsiExtractorTest {
+ @Mock
+ private PsiExtractor.PsiReader mPsiReader;
+ private PsiExtractor mPsiExtractor;
+ // PSI file content with both some and full lines.
+ private static final String PSI_FILE_CONTENT_BOTH_LINES =
+ "some avg10=0.12 avg60=0.34 avg300=0.56 total=12345678\n"
+ + "full avg10=0.21 avg60=0.43 avg300=0.65 total=87654321";
+ // PSI file content with only some line.
+ private static final String PSI_FILE_CONTENT_ONLY_SOME_LINE =
+ "some avg10=0.12 avg60=0.34 avg300=0.56 total=12345678";
+
+ // PSI file content with only full line.
+ private static final String PSI_FILE_CONTENT_ONLY_FULL_LINE =
+ "\nfull avg10=0.21 avg60=0.43 avg300=0.65 total=87654321";
+
+ // PSI file content that is malformed with "avg60" missing from the both lines.
+ private static final String BOTH_AVG60_MISSING_PSI_FILE_CONTENT =
+ "some avg10=0.12 avg300=0.56 total=12345678\n"
+ + "full avg10=0.21 avg300=0.65 total=87654321";
+
+ // PSI file content that is malformed with non number "avg10" from the both lines.
+ private static final String NON_NUM_AVG10_PSI_FILE_CONTENT =
+ "some avg10=1.a2 avg300=0.56 total=12345678\n"
+ + "full avg10=0.2s1 avg60=0.43 avg300=0.65 total=87654321";
+
+ // PSI file content that is malformed with non number "avg300" from the both lines.
+ private static final String NON_NUM_AVG300_PSI_FILE_CONTENT =
+ "some avg10=0.2 avg60=0.43 avg300=0.5ss6 total=12345678\n"
+ + "full avg10=0.21 avg60=0.43 avg300=0.6b5 total=87654321";
+
+ // PSI file content that is malformed with non number "total" from the both lines.
+ private static final String BOTH_TOTAL_MISSING_PSI_FILE_CONTENT =
+ "some avg10=0.2 avg60=0.43 avg300=0.56\n"
+ + "full avg10=0.21 avg60=0.43 avg300=0.65";
+
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mPsiExtractor = new PsiExtractor(mPsiReader);
+ }
+
+ @Test
+ public void getPsiData_bothLinesPresentedAndValidMemory() {
+ Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+ PSI_FILE_CONTENT_BOTH_LINES);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+ assertEquals(psiData.getResourceType(), PsiData.ResourceType.MEMORY);
+ assertEquals(psiData.getSomeAvg10SecPercentage(), (float) 0.12);
+ assertEquals(psiData.getSomeAvg60SecPercentage(), (float) 0.34);
+ assertEquals(psiData.getSomeAvg300SecPercentage(), (float) 0.56);
+ assertEquals(psiData.getSomeTotalUsec(), 12345678);
+ assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.21);
+ assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.43);
+ assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.65);
+ assertEquals(psiData.getFullTotalUsec(), 87654321);
+ }
+
+ @Test
+ public void getPsiData_bothLinesPresentedAndValidCpu() {
+ Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+ PSI_FILE_CONTENT_BOTH_LINES);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+ assertEquals(psiData.getResourceType(), PsiData.ResourceType.CPU);
+ assertEquals(psiData.getSomeAvg10SecPercentage(), (float) 0.12);
+ assertEquals(psiData.getSomeAvg60SecPercentage(), (float) 0.34);
+ assertEquals(psiData.getSomeAvg300SecPercentage(), (float) 0.56);
+ assertEquals(psiData.getSomeTotalUsec(), 12345678);
+ assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.21);
+ assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.43);
+ assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.65);
+ assertEquals(psiData.getFullTotalUsec(), 87654321);
+ }
+
+ @Test
+ public void getPsiData_bothLinesPresentedAndValidIO() {
+ Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+ PSI_FILE_CONTENT_BOTH_LINES);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+ assertEquals(psiData.getResourceType(), PsiData.ResourceType.IO);
+ assertEquals(psiData.getSomeAvg10SecPercentage(), (float) 0.12);
+ assertEquals(psiData.getSomeAvg60SecPercentage(), (float) 0.34);
+ assertEquals(psiData.getSomeAvg300SecPercentage(), (float) 0.56);
+ assertEquals(psiData.getSomeTotalUsec(), 12345678);
+ assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.21);
+ assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.43);
+ assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.65);
+ assertEquals(psiData.getFullTotalUsec(), 87654321);
+ }
+
+ @Test
+ public void getPsiData_onlySomePresentedAndValidMemory() {
+ Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+ PSI_FILE_CONTENT_ONLY_SOME_LINE);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+ assertEquals(psiData.getResourceType(), PsiData.ResourceType.MEMORY);
+ assertEquals(psiData.getSomeAvg10SecPercentage(), (float) 0.12);
+ assertEquals(psiData.getSomeAvg60SecPercentage(), (float) 0.34);
+ assertEquals(psiData.getSomeAvg300SecPercentage(), (float) 0.56);
+ assertEquals(psiData.getSomeTotalUsec(), 12345678);
+ assertEquals(psiData.getFullAvg10SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getFullAvg60SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getFullAvg300SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getFullTotalUsec(), -1);
+ }
+
+ @Test
+ public void getPsiData_onlySomePresentedAndValidCpu() {
+ Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+ PSI_FILE_CONTENT_ONLY_SOME_LINE);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+ assertEquals(psiData.getResourceType(), PsiData.ResourceType.CPU);
+ assertEquals(psiData.getSomeAvg10SecPercentage(), (float) 0.12);
+ assertEquals(psiData.getSomeAvg60SecPercentage(), (float) 0.34);
+ assertEquals(psiData.getSomeAvg300SecPercentage(), (float) 0.56);
+ assertEquals(psiData.getSomeTotalUsec(), 12345678);
+ assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.0);
+ assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.0);
+ assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.0);
+ assertEquals(psiData.getFullTotalUsec(), 0);
+ }
+
+ @Test
+ public void getPsiData_onlySomePresentedAndValidIO() {
+ Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+ PSI_FILE_CONTENT_ONLY_SOME_LINE);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+ assertEquals(psiData.getResourceType(), PsiData.ResourceType.IO);
+ assertEquals(psiData.getSomeAvg10SecPercentage(), (float) 0.12);
+ assertEquals(psiData.getSomeAvg60SecPercentage(), (float) 0.34);
+ assertEquals(psiData.getSomeAvg300SecPercentage(), (float) 0.56);
+ assertEquals(psiData.getSomeTotalUsec(), 12345678);
+ assertEquals(psiData.getFullAvg10SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getFullAvg60SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getFullAvg300SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getFullTotalUsec(), -1);
+ }
+
+ @Test
+ public void getPsiData_onlyFullPresentedAndValidMemory() {
+ Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+ PSI_FILE_CONTENT_ONLY_FULL_LINE);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+ assertEquals(psiData.getResourceType(), PsiData.ResourceType.MEMORY);
+ assertEquals(psiData.getSomeAvg10SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getSomeAvg60SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getSomeAvg300SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getSomeTotalUsec(), -1);
+ assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.21);
+ assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.43);
+ assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.65);
+ assertEquals(psiData.getFullTotalUsec(), 87654321);
+ }
+
+ @Test
+ public void getPsiData_onlyFullPresentedAndValidCpu() {
+ Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+ PSI_FILE_CONTENT_ONLY_FULL_LINE);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+ assertEquals(psiData.getResourceType(), PsiData.ResourceType.CPU);
+ assertEquals(psiData.getSomeAvg10SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getSomeAvg60SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getSomeAvg300SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getSomeTotalUsec(), -1);
+ assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.21);
+ assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.43);
+ assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.65);
+ assertEquals(psiData.getFullTotalUsec(), 87654321);
+ }
+
+ @Test
+ public void getPsiData_onlyFullPresentedAndValidIO() {
+ Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+ PSI_FILE_CONTENT_ONLY_FULL_LINE);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+ assertEquals(psiData.getResourceType(), PsiData.ResourceType.IO);
+ assertEquals(psiData.getSomeAvg10SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getSomeAvg60SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getSomeAvg300SecPercentage(), (float) -1.0);
+ assertEquals(psiData.getSomeTotalUsec(), -1);
+ assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.21);
+ assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.43);
+ assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.65);
+ assertEquals(psiData.getFullTotalUsec(), 87654321);
+ }
+
+ @Test
+ public void getPsiData_emptyFile() {
+ Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn("");
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+ assertEquals(psiData, null);
+
+ Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn("");
+ psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+ assertEquals(psiData, null);
+
+ Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn("");
+ psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+ assertEquals(psiData, null);
+ }
+
+ @Test
+ public void getPsiData_avg60Missing() {
+ Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+ BOTH_AVG60_MISSING_PSI_FILE_CONTENT);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+ assertEquals(psiData, null);
+
+ Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+ BOTH_AVG60_MISSING_PSI_FILE_CONTENT);
+ psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+ assertEquals(psiData, null);
+
+ Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+ BOTH_AVG60_MISSING_PSI_FILE_CONTENT);
+ psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+ assertEquals(psiData, null);
+ }
+
+ @Test
+ public void getPsiData_totalMissing() {
+ Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+ BOTH_TOTAL_MISSING_PSI_FILE_CONTENT);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+ assertEquals(psiData, null);
+
+ Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+ BOTH_TOTAL_MISSING_PSI_FILE_CONTENT);
+ psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+ assertEquals(psiData, null);
+
+ Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+ BOTH_TOTAL_MISSING_PSI_FILE_CONTENT);
+ psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+ assertEquals(psiData, null);
+ }
+
+ @Test
+ public void getPsiData_avg10NonNum() {
+ Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+ NON_NUM_AVG10_PSI_FILE_CONTENT);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+ assertEquals(psiData, null);
+
+ Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+ NON_NUM_AVG10_PSI_FILE_CONTENT);
+ psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+ assertEquals(psiData, null);
+
+ Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+ NON_NUM_AVG10_PSI_FILE_CONTENT);
+ psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+ assertEquals(psiData, null);
+ }
+
+ @Test
+ public void getPsiData_avg300NonNum() {
+ Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+ NON_NUM_AVG300_PSI_FILE_CONTENT);
+ PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+ assertEquals(psiData, null);
+
+ Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+ NON_NUM_AVG300_PSI_FILE_CONTENT);
+ psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+ assertEquals(psiData, null);
+
+ Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+ NON_NUM_AVG300_PSI_FILE_CONTENT);
+ psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+ assertEquals(psiData, null);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
index 6bd4279..79b06236 100644
--- a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
+++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
@@ -16,12 +16,20 @@
package com.android.server.supervision
+import android.app.admin.DevicePolicyManagerInternal
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.UserInfo
import android.os.Bundle
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.R
import com.android.server.LocalServices
+import com.android.server.SystemService.TargetUser
import com.android.server.pm.UserManagerInternal
import com.google.common.truth.Truth.assertThat
-import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -29,11 +37,12 @@
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.whenever
/**
- * Unit tests for {@link SupervisionService}.
- * <p/>
- * Run with <code>atest SupervisionServiceTest</code>.
+ * Unit tests for [SupervisionService].
+ *
+ * Run with `atest SupervisionServiceTest`.
*/
@RunWith(AndroidJUnit4::class)
class SupervisionServiceTest {
@@ -41,18 +50,21 @@
const val USER_ID = 100
}
+ @get:Rule val mocks: MockitoRule = MockitoJUnit.rule()
+ @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ @Mock private lateinit var mockDpmInternal: DevicePolicyManagerInternal
+ @Mock private lateinit var mockUserManagerInternal: UserManagerInternal
+
+ private lateinit var context: Context
private lateinit var service: SupervisionService
- @Rule
- @JvmField
- val mocks: MockitoRule = MockitoJUnit.rule()
-
- @Mock
- private lateinit var mockUserManagerInternal: UserManagerInternal
-
@Before
- fun setup() {
- val context = InstrumentationRegistry.getInstrumentation().context
+ fun setUp() {
+ context = InstrumentationRegistry.getInstrumentation().context
+
+ LocalServices.removeServiceForTest(DevicePolicyManagerInternal::class.java)
+ LocalServices.addService(DevicePolicyManagerInternal::class.java, mockDpmInternal)
LocalServices.removeServiceForTest(UserManagerInternal::class.java)
LocalServices.addService(UserManagerInternal::class.java, mockUserManagerInternal)
@@ -61,7 +73,46 @@
}
@Test
- fun testSetSupervisionEnabledForUser() {
+ @RequiresFlagsEnabled(android.app.admin.flags.Flags.FLAG_ENABLE_SUPERVISION_SERVICE_SYNC)
+ fun syncStateWithDevicePolicyManager_supervisionAppIsProfileOwner_enablesSupervision() {
+ val supervisionPackageName =
+ context.getResources().getString(R.string.config_systemSupervision)
+
+ whenever(mockDpmInternal.getProfileOwnerAsUser(USER_ID))
+ .thenReturn(ComponentName(supervisionPackageName, "MainActivity"))
+
+ service.syncStateWithDevicePolicyManager(newTargetUser(USER_ID))
+
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.app.admin.flags.Flags.FLAG_ENABLE_SUPERVISION_SERVICE_SYNC)
+ fun syncStateWithDevicePolicyManager_userPreCreated_doesNotEnableSupervision() {
+ val supervisionPackageName =
+ context.getResources().getString(R.string.config_systemSupervision)
+
+ whenever(mockDpmInternal.getProfileOwnerAsUser(USER_ID))
+ .thenReturn(ComponentName(supervisionPackageName, "MainActivity"))
+
+ service.syncStateWithDevicePolicyManager(newTargetUser(USER_ID, preCreated = true))
+
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.app.admin.flags.Flags.FLAG_ENABLE_SUPERVISION_SERVICE_SYNC)
+ fun syncStateWithDevicePolicyManager_supervisionAppIsNotProfileOwner_doesNotEnableSupervision() {
+ whenever(mockDpmInternal.getProfileOwnerAsUser(USER_ID))
+ .thenReturn(ComponentName("other.package", "MainActivity"))
+
+ service.syncStateWithDevicePolicyManager(newTargetUser(USER_ID))
+
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+ }
+
+ @Test
+ fun setSupervisionEnabledForUser() {
assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
service.setSupervisionEnabledForUser(USER_ID, true)
@@ -72,7 +123,18 @@
}
@Test
- fun testSetSupervisionLockscreenEnabledForUser() {
+ fun supervisionEnabledForUser_internal() {
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+
+ service.mInternal.setSupervisionEnabledForUser(USER_ID, true)
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+
+ service.mInternal.setSupervisionEnabledForUser(USER_ID, false)
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+ }
+
+ @Test
+ fun setSupervisionLockscreenEnabledForUser() {
var userData = service.getUserDataLocked(USER_ID)
assertThat(userData.supervisionLockScreenEnabled).isFalse()
assertThat(userData.supervisionLockScreenOptions).isNull()
@@ -87,4 +149,10 @@
assertThat(userData.supervisionLockScreenEnabled).isFalse()
assertThat(userData.supervisionLockScreenOptions).isNull()
}
+
+ private fun newTargetUser(userId: Int, preCreated: Boolean = false): TargetUser {
+ val userInfo = UserInfo(userId, /* name= */ "tempUser", /* flags= */ 0)
+ userInfo.preCreated = preCreated
+ return TargetUser(userInfo)
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index c3466b9..fee646d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3139,11 +3139,13 @@
@Test
public void testOnStartingWindowDrawn() {
+ // Skip unnecessary resume top.
+ mSupervisor.beginDeferResume();
final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
// The task-has-been-visible should not affect the decision of making transition ready.
activity.getTask().setHasBeenVisible(true);
activity.detachFromProcess();
- activity.mStartingData = mock(StartingData.class);
+ activity.mStartingData = new SplashScreenStartingData(mWm, 0, 0);
registerTestTransitionPlayer();
final Transition transition = activity.mTransitionController.requestTransitionIfNeeded(
WindowManager.TRANSIT_OPEN, 0 /* flags */, null /* trigger */, mDisplayContent);
@@ -3151,7 +3153,11 @@
assertTrue(activity.mStartingData.mIsDisplayed);
// The transition can be ready by the starting window of a visible-requested activity
// without a running process.
- assertTrue(transition.allReady());
+ if (!transition.allReady()) {
+ // Print unsatisfied conditions.
+ transition.onReadyTimeout();
+ Assert.fail(transition + " must be ready by onStartingWindowDrawn");
+ }
// If other event makes the transition unready, the reentrant of onStartingWindowDrawn
// should not replace the readiness again.
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 62a4711..41f1e23 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -87,6 +87,7 @@
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.times;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
@@ -4782,6 +4783,114 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+ public void testCameraCompatAspectRatioAppliedForFixedOrientationCameraActivities() {
+ // Needed to create camera compat policy in DisplayContent.
+ allowDesktopMode();
+ // Create display that has all stable insets and does not rotate.
+ final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1080, 600)
+ .setSystemDecorations(true).setCanRotate(false).build();
+
+ final float cameraCompatAspectRatio = 4.0f;
+ setupCameraCompatAspectRatio(cameraCompatAspectRatio, display);
+
+ // Create task on test display.
+ final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
+
+ // Create fixed portrait activity.
+ final ActivityRecord fixedOrientationActivity = new ActivityBuilder(mAtm)
+ .setTask(task).setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT).build();
+ final Rect fixedOrientationAppBounds = new Rect(fixedOrientationActivity.getConfiguration()
+ .windowConfiguration.getAppBounds());
+
+ assertEquals(cameraCompatAspectRatio, computeAspectRatio(fixedOrientationAppBounds),
+ DELTA_ASPECT_RATIO_TOLERANCE);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+ public void testCameraCompatAspectRatioForFixedOrientationCameraActivitiesPortraitWindow() {
+ // Needed to create camera compat policy in DisplayContent.
+ allowDesktopMode();
+ // Create portrait display that has all stable insets and does not rotate.
+ final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1080, 1600)
+ .setSystemDecorations(true).setCanRotate(false).build();
+
+ final float cameraCompatAspectRatio = 4.0f;
+ setupCameraCompatAspectRatio(cameraCompatAspectRatio, display);
+
+ // Create task on test display.
+ final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
+
+ // Create fixed portrait activity.
+ final ActivityRecord fixedOrientationActivity = new ActivityBuilder(mAtm)
+ .setTask(task).setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT).build();
+ final Rect fixedOrientationAppBounds = new Rect(fixedOrientationActivity.getConfiguration()
+ .windowConfiguration.getAppBounds());
+
+ assertEquals(cameraCompatAspectRatio, computeAspectRatio(fixedOrientationAppBounds),
+ DELTA_ASPECT_RATIO_TOLERANCE);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+ public void testCameraCompatAspectRatioAppliedInsteadOfDefaultAspectRatio() {
+ // Needed to create camera compat policy in DisplayContent.
+ allowDesktopMode();
+ // Create display that has all stable insets and does not rotate.
+ final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1080, 600)
+ .setSystemDecorations(true).setCanRotate(false).build();
+
+ final float cameraCompatAspectRatio = 5.0f;
+ setupCameraCompatAspectRatio(cameraCompatAspectRatio, display);
+
+ // Create task on test display.
+ final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
+
+ // App's target min aspect ratio - this should not be used, as camera controls aspect ratio.
+ final float targetMinAspectRatio = 4.0f;
+
+ // Create fixed portrait activity with min aspect ratio greater than parent aspect ratio.
+ final ActivityRecord minAspectRatioActivity = new ActivityBuilder(mAtm)
+ .setTask(task).setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
+ .setMinAspectRatio(targetMinAspectRatio).build();
+ final Rect minAspectRatioAppBounds = new Rect(minAspectRatioActivity.getConfiguration()
+ .windowConfiguration.getAppBounds());
+
+ assertEquals(cameraCompatAspectRatio, computeAspectRatio(minAspectRatioAppBounds),
+ DELTA_ASPECT_RATIO_TOLERANCE);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+ public void testCameraCompatAspectRatio_defualtAspectRatioAppliedWhenGreater() {
+ // Needed to create camera compat policy in DisplayContent.
+ allowDesktopMode();
+ // Create display that has all stable insets and does not rotate.
+ final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1080, 600)
+ .setSystemDecorations(true).setCanRotate(false).build();
+
+ final float cameraCompatAspectRatio = 5.0f;
+ setupCameraCompatAspectRatio(cameraCompatAspectRatio, display);
+
+ // Create task on test display.
+ final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
+
+ // App's target min aspect ratio bigger than camera compat aspect ratio - use that instead.
+ final float targetMinAspectRatio = 6.0f;
+
+ // Create fixed portrait activity with min aspect ratio greater than parent aspect ratio.
+ final ActivityRecord minAspectRatioActivity = new ActivityBuilder(mAtm)
+ .setTask(task).setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
+ .setMinAspectRatio(targetMinAspectRatio).build();
+ final Rect minAspectRatioAppBounds = new Rect(minAspectRatioActivity.getConfiguration()
+ .windowConfiguration.getAppBounds());
+
+ assertEquals(targetMinAspectRatio, computeAspectRatio(minAspectRatioAppBounds),
+ DELTA_ASPECT_RATIO_TOLERANCE);
+ }
+
+ @Test
public void testUniversalResizeable() {
mWm.mConstants.mIgnoreActivityOrientationRequest = true;
setUpApp(mDisplayContent);
@@ -4868,6 +4977,25 @@
assertEquals(newDensity, mActivity.getConfiguration().densityDpi);
}
+ /**
+ * {@code canEnterDesktopMode} is called when {@link CameraCompatFreeformPolicy} is created in
+ * {@link AppCompatCameraPolicy}.
+ *
+ * <p>{@link #allowDesktopMode()} needs to be called before {@link DisplayContent} is created.
+ */
+ private void allowDesktopMode() {
+ doReturn(true).when(() -> DesktopModeHelper.canEnterDesktopMode(any()));
+ }
+
+ private void setupCameraCompatAspectRatio(float cameraCompatAspectRatio,
+ @NonNull DisplayContent display) {
+ CameraCompatFreeformPolicy cameraPolicy = display.mAppCompatCameraPolicy
+ .mCameraCompatFreeformPolicy;
+ spyOn(cameraPolicy);
+ doReturn(true).when(cameraPolicy).shouldCameraCompatControlAspectRatio(any());
+ doReturn(cameraCompatAspectRatio).when(cameraPolicy).getCameraCompatAspectRatio(any());
+ }
+
private void setUpAllowThinLetterboxed(boolean thinLetterboxAllowed) {
final AppCompatReachabilityOverrides reachabilityOverrides =
mActivity.mAppCompatController.getAppCompatReachabilityOverrides();
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 7e3d99a..887b798 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -3511,6 +3511,40 @@
}
}
+ /**
+ * Inform whether application supports NTN SMS in satellite mode.
+ *
+ * This method is used by default messaging application to inform framework whether it supports
+ * NTN SMS or not.
+ *
+ * Invoking this API will internally result in triggering
+ * {@link android.telephony.TelephonyCallback.CarrierRoamingNtnModeListener
+ * #onCarrierRoamingNtnAvailableServicesChanged(List)} and
+ * {@link android.telephony.TelephonyCallback.CarrierRoamingNtnModeListener
+ * #onCarrierRoamingNtnEligibleStateChanged(boolean)} callbacks.
+ *
+ * @param ntnSmsSupported {@code true} If application supports NTN SMS, else {@code false}.
+ *
+ * @throws SecurityException if the caller doesn't have required permission.
+ * @throws IllegalStateException if the Telephony process is not currently available.
+ * @hide
+ */
+ @RequiresPermission(allOf = {Manifest.permission.SATELLITE_COMMUNICATION,
+ Manifest.permission.SEND_SMS})
+ public void setNtnSmsSupported(boolean ntnSmsSupported) {
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ telephony.setNtnSmsSupported(ntnSmsSupported);
+ } else {
+ throw new IllegalStateException("telephony service is null.");
+ }
+ } catch (RemoteException ex) {
+ loge("setNtnSmsSupported() RemoteException:" + ex);
+ ex.rethrowAsRuntimeException();
+ }
+ }
+
@Nullable
private static ITelephony getITelephony() {
ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 210200b..a584273 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3497,4 +3497,15 @@
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
void deprovisionSatellite(in List<SatelliteSubscriberInfo> list, in ResultReceiver result);
+
+ /**
+ * Inform whether application supports NTN SMS in satellite mode.
+ *
+ * This method is used by default messaging application to inform framework whether it supports
+ * NTN SMS or not.
+ *
+ * @param ntnSmsSupported {@code true} If application supports NTN SMS, else {@code false}.
+ * @hide
+ */
+ void setNtnSmsSupported(boolean ntnSmsSupported);
}