Merge "[Expressive design] Add ZeroStatePreference" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index cc0354c..bfddf4f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -6822,6 +6822,17 @@
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.ModuleProperties> CREATOR;
}
+ @FlaggedApi("android.media.soundtrigger.manager_api") public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getAudioCapabilities();
+ method @NonNull public byte[] getData();
+ method @NonNull public java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra> getKeyphrases();
+ method public boolean isAllowMultipleTriggers();
+ method public boolean isCaptureRequested();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.RecognitionConfig> CREATOR;
+ }
+
public static class SoundTrigger.RecognitionEvent {
method @Nullable public android.media.AudioFormat getCaptureFormat();
method public int getCaptureSession();
@@ -7772,10 +7783,16 @@
method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void deleteModel(java.util.UUID);
method public int getDetectionServiceOperationsTimeout();
method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.media.soundtrigger.SoundTriggerManager.Model getModel(java.util.UUID);
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getModelState(@NonNull java.util.UUID);
method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModuleProperties getModuleProperties();
method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getParameter(@NonNull java.util.UUID, int);
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public boolean isRecognitionActive(@NonNull java.util.UUID);
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int loadSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.SoundModel);
method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModelParamRange queryParameter(@Nullable java.util.UUID, int);
method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int setParameter(@Nullable java.util.UUID, int, int);
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int startRecognition(@NonNull java.util.UUID, @Nullable android.os.Bundle, @NonNull android.content.ComponentName, @NonNull android.hardware.soundtrigger.SoundTrigger.RecognitionConfig);
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int stopRecognition(@NonNull java.util.UUID);
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int unloadSoundModel(@NonNull java.util.UUID);
method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void updateModel(android.media.soundtrigger.SoundTriggerManager.Model);
}
@@ -13020,6 +13037,8 @@
method public void onSuggestedReplySent(@NonNull String, @NonNull CharSequence, int);
method public final void unsnoozeNotification(@NonNull String);
field public static final String ACTION_NOTIFICATION_ASSISTANT_DETAIL_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_DETAIL_SETTINGS";
+ field @FlaggedApi("android.service.notification.notification_classification") public static final String ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS";
+ field @FlaggedApi("android.service.notification.notification_classification") public static final String EXTRA_NOTIFICATION_KEY = "android.service.notification.extra.NOTIFICATION_KEY";
field public static final String FEEDBACK_RATING = "feedback.rating";
field public static final String SERVICE_INTERFACE = "android.service.notification.NotificationAssistantService";
field public static final int SOURCE_FROM_APP = 0; // 0x0
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 659222d..cc9e836 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1886,17 +1886,9 @@
ctor public SoundTrigger.ModuleProperties(int, @NonNull String, @NonNull String, @NonNull String, int, @NonNull String, int, int, int, int, boolean, int, boolean, int, boolean, int);
}
- public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable {
+ @FlaggedApi("android.media.soundtrigger.manager_api") public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable {
ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[], int);
ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[]);
- method public int describeContents();
- method public void writeToParcel(@NonNull android.os.Parcel, int);
- field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.RecognitionConfig> CREATOR;
- field public final boolean allowMultipleTriggers;
- field public final int audioCapabilities;
- field public final boolean captureRequested;
- field @NonNull public final byte[] data;
- field @NonNull public final android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[] keyphrases;
}
public static class SoundTrigger.RecognitionEvent {
@@ -2217,7 +2209,7 @@
public class SoundTriggerInstrumentation.RecognitionSession {
method public void clearRecognitionCallback();
method public int getAudioSession();
- method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig getRecognitionConfig();
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig getRecognitionConfig();
method public void setRecognitionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.soundtrigger.SoundTriggerInstrumentation.RecognitionCallback);
method public void triggerAbortRecognition();
method public void triggerRecognitionEvent(@NonNull byte[], @Nullable java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>);
@@ -2228,7 +2220,6 @@
method @NonNull public android.media.soundtrigger.SoundTriggerManager createManagerForModule(@NonNull android.hardware.soundtrigger.SoundTrigger.ModuleProperties);
method @NonNull public android.media.soundtrigger.SoundTriggerManager createManagerForTestModule();
method @NonNull public static java.util.List<android.hardware.soundtrigger.SoundTrigger.ModuleProperties> listModuleProperties();
- method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int loadSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.SoundModel);
}
public static class SoundTriggerManager.Model {
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index fad289c..edcdb6c 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -1047,7 +1047,7 @@
private static String startComponentToString(@StartComponent int startComponent) {
return switch (startComponent) {
case START_COMPONENT_ACTIVITY -> "ACTIVITY";
- case START_COMPONENT_BROADCAST -> "SERVICE";
+ case START_COMPONENT_BROADCAST -> "BROADCAST";
case START_COMPONENT_CONTENT_PROVIDER -> "CONTENT PROVIDER";
case START_COMPONENT_SERVICE -> "SERVICE";
case START_COMPONENT_OTHER -> "OTHER";
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 237780f..8a54b5d 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -258,7 +258,7 @@
@EnforcePermission(allOf={"INTERACT_ACROSS_USERS", "ACCESS_NOTIFICATIONS"})
void unregisterCallNotificationEventListener(String packageName, in UserHandle userHandle, in ICallNotificationEventCallback listener);
- void setCanBePromoted(String pkg, int uid, boolean promote);
+ void setCanBePromoted(String pkg, int uid, boolean promote, boolean fromUser);
boolean appCanBePromoted(String pkg, int uid);
boolean canBePromoted(String pkg);
}
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 06f21b8..c7b84ae 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -976,7 +976,7 @@
public void setCanPostPromotedNotifications(@NonNull String pkg, int uid, boolean allowed) {
INotificationManager service = getService();
try {
- service.setCanBePromoted(pkg, uid, allowed);
+ service.setCanBePromoted(pkg, uid, allowed, true);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 108b5f4..b139017 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -31,6 +31,16 @@
}
flag {
+ name: "modes_ui_empty_shade"
+ namespace: "systemui"
+ description: "Shows mode that is currently blocking notifications in the empty shade; dependent on flags modes_api and modes_ui"
+ bug: "366003631"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "modes_ui_test"
namespace: "systemui"
description: "Guards new CTS tests for Modes; dependent on flags modes_api and modes_ui"
diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl
index 8d25cad..4598421 100644
--- a/core/java/android/app/supervision/ISupervisionManager.aidl
+++ b/core/java/android/app/supervision/ISupervisionManager.aidl
@@ -21,5 +21,5 @@
* {@hide}
*/
interface ISupervisionManager {
- boolean isSupervisionEnabled();
+ boolean isSupervisionEnabledForUser(int userId);
}
diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java
index 8611a92..aee1cd9 100644
--- a/core/java/android/app/supervision/SupervisionManager.java
+++ b/core/java/android/app/supervision/SupervisionManager.java
@@ -17,6 +17,7 @@
package android.app.supervision;
import android.annotation.SystemService;
+import android.annotation.UserHandleAware;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.os.RemoteException;
@@ -45,13 +46,12 @@
*
* @hide
*/
+ @UserHandleAware
public boolean isSupervisionEnabled() {
try {
- return mService.isSupervisionEnabled();
+ return mService.isSupervisionEnabledForUser(mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
-
-
}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 044178c..031380d 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -20,7 +20,6 @@
import static android.content.ContentProvider.maybeAddUserId;
import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
import static android.security.Flags.FLAG_FRP_ENFORCEMENT;
-import static android.security.Flags.preventIntentRedirect;
import android.Manifest;
import android.accessibilityservice.AccessibilityService;
@@ -7688,17 +7687,9 @@
/** @hide */
public static final int LOCAL_FLAG_FROM_SYSTEM = 1 << 5;
- /**
- * This flag indicates the creator token of this intent has been verified.
- *
- * @hide
- */
- public static final int LOCAL_FLAG_CREATOR_TOKEN_VERIFIED = 1 << 6;
-
/** @hide */
@IntDef(flag = true, prefix = { "EXTENDED_FLAG_" }, value = {
EXTENDED_FLAG_FILTER_MISMATCH,
- EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ExtendedFlags {}
@@ -7712,13 +7703,6 @@
@TestApi
public static final int EXTENDED_FLAG_FILTER_MISMATCH = 1 << 0;
- /**
- * This flag indicates the creator token of this intent is either missing or invalid.
- *
- * @hide
- */
- public static final int EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN = 1 << 1;
-
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// toUri() and parseUri() options.
@@ -7886,7 +7870,6 @@
this.mPackage = o.mPackage;
this.mComponent = o.mComponent;
this.mOriginalIntent = o.mOriginalIntent;
- this.mCreatorTokenInfo = o.mCreatorTokenInfo;
if (o.mCategories != null) {
this.mCategories = new ArraySet<>(o.mCategories);
@@ -12193,60 +12176,6 @@
return (mExtras != null) ? mExtras.describeContents() : 0;
}
- private static class CreatorTokenInfo {
- // Stores a creator token for an intent embedded as an extra intent in a top level intent,
- private IBinder mCreatorToken;
- // Stores all extra keys whose values are intents for a top level intent.
- private ArraySet<String> mExtraIntentKeys;
- }
-
- private @Nullable CreatorTokenInfo mCreatorTokenInfo;
-
- /** @hide */
- public void removeCreatorTokenInfo() {
- mCreatorTokenInfo = null;
- }
-
- /** @hide */
- public @Nullable IBinder getCreatorToken() {
- return mCreatorTokenInfo == null ? null : mCreatorTokenInfo.mCreatorToken;
- }
-
- /** @hide */
- public Set<String> getExtraIntentKeys() {
- return mCreatorTokenInfo == null ? null : mCreatorTokenInfo.mExtraIntentKeys;
- }
-
- /** @hide */
- public void setCreatorToken(@NonNull IBinder creatorToken) {
- if (mCreatorTokenInfo == null) {
- mCreatorTokenInfo = new CreatorTokenInfo();
- }
- mCreatorTokenInfo.mCreatorToken = creatorToken;
- }
-
- /**
- * Collects keys in the extra bundle whose value are intents.
- * @hide
- */
- public void collectExtraIntentKeys() {
- if (!preventIntentRedirect()) return;
-
- if (mExtras != null && !mExtras.isParcelled() && !mExtras.isEmpty()) {
- for (String key : mExtras.keySet()) {
- if (mExtras.get(key) instanceof Intent) {
- if (mCreatorTokenInfo == null) {
- mCreatorTokenInfo = new CreatorTokenInfo();
- }
- if (mCreatorTokenInfo.mExtraIntentKeys == null) {
- mCreatorTokenInfo.mExtraIntentKeys = new ArraySet<>();
- }
- mCreatorTokenInfo.mExtraIntentKeys.add(key);
- }
- }
- }
- }
-
public void writeToParcel(Parcel out, int flags) {
out.writeString8(mAction);
Uri.writeToParcel(out, mData);
@@ -12296,16 +12225,6 @@
} else {
out.writeInt(0);
}
-
- if (preventIntentRedirect()) {
- if (mCreatorTokenInfo == null) {
- out.writeInt(0);
- } else {
- out.writeInt(1);
- out.writeStrongBinder(mCreatorTokenInfo.mCreatorToken);
- out.writeArraySet(mCreatorTokenInfo.mExtraIntentKeys);
- }
- }
}
public static final @android.annotation.NonNull Parcelable.Creator<Intent> CREATOR
@@ -12363,14 +12282,6 @@
if (in.readInt() != 0) {
mOriginalIntent = new Intent(in);
}
-
- if (preventIntentRedirect()) {
- if (in.readInt() != 0) {
- mCreatorTokenInfo = new CreatorTokenInfo();
- mCreatorTokenInfo.mCreatorToken = in.readStrongBinder();
- mCreatorTokenInfo.mExtraIntentKeys = (ArraySet<String>) in.readArraySet(null);
- }
- }
}
/**
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 139ff65..60b409a 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -293,4 +293,12 @@
description: "Feature flag to provide the new methods within launcher apps class to get packages."
bug: "363324203"
is_fixed_read_only: true
+}
+
+flag {
+ name: "remove_cross_user_permission_hack"
+ namespace: "package_manager_service"
+ description: "Feature flag to remove hack code of using PackageManager.MATCH_ANY_USER flag without cross user permission."
+ bug: "332664521"
+ is_fixed_read_only: true
}
\ No newline at end of file
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index bdbec55..5ee61bc 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -48,49 +48,57 @@
public static final int KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL = 8;
public static final int KEY_GESTURE_TYPE_TOGGLE_TASKBAR = 9;
public static final int KEY_GESTURE_TYPE_TAKE_SCREENSHOT = 10;
- public static final int KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER = 11;
- public static final int KEY_GESTURE_TYPE_BRIGHTNESS_UP = 12;
- public static final int KEY_GESTURE_TYPE_BRIGHTNESS_DOWN = 13;
- public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP = 14;
- public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN = 15;
- public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE = 16;
- public static final int KEY_GESTURE_TYPE_VOLUME_UP = 17;
- public static final int KEY_GESTURE_TYPE_VOLUME_DOWN = 18;
- public static final int KEY_GESTURE_TYPE_VOLUME_MUTE = 19;
- public static final int KEY_GESTURE_TYPE_ALL_APPS = 20;
- public static final int KEY_GESTURE_TYPE_LAUNCH_SEARCH = 21;
- public static final int KEY_GESTURE_TYPE_LANGUAGE_SWITCH = 22;
- public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS = 23;
- public static final int KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK = 24;
- public static final int KEY_GESTURE_TYPE_SYSTEM_MUTE = 25;
- public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT = 26;
- public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT = 27;
- public static final int KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT = 28;
- public static final int KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT = 29;
- public static final int KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT = 30;
- public static final int KEY_GESTURE_TYPE_LOCK_SCREEN = 31;
- public static final int KEY_GESTURE_TYPE_OPEN_NOTES = 32;
- public static final int KEY_GESTURE_TYPE_TOGGLE_POWER = 33;
- public static final int KEY_GESTURE_TYPE_SYSTEM_NAVIGATION = 34;
- public static final int KEY_GESTURE_TYPE_SLEEP = 35;
- public static final int KEY_GESTURE_TYPE_WAKEUP = 36;
- public static final int KEY_GESTURE_TYPE_MEDIA_KEY = 37;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER = 38;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL = 39;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS = 40;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR = 41;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR = 42;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MUSIC = 43;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS = 44;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING = 45;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_GALLERY = 46;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FILES = 47;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_WEATHER = 48;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FITNESS = 49;
- public static final int KEY_GESTURE_TYPE_LAUNCH_APPLICATION_BY_PACKAGE_NAME = 50;
- public static final int KEY_GESTURE_TYPE_DESKTOP_MODE = 51;
- public static final int KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION = 52;
- public static final int KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER = 53;
+ public static final int KEY_GESTURE_TYPE_SCREENSHOT_CHORD = 11;
+ public static final int KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER = 12;
+ public static final int KEY_GESTURE_TYPE_BRIGHTNESS_UP = 13;
+ public static final int KEY_GESTURE_TYPE_BRIGHTNESS_DOWN = 14;
+ public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP = 15;
+ public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN = 16;
+ public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE = 17;
+ public static final int KEY_GESTURE_TYPE_VOLUME_UP = 18;
+ public static final int KEY_GESTURE_TYPE_VOLUME_DOWN = 19;
+ public static final int KEY_GESTURE_TYPE_VOLUME_MUTE = 20;
+ public static final int KEY_GESTURE_TYPE_ALL_APPS = 21;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_SEARCH = 22;
+ public static final int KEY_GESTURE_TYPE_LANGUAGE_SWITCH = 23;
+ public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS = 24;
+ public static final int KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK = 25;
+ public static final int KEY_GESTURE_TYPE_SYSTEM_MUTE = 26;
+ public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT = 27;
+ public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT = 28;
+ public static final int KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT = 29;
+ public static final int KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT = 30;
+ public static final int KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT = 31;
+ public static final int KEY_GESTURE_TYPE_LOCK_SCREEN = 32;
+ public static final int KEY_GESTURE_TYPE_OPEN_NOTES = 33;
+ public static final int KEY_GESTURE_TYPE_TOGGLE_POWER = 34;
+ public static final int KEY_GESTURE_TYPE_SYSTEM_NAVIGATION = 35;
+ public static final int KEY_GESTURE_TYPE_SLEEP = 36;
+ public static final int KEY_GESTURE_TYPE_WAKEUP = 37;
+ public static final int KEY_GESTURE_TYPE_MEDIA_KEY = 38;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER = 39;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL = 40;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS = 41;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR = 42;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR = 43;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MUSIC = 44;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS = 45;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING = 46;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_GALLERY = 47;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FILES = 48;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_WEATHER = 49;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FITNESS = 50;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_APPLICATION_BY_PACKAGE_NAME = 51;
+ public static final int KEY_GESTURE_TYPE_DESKTOP_MODE = 52;
+ public static final int KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION = 53;
+ public static final int KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER = 54;
+ public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD = 55;
+ public static final int KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD = 56;
+ public static final int KEY_GESTURE_TYPE_GLOBAL_ACTIONS = 57;
+ public static final int KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58;
+ public static final int KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT = 59;
+ public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT = 60;
+ public static final int KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS = 61;
public static final int FLAG_CANCELLED = 1;
@@ -116,6 +124,7 @@
KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
KEY_GESTURE_TYPE_TOGGLE_TASKBAR,
KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
+ KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
KEY_GESTURE_TYPE_BRIGHTNESS_UP,
KEY_GESTURE_TYPE_BRIGHTNESS_DOWN,
@@ -158,7 +167,15 @@
KEY_GESTURE_TYPE_LAUNCH_APPLICATION_BY_PACKAGE_NAME,
KEY_GESTURE_TYPE_DESKTOP_MODE,
KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
- KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER
+ KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER,
+ KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
+ KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD,
+ KEY_GESTURE_TYPE_GLOBAL_ACTIONS,
+ KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
+ KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
+ KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
+
})
@Retention(RetentionPolicy.SOURCE)
public @interface KeyGestureType {
@@ -360,6 +377,7 @@
case KEY_GESTURE_TYPE_TOGGLE_TASKBAR:
return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_TASKBAR;
case KEY_GESTURE_TYPE_TAKE_SCREENSHOT:
+ case KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TAKE_SCREENSHOT;
case KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_SHORTCUT_HELPER;
@@ -472,6 +490,8 @@
return "KEY_GESTURE_TYPE_TOGGLE_TASKBAR";
case KEY_GESTURE_TYPE_TAKE_SCREENSHOT:
return "KEY_GESTURE_TYPE_TAKE_SCREENSHOT";
+ case KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
+ return "KEY_GESTURE_TYPE_SCREENSHOT_CHORD";
case KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
return "KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER";
case KEY_GESTURE_TYPE_BRIGHTNESS_UP:
@@ -558,6 +578,20 @@
return "KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION";
case KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER:
return "KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER";
+ case KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+ return "KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD";
+ case KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
+ return "KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD";
+ case KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
+ return "KEY_GESTURE_TYPE_GLOBAL_ACTIONS";
+ case KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
+ return "KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD";
+ case KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
+ return "KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT";
+ case KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
+ return "KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT";
+ case KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
+ return "KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS";
default:
return Integer.toHexString(value);
}
diff --git a/core/java/android/hardware/soundtrigger/ConversionUtil.java b/core/java/android/hardware/soundtrigger/ConversionUtil.java
index 5c07fa4..22ae676 100644
--- a/core/java/android/hardware/soundtrigger/ConversionUtil.java
+++ b/core/java/android/hardware/soundtrigger/ConversionUtil.java
@@ -155,16 +155,16 @@
public static RecognitionConfig api2aidlRecognitionConfig(
SoundTrigger.RecognitionConfig apiConfig) {
RecognitionConfig aidlConfig = new RecognitionConfig();
- aidlConfig.captureRequested = apiConfig.captureRequested;
- // apiConfig.allowMultipleTriggers is ignored by the lower layers.
+ aidlConfig.captureRequested = apiConfig.isCaptureRequested();
+ // apiConfig.isAllowMultipleTriggers() is ignored by the lower layers.
aidlConfig.phraseRecognitionExtras =
- new PhraseRecognitionExtra[apiConfig.keyphrases.length];
- for (int i = 0; i < apiConfig.keyphrases.length; ++i) {
+ new PhraseRecognitionExtra[apiConfig.getKeyphrases().size()];
+ for (int i = 0; i < apiConfig.getKeyphrases().size(); ++i) {
aidlConfig.phraseRecognitionExtras[i] = api2aidlPhraseRecognitionExtra(
- apiConfig.keyphrases[i]);
+ apiConfig.getKeyphrases().get(i));
}
- aidlConfig.data = Arrays.copyOf(apiConfig.data, apiConfig.data.length);
- aidlConfig.audioCapabilities = api2aidlAudioCapabilities(apiConfig.audioCapabilities);
+ aidlConfig.data = Arrays.copyOf(apiConfig.getData(), apiConfig.getData().length);
+ aidlConfig.audioCapabilities = api2aidlAudioCapabilities(apiConfig.getAudioCapabilities());
return aidlConfig;
}
diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java
index 9f3e3ad..79cbd19 100644
--- a/core/java/android/hardware/soundtrigger/SoundTrigger.java
+++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java
@@ -63,6 +63,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
@@ -1513,50 +1514,56 @@
* A RecognitionConfig is provided to
* {@link SoundTriggerModule#startRecognition(int, RecognitionConfig)} to configure the
* recognition request.
- *
- * @hide
*/
- @TestApi
+ @FlaggedApi(android.media.soundtrigger.Flags.FLAG_MANAGER_API)
public static final class RecognitionConfig implements Parcelable {
- /** True if the DSP should capture the trigger sound and make it available for further
- * capture. */
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public final boolean captureRequested;
- /**
- * True if the service should restart listening after the DSP triggers.
- * Note: This config flag is currently used at the service layer rather than by the DSP.
- */
- public final boolean allowMultipleTriggers;
- /** List of all keyphrases in the sound model for which recognition should be performed with
- * options for each keyphrase. */
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- @NonNull
- @SuppressLint("ArrayReturn")
- public final KeyphraseRecognitionExtra keyphrases[];
- /** Opaque data for use by system applications who know about voice engine internals,
- * typically during enrollment. */
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- @NonNull
- public final byte[] data;
-
- /**
- * Bit field encoding of the AudioCapabilities
- * supported by the firmware.
- */
+ private final boolean mCaptureRequested;
+ private final boolean mAllowMultipleTriggers;
+ private final KeyphraseRecognitionExtra mKeyphrases[];
+ private final byte[] mData;
@ModuleProperties.AudioCapabilities
- public final int audioCapabilities;
+ private final int mAudioCapabilities;
+ /**
+ * Constructor for {@link RecognitionConfig} with {@code audioCapabilities} describes a
+ * config that can be used by
+ * {@link SoundTriggerModule#startRecognition(int, RecognitionConfig)}
+ *
+ * @param captureRequested Whether the DSP should capture the trigger sound.
+ * @param allowMultipleTriggers Whether the service should restart listening after the DSP
+ * triggers.
+ * @param keyphrases List of keyphrases in the sound model.
+ * @param data Opaque data for use by system applications who know about voice engine
+ * internals, typically during enrollment.
+ * @param audioCapabilities Bit field encoding of the AudioCapabilities.
+ *
+ * @hide
+ */
+ @TestApi
public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers,
@SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases,
@Nullable byte[] data, int audioCapabilities) {
- this.captureRequested = captureRequested;
- this.allowMultipleTriggers = allowMultipleTriggers;
- this.keyphrases = keyphrases != null ? keyphrases : new KeyphraseRecognitionExtra[0];
- this.data = data != null ? data : new byte[0];
- this.audioCapabilities = audioCapabilities;
+ this.mCaptureRequested = captureRequested;
+ this.mAllowMultipleTriggers = allowMultipleTriggers;
+ this.mKeyphrases = keyphrases != null ? keyphrases : new KeyphraseRecognitionExtra[0];
+ this.mData = data != null ? data : new byte[0];
+ this.mAudioCapabilities = audioCapabilities;
}
+ /**
+ * Constructor for {@link RecognitionConfig} without audioCapabilities. The
+ * audioCapabilities is set to 0.
+ *
+ * @param captureRequested Whether the DSP should capture the trigger sound.
+ * @param allowMultipleTriggers Whether the service should restart listening after the DSP
+ * triggers.
+ * @param keyphrases List of keyphrases in the sound model.
+ * @param data Opaque data for use by system applications.
+ *
+ * @hide
+ */
@UnsupportedAppUsage
+ @TestApi
public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers,
@SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases,
@Nullable byte[] data) {
@@ -1574,9 +1581,52 @@
}
};
+ /**
+ * Returns whether the DSP should capture the trigger sound and make it available for
+ * further capture.
+ */
+ public boolean isCaptureRequested() {
+ return mCaptureRequested;
+ }
+
+ /**
+ * Returns whether the service should restart listening after the DSP triggers.
+ *
+ * <p><b>Note:</b> This config flag is currently used at the service layer rather than by
+ * the DSP.
+ */
+ public boolean isAllowMultipleTriggers() {
+ return mAllowMultipleTriggers;
+ }
+
+ /**
+ * Gets all keyphrases in the sound model for which recognition should be performed with
+ * options for each keyphrase.
+ */
+ @NonNull
+ public List<KeyphraseRecognitionExtra> getKeyphrases() {
+ return Arrays.asList(mKeyphrases);
+ }
+
+ /**
+ * Opaque data.
+ *
+ * <p>For use by system applications who knows about voice engine internals, typically
+ * during enrollment.
+ */
+ @NonNull
+ public byte[] getData() {
+ return mData;
+ }
+
+ /** Bit field encoding of the AudioCapabilities supported by the firmware. */
+ public int getAudioCapabilities() {
+ return mAudioCapabilities;
+ }
+
private static RecognitionConfig fromParcel(Parcel in) {
- boolean captureRequested = in.readByte() == 1;
- boolean allowMultipleTriggers = in.readByte() == 1;
+ boolean captureRequested = in.readBoolean();
+ boolean allowMultipleTriggers = in.readBoolean();
KeyphraseRecognitionExtra[] keyphrases =
in.createTypedArray(KeyphraseRecognitionExtra.CREATOR);
byte[] data = in.readBlob();
@@ -1587,11 +1637,11 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
- dest.writeByte((byte) (captureRequested ? 1 : 0));
- dest.writeByte((byte) (allowMultipleTriggers ? 1 : 0));
- dest.writeTypedArray(keyphrases, flags);
- dest.writeBlob(data);
- dest.writeInt(audioCapabilities);
+ dest.writeBoolean(mCaptureRequested);
+ dest.writeBoolean(mAllowMultipleTriggers);
+ dest.writeTypedArray(mKeyphrases, flags);
+ dest.writeBlob(mData);
+ dest.writeInt(mAudioCapabilities);
}
@Override
@@ -1601,10 +1651,10 @@
@Override
public String toString() {
- return "RecognitionConfig [captureRequested=" + captureRequested
- + ", allowMultipleTriggers=" + allowMultipleTriggers + ", keyphrases="
- + Arrays.toString(keyphrases) + ", data=" + Arrays.toString(data)
- + ", audioCapabilities=" + Integer.toHexString(audioCapabilities) + "]";
+ return "RecognitionConfig [captureRequested=" + mCaptureRequested
+ + ", allowMultipleTriggers=" + mAllowMultipleTriggers + ", keyphrases="
+ + Arrays.toString(mKeyphrases) + ", data=" + Arrays.toString(mData)
+ + ", audioCapabilities=" + Integer.toHexString(mAudioCapabilities) + "]";
}
@Override
@@ -1616,19 +1666,19 @@
if (!(obj instanceof RecognitionConfig))
return false;
RecognitionConfig other = (RecognitionConfig) obj;
- if (captureRequested != other.captureRequested) {
+ if (mCaptureRequested != other.mCaptureRequested) {
return false;
}
- if (allowMultipleTriggers != other.allowMultipleTriggers) {
+ if (mAllowMultipleTriggers != other.mAllowMultipleTriggers) {
return false;
}
- if (!Arrays.equals(keyphrases, other.keyphrases)) {
+ if (!Arrays.equals(mKeyphrases, other.mKeyphrases)) {
return false;
}
- if (!Arrays.equals(data, other.data)) {
+ if (!Arrays.equals(mData, other.mData)) {
return false;
}
- if (audioCapabilities != other.audioCapabilities) {
+ if (mAudioCapabilities != other.mAudioCapabilities) {
return false;
}
return true;
@@ -1638,11 +1688,11 @@
public final int hashCode() {
final int prime = 31;
int result = 1;
- result = prime * result + (captureRequested ? 1 : 0);
- result = prime * result + (allowMultipleTriggers ? 1 : 0);
- result = prime * result + Arrays.hashCode(keyphrases);
- result = prime * result + Arrays.hashCode(data);
- result = prime * result + audioCapabilities;
+ result = prime * result + (mCaptureRequested ? 1 : 0);
+ result = prime * result + (mAllowMultipleTriggers ? 1 : 0);
+ result = prime * result + Arrays.hashCode(mKeyphrases);
+ result = prime * result + Arrays.hashCode(mData);
+ result = prime * result + mAudioCapabilities;
return result;
}
}
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 91cdf8d..1c9be6f 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -76,11 +76,15 @@
* PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION} before querying the service. If the feature is
* absent, {@link Context#getSystemService} may return null.
*/
-@SystemService(Context.VCN_MANAGEMENT_SERVICE)
+@SystemService(VcnManager.VCN_MANAGEMENT_SERVICE_STRING)
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
public class VcnManager {
@NonNull private static final String TAG = VcnManager.class.getSimpleName();
+ // TODO: b/366598445: Expose and use Context.VCN_MANAGEMENT_SERVICE
+ /** @hide */
+ public static final String VCN_MANAGEMENT_SERVICE_STRING = "vcn_management";
+
/**
* Key for WiFi entry RSSI thresholds
*
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 1fef602..a698b9d 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -788,6 +788,12 @@
/** Parses an XML representation of BatteryUsageStats */
public static BatteryUsageStats createFromXml(TypedXmlPullParser parser)
throws XmlPullParserException, IOException {
+ return createBuilderFromXml(parser).build();
+ }
+
+ /** Parses an XML representation of BatteryUsageStats */
+ public static BatteryUsageStats.Builder createBuilderFromXml(TypedXmlPullParser parser)
+ throws XmlPullParserException, IOException {
Builder builder = null;
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
@@ -862,7 +868,7 @@
eventType = parser.next();
}
- return builder.build();
+ return builder;
}
@Override
@@ -978,10 +984,21 @@
*/
@NonNull
public BatteryUsageStats build() {
+ if (mBatteryConsumersCursorWindow == null) {
+ throw new IllegalStateException("Builder has been discarded");
+ }
return new BatteryUsageStats(this);
}
/**
+ * Close this builder without actually calling ".build()". Do not attempt
+ * to continue using the builder after this call.
+ */
+ public void discard() {
+ mBatteryConsumersCursorWindow.close();
+ }
+
+ /**
* Sets the battery capacity in milli-amp-hours.
*/
public Builder setBatteryCapacity(double batteryCapacityMah) {
diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java
index a12606b..b533225 100644
--- a/core/java/android/os/BatteryUsageStatsQuery.java
+++ b/core/java/android/os/BatteryUsageStatsQuery.java
@@ -77,6 +77,8 @@
public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE = 0x0040;
+ public static final int FLAG_BATTERY_USAGE_STATS_ACCUMULATED = 0x0080;
+
private static final long DEFAULT_MAX_STATS_AGE_MS = 5 * 60 * 1000;
private final int mFlags;
@@ -328,6 +330,15 @@
}
/**
+ * Requests the full continuously accumulated battery usage stats: across reboots
+ * and most battery stats resets.
+ */
+ public Builder accumulated() {
+ mFlags |= FLAG_BATTERY_USAGE_STATS_ACCUMULATED;
+ return this;
+ }
+
+ /**
* Requests to aggregate stored snapshots between the two supplied timestamps
* @param fromTimestamp Exclusive starting timestamp, as per System.currentTimeMillis()
* @param toTimestamp Inclusive ending timestamp, as per System.currentTimeMillis()
diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java
index cfbf528..a5697fb 100644
--- a/core/java/android/os/SystemVibratorManager.java
+++ b/core/java/android/os/SystemVibratorManager.java
@@ -16,6 +16,8 @@
package android.os;
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -138,14 +140,14 @@
Log.w(TAG, "Failed to vibrate; no vibrator manager service.");
return;
}
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason=" + reason);
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "vibrate");
try {
mService.vibrate(uid, mContext.getDeviceId(), opPkg, effect, attributes, reason,
mToken);
} catch (RemoteException e) {
Log.w(TAG, "Failed to vibrate.", e);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -155,14 +157,14 @@
Log.w(TAG, "Failed to perform haptic feedback; no vibrator manager service.");
return;
}
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedback, reason=" + reason);
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedback");
try {
mService.performHapticFeedback(mUid, mContext.getDeviceId(), mPackageName, constant,
reason, flags, privFlags);
} catch (RemoteException e) {
Log.w(TAG, "Failed to perform haptic feedback.", e);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -174,15 +176,14 @@
+ " no vibrator manager service.");
return;
}
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR,
- "performHapticFeedbackForInputDevice, reason=" + reason);
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
try {
mService.performHapticFeedbackForInputDevice(mUid, mContext.getDeviceId(), mPackageName,
constant, inputDeviceId, inputSource, reason, flags, privFlags);
} catch (RemoteException e) {
Log.w(TAG, "Failed to perform haptic feedback for input device.", e);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 45e9def..56d3669 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -52,11 +52,3 @@
description: "Opt the system into enforcement of BAL"
bug: "339403750"
}
-
-flag {
- name: "prevent_intent_redirect"
- namespace: "responsible_apis"
- description: "Prevent intent redirect attacks"
- bug: "361143368"
- is_fixed_read_only: true
-}
\ No newline at end of file
diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java
index 88da8eb..48d7cf7 100644
--- a/core/java/android/service/notification/NotificationAssistantService.java
+++ b/core/java/android/service/notification/NotificationAssistantService.java
@@ -18,6 +18,7 @@
import static java.lang.annotation.RetentionPolicy.SOURCE;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -90,10 +91,11 @@
= "android.service.notification.NotificationAssistantService";
/**
- * Activity Action: Show notification assistant detail setting page in NAS app.
+ * Activity Action: Show notification assistant detail setting page in the NAS app.
* <p>
- * In some cases, a matching Activity may not exist, so ensure you
- * safeguard against this.
+ * To be implemented by the NAS to offer users additional customization of intelligence
+ * features. If the action is not implemented, the OS will not provide a link to it in the
+ * Settings UI.
* <p>
* Input: Nothing.
* <p>
@@ -103,6 +105,30 @@
public static final String ACTION_NOTIFICATION_ASSISTANT_DETAIL_SETTINGS =
"android.service.notification.action.NOTIFICATION_ASSISTANT_DETAIL_SETTINGS";
+ /**
+ * Activity Action: Open notification assistant feedback page in the NAS app.
+ * <p>
+ * If the NAS does not implement this page, the OS will not show any feedback calls to action in
+ * the UI.
+ * <p>
+ * Input: {@link #EXTRA_NOTIFICATION_KEY}, the {@link StatusBarNotification#getKey()} of the
+ * notification the user wants to file feedback for.
+ * <p>
+ * Output: Nothing.
+ */
+ @FlaggedApi(Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS =
+ "android.service.notification.action.NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS";
+
+ /**
+ * A string extra containing the key of the notification that the user triggered feedback for.
+ *
+ * Extra for {@link #ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS}.
+ */
+ @FlaggedApi(Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public static final String EXTRA_NOTIFICATION_KEY
+ = "android.service.notification.extra.NOTIFICATION_KEY";
/**
* Data type: int, the feedback rating score provided by user. The score can be any integer
diff --git a/core/java/android/view/WindowManagerPolicyConstants.java b/core/java/android/view/WindowManagerPolicyConstants.java
index c88048c..1f341ca 100644
--- a/core/java/android/view/WindowManagerPolicyConstants.java
+++ b/core/java/android/view/WindowManagerPolicyConstants.java
@@ -17,6 +17,7 @@
package android.view;
import static android.os.IInputConstants.POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY;
+import static android.os.IInputConstants.POLICY_FLAG_KEY_GESTURE_TRIGGERED;
import android.annotation.IntDef;
import android.os.PowerManager;
@@ -35,6 +36,7 @@
int FLAG_VIRTUAL = 0x00000002;
int FLAG_INJECTED_FROM_ACCESSIBILITY = POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY;
+ int FLAG_KEY_GESTURE_TRIGGERED = POLICY_FLAG_KEY_GESTURE_TRIGGERED;
int FLAG_INJECTED = 0x01000000;
int FLAG_TRUSTED = 0x02000000;
int FLAG_FILTERED = 0x04000000;
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index cc5e583..fbc30ed 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -287,6 +287,13 @@
}
flag {
+ name: "enable_fully_immersive_in_desktop"
+ namespace: "lse_desktop_experience"
+ description: "Enabled the fully immersive experience from desktop"
+ bug: "359523924"
+}
+
+flag {
name: "enable_display_focus_in_shell_transitions"
namespace: "lse_desktop_experience"
description: "Creates a shell transition when display focus switches."
diff --git a/core/java/com/android/internal/util/ScreenshotRequest.java b/core/java/com/android/internal/util/ScreenshotRequest.java
index c8b7def..702e5e2 100644
--- a/core/java/com/android/internal/util/ScreenshotRequest.java
+++ b/core/java/com/android/internal/util/ScreenshotRequest.java
@@ -33,6 +33,7 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
+import android.view.Display;
import android.view.WindowManager;
import java.util.Objects;
@@ -53,11 +54,12 @@
private final Bitmap mBitmap;
private final Rect mBoundsInScreen;
private final Insets mInsets;
+ private final int mDisplayId;
private ScreenshotRequest(
@WindowManager.ScreenshotType int type, @WindowManager.ScreenshotSource int source,
ComponentName topComponent, int taskId, int userId,
- Bitmap bitmap, Rect boundsInScreen, Insets insets) {
+ Bitmap bitmap, Rect boundsInScreen, Insets insets, int displayId) {
mType = type;
mSource = source;
mTopComponent = topComponent;
@@ -66,6 +68,7 @@
mBitmap = bitmap;
mBoundsInScreen = boundsInScreen;
mInsets = insets;
+ mDisplayId = displayId;
}
ScreenshotRequest(Parcel in) {
@@ -77,6 +80,7 @@
mBitmap = HardwareBitmapBundler.bundleToHardwareBitmap(in.readTypedObject(Bundle.CREATOR));
mBoundsInScreen = in.readTypedObject(Rect.CREATOR);
mInsets = in.readTypedObject(Insets.CREATOR);
+ mDisplayId = in.readInt();
}
@WindowManager.ScreenshotType
@@ -113,6 +117,10 @@
return mTopComponent;
}
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -128,6 +136,7 @@
dest.writeTypedObject(HardwareBitmapBundler.hardwareBitmapToBundle(mBitmap), 0);
dest.writeTypedObject(mBoundsInScreen, 0);
dest.writeTypedObject(mInsets, 0);
+ dest.writeInt(mDisplayId);
}
@NonNull
@@ -161,6 +170,7 @@
private int mTaskId = INVALID_TASK_ID;
private int mUserId = USER_NULL;
private ComponentName mTopComponent;
+ private int mDisplayId = Display.INVALID_DISPLAY;
/**
* Begin building a ScreenshotRequest.
@@ -193,7 +203,7 @@
}
return new ScreenshotRequest(mType, mSource, mTopComponent, mTaskId, mUserId, mBitmap,
- mBoundsInScreen, mInsets);
+ mBoundsInScreen, mInsets, mDisplayId);
}
/**
@@ -255,6 +265,16 @@
mInsets = insets;
return this;
}
+
+ /**
+ * Set the display ID for this request.
+ *
+ * @param displayId see {@link Display}
+ */
+ public Builder setDisplayId(int displayId) {
+ mDisplayId = displayId;
+ return this;
+ }
}
/**
diff --git a/core/jni/android_util_XmlBlock.cpp b/core/jni/android_util_XmlBlock.cpp
index 5a444bb..c364451 100644
--- a/core/jni/android_util_XmlBlock.cpp
+++ b/core/jni/android_util_XmlBlock.cpp
@@ -83,7 +83,7 @@
return 0;
}
- ResXMLParser* st = new ResXMLParser(*osb);
+ ResXMLParser* st = new(std::nothrow) ResXMLParser(*osb);
if (st == NULL) {
jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
return 0;
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index e9ce712..d98836f 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -104,7 +104,6 @@
"mockito-target-extended-minus-junit4",
"TestParameterInjector",
"android.content.res.flags-aconfig-java",
- "android.security.flags-aconfig-java",
],
libs: [
diff --git a/core/tests/coretests/src/android/content/IntentTest.java b/core/tests/coretests/src/android/content/IntentTest.java
deleted file mode 100644
index d169ce3..0000000
--- a/core/tests/coretests/src/android/content/IntentTest.java
+++ /dev/null
@@ -1,99 +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 android.content;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertEquals;
-
-import android.net.Uri;
-import android.os.Binder;
-import android.os.IBinder;
-import android.os.Parcel;
-import android.platform.test.annotations.Presubmit;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
-import android.security.Flags;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Build/Install/Run:
- * atest FrameworksCoreTests:IntentTest
- */
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class IntentTest {
- private static final String TEST_ACTION = "android.content.IntentTest_test";
- private static final String TEST_EXTRA_NAME = "testExtraName";
- private static final Uri TEST_URI = Uri.parse("content://com.example/people");
-
- @Rule
- public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
-
- @Test
- @RequiresFlagsEnabled(Flags.FLAG_PREVENT_INTENT_REDIRECT)
- public void testReadFromParcelWithExtraIntentKeys() {
- Intent intent = new Intent("TEST_ACTION");
- intent.putExtra(TEST_EXTRA_NAME, new Intent(TEST_ACTION));
- intent.putExtra(TEST_EXTRA_NAME + "2", 1);
-
- intent.collectExtraIntentKeys();
- final Parcel parcel = Parcel.obtain();
- intent.writeToParcel(parcel, 0);
- parcel.setDataPosition(0);
- final Intent target = new Intent();
- target.readFromParcel(parcel);
-
- assertEquals(intent.getAction(), target.getAction());
- assertEquals(intent.getExtraIntentKeys(), target.getExtraIntentKeys());
- assertThat(intent.getExtraIntentKeys()).hasSize(1);
- }
-
- @Test
- public void testCreatorTokenInfo() {
- Intent intent = new Intent(TEST_ACTION);
- IBinder creatorToken = new Binder();
-
- intent.setCreatorToken(creatorToken);
- assertThat(intent.getCreatorToken()).isEqualTo(creatorToken);
-
- intent.removeCreatorTokenInfo();
- assertThat(intent.getCreatorToken()).isNull();
- }
-
- @Test
- @RequiresFlagsEnabled(Flags.FLAG_PREVENT_INTENT_REDIRECT)
- public void testCollectExtraIntentKeys() {
- Intent intent = new Intent(TEST_ACTION);
- Intent extraIntent = new Intent(TEST_ACTION, TEST_URI);
- intent.putExtra(TEST_EXTRA_NAME, extraIntent);
-
- intent.collectExtraIntentKeys();
-
- assertThat(intent.getExtraIntentKeys()).hasSize(1);
- assertThat(intent.getExtraIntentKeys()).contains(TEST_EXTRA_NAME);
- }
-
-}
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
index 89acbc7..0ce403e 100644
--- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
@@ -35,6 +35,7 @@
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
import android.os.Parcel;
+import android.view.Display;
import androidx.test.runner.AndroidJUnit4;
@@ -64,10 +65,12 @@
assertNull("Bitmap was expected to be null", out.getBitmap());
assertNull("Bounds were expected to be null", out.getBoundsInScreen());
assertEquals(Insets.NONE, out.getInsets());
+ assertEquals(Display.INVALID_DISPLAY, out.getDisplayId());
}
@Test
public void testProvidedScreenshot() {
+ int displayId = 5;
Bitmap bitmap = makeHardwareBitmap(50, 50);
ScreenshotRequest in =
new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
@@ -77,6 +80,7 @@
.setBitmap(bitmap)
.setBoundsOnScreen(new Rect(10, 10, 60, 60))
.setInsets(Insets.of(2, 3, 4, 5))
+ .setDisplayId(displayId)
.build();
Parcel parcel = Parcel.obtain();
@@ -92,6 +96,7 @@
assertTrue("Bitmaps should be equal", out.getBitmap().sameAs(bitmap));
assertEquals(new Rect(10, 10, 60, 60), out.getBoundsInScreen());
assertEquals(Insets.of(2, 3, 4, 5), out.getInsets());
+ assertEquals(displayId, out.getDisplayId());
}
@Test
diff --git a/libs/appfunctions/tests/Android.bp b/libs/appfunctions/tests/Android.bp
new file mode 100644
index 0000000..6f5eff3
--- /dev/null
+++ b/libs/appfunctions/tests/Android.bp
@@ -0,0 +1,41 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "AppFunctionsSidecarTestCases",
+ team: "trendy_team_system_intelligence",
+ static_libs: [
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "androidx.core_core-ktx",
+ "com.google.android.appfunctions.sidecar.impl",
+ "junit",
+ "kotlin-test",
+ "mockito-target-extended-minus-junit4",
+ "platform-test-annotations",
+ "testables",
+ "testng",
+ "truth",
+ ],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+}
diff --git a/libs/appfunctions/tests/AndroidManifest.xml b/libs/appfunctions/tests/AndroidManifest.xml
new file mode 100644
index 0000000..9a7d460
--- /dev/null
+++ b/libs/appfunctions/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.appfunctions.sidecar.tests">
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.mock" />
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.google.android.appfunctions.sidecar.tests">
+ </instrumentation>
+</manifest>
diff --git a/libs/appfunctions/tests/AndroidTest.xml b/libs/appfunctions/tests/AndroidTest.xml
new file mode 100644
index 0000000..8251212
--- /dev/null
+++ b/libs/appfunctions/tests/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Config for AppFunctions Sidecar Tests">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="AppFunctionsSidecarTestCases.apk" />
+ </target_preparer>
+ <option name="test-tag" value="AppFunctionsSidecarTestCases" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.google.android.appfunctions.sidecar.tests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt b/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt
new file mode 100644
index 0000000..1f9fddd
--- /dev/null
+++ b/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.google.android.appfunctions.sidecar.tests
+
+import android.app.appfunctions.ExecuteAppFunctionRequest
+import android.app.appfunctions.ExecuteAppFunctionResponse
+import android.app.appsearch.GenericDocument
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.appfunctions.sidecar.SidecarConverter
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SidecarConverterTest {
+ @Test
+ fun getSidecarExecuteAppFunctionRequest_sameContents() {
+ val extras = Bundle()
+ extras.putString("extra", "value")
+ val parameters: GenericDocument =
+ GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+ .setPropertyLong("testLong", 23)
+ .build()
+ val platformRequest: ExecuteAppFunctionRequest =
+ ExecuteAppFunctionRequest.Builder("targetPkg", "targetFunctionId")
+ .setExtras(extras)
+ .setParameters(parameters)
+ .build()
+
+ val sidecarRequest = SidecarConverter.getSidecarExecuteAppFunctionRequest(platformRequest)
+
+ assertThat(sidecarRequest.targetPackageName).isEqualTo("targetPkg")
+ assertThat(sidecarRequest.functionIdentifier).isEqualTo("targetFunctionId")
+ assertThat(sidecarRequest.parameters).isEqualTo(parameters)
+ assertThat(sidecarRequest.extras.size()).isEqualTo(1)
+ assertThat(sidecarRequest.extras.getString("extra")).isEqualTo("value")
+ }
+
+ @Test
+ fun getPlatformExecuteAppFunctionRequest_sameContents() {
+ val extras = Bundle()
+ extras.putString("extra", "value")
+ val parameters: GenericDocument =
+ GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+ .setPropertyLong("testLong", 23)
+ .build()
+ val sidecarRequest =
+ com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder(
+ "targetPkg",
+ "targetFunctionId"
+ )
+ .setExtras(extras)
+ .setParameters(parameters)
+ .build()
+
+ val platformRequest = SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest)
+
+ assertThat(platformRequest.targetPackageName).isEqualTo("targetPkg")
+ assertThat(platformRequest.functionIdentifier).isEqualTo("targetFunctionId")
+ assertThat(platformRequest.parameters).isEqualTo(parameters)
+ assertThat(platformRequest.extras.size()).isEqualTo(1)
+ assertThat(platformRequest.extras.getString("extra")).isEqualTo("value")
+ }
+
+ @Test
+ fun getSidecarExecuteAppFunctionResponse_successResponse_sameContents() {
+ val resultGd: GenericDocument =
+ GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+ .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true)
+ .build()
+ val platformResponse = ExecuteAppFunctionResponse.newSuccess(resultGd, null)
+
+ val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse(
+ platformResponse
+ )
+
+ assertThat(sidecarResponse.isSuccess).isTrue()
+ assertThat(
+ sidecarResponse.resultDocument.getProperty(
+ ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE
+ )
+ )
+ .isEqualTo(booleanArrayOf(true))
+ assertThat(sidecarResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK)
+ assertThat(sidecarResponse.errorMessage).isNull()
+ }
+
+ @Test
+ fun getSidecarExecuteAppFunctionResponse_errorResponse_sameContents() {
+ val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build()
+ val platformResponse =
+ ExecuteAppFunctionResponse.newFailure(
+ ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+ null,
+ null
+ )
+
+ val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse(
+ platformResponse
+ )
+
+ assertThat(sidecarResponse.isSuccess).isFalse()
+ assertThat(sidecarResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace)
+ assertThat(sidecarResponse.resultDocument.id).isEqualTo(emptyGd.id)
+ assertThat(sidecarResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType)
+ assertThat(sidecarResponse.resultCode)
+ .isEqualTo(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR)
+ assertThat(sidecarResponse.errorMessage).isNull()
+ }
+
+ @Test
+ fun getPlatformExecuteAppFunctionResponse_successResponse_sameContents() {
+ val resultGd: GenericDocument =
+ GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+ .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true)
+ .build()
+ val sidecarResponse = com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse
+ .newSuccess(resultGd, null)
+
+ val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse(
+ sidecarResponse
+ )
+
+ assertThat(platformResponse.isSuccess).isTrue()
+ assertThat(
+ platformResponse.resultDocument.getProperty(
+ ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE
+ )
+ )
+ .isEqualTo(booleanArrayOf(true))
+ assertThat(platformResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK)
+ assertThat(platformResponse.errorMessage).isNull()
+ }
+
+ @Test
+ fun getPlatformExecuteAppFunctionResponse_errorResponse_sameContents() {
+ val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build()
+ val sidecarResponse =
+ com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse.newFailure(
+ ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+ null,
+ null
+ )
+
+ val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse(
+ sidecarResponse
+ )
+
+ assertThat(platformResponse.isSuccess).isFalse()
+ assertThat(platformResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace)
+ assertThat(platformResponse.resultDocument.id).isEqualTo(emptyGd.id)
+ assertThat(platformResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType)
+ assertThat(platformResponse.resultCode)
+ .isEqualTo(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR)
+ assertThat(platformResponse.errorMessage).isNull()
+ }
+}
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 31f8996..ef4c3ef 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -16,6 +16,8 @@
package android.media.projection;
+import static android.view.Display.DEFAULT_DISPLAY;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.compat.CompatChanges;
@@ -29,6 +31,7 @@
import android.os.Build;
import android.os.Handler;
import android.os.RemoteException;
+import android.os.UserManager;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
@@ -70,6 +73,7 @@
private final DisplayManager mDisplayManager;
@NonNull
private final Map<Callback, CallbackRecord> mCallbacks = new ArrayMap<>();
+ private final int mDisplayId;
/** @hide */
public MediaProjection(Context context, IMediaProjection impl) {
@@ -88,6 +92,11 @@
throw new RuntimeException("Failed to start media projection", e);
}
mDisplayManager = displayManager;
+
+ final UserManager userManager = context.getSystemService(UserManager.class);
+ mDisplayId = userManager.isVisibleBackgroundUsersSupported()
+ ? userManager.getMainDisplayIdAssignedToUser()
+ : DEFAULT_DISPLAY;
}
/**
@@ -156,6 +165,7 @@
if (surface != null) {
builder.setSurface(surface);
}
+ builder.setDisplayIdToMirror(mDisplayId);
return createVirtualDisplay(builder, callback, handler);
}
@@ -234,6 +244,7 @@
if (surface != null) {
builder.setSurface(surface);
}
+ builder.setDisplayIdToMirror(mDisplayId);
return createVirtualDisplay(builder, callback, handler);
}
diff --git a/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java b/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java
index c376f25..8b28347 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java
@@ -17,6 +17,7 @@
package android.media.soundtrigger;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -328,6 +329,7 @@
* Get the recognition config used to start this recognition.
* @return - The config passed to the HAL for startRecognition.
*/
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
public @NonNull SoundTrigger.RecognitionConfig getRecognitionConfig() {
return mRecognitionConfig;
}
diff --git a/media/java/android/media/soundtrigger/SoundTriggerManager.java b/media/java/android/media/soundtrigger/SoundTriggerManager.java
index 78352871..3d0c406 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerManager.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerManager.java
@@ -19,6 +19,7 @@
import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -42,7 +43,6 @@
import android.media.permission.Identity;
import android.media.permission.SafeCloseable;
import android.os.Binder;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -463,13 +463,18 @@
public static final String EXTRA_STATUS = "android.media.soundtrigger.STATUS";
/**
- * Loads a given sound model into the sound trigger. Note the model will be unloaded if there is
- * an error/the system service is restarted.
- * @hide
+ * Loads a given sound model into the sound trigger.
+ *
+ * <p><b>Note:</b> the model will be unloaded if there is an error/the system service is
+ * restarted.
+ *
+ * @return {@link SoundTrigger#STATUS_OK} if the model was loaded successfully, error code
+ * otherwise
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
@UnsupportedAppUsage
- @TestApi
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
public int loadSoundModel(@NonNull SoundModel soundModel) {
if (mSoundTriggerSession == null) {
throw new IllegalStateException("No underlying SoundTriggerModule available");
@@ -508,11 +513,11 @@
*
* @return {@link SoundTrigger#STATUS_OK} if the recognition could be started, error code
* otherwise
- *
- * @hide
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
@UnsupportedAppUsage
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
public int startRecognition(@NonNull UUID soundModelId, @Nullable Bundle params,
@NonNull ComponentName detectionService, @NonNull RecognitionConfig config) {
Objects.requireNonNull(soundModelId);
@@ -531,11 +536,15 @@
/**
* Stops the given model's recognition.
- * @hide
+ *
+ * @return {@link SoundTrigger#STATUS_OK} if the recognition could be stopped, error code
+ * otherwise
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public int stopRecognition(UUID soundModelId) {
+ @UnsupportedAppUsage
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
+ public int stopRecognition(@NonNull UUID soundModelId) {
if (mSoundTriggerSession == null) {
throw new IllegalStateException("No underlying SoundTriggerModule available");
}
@@ -548,12 +557,18 @@
}
/**
- * Removes the given model from memory. Will also stop any pending recognitions.
- * @hide
+ * Removes the given model from memory.
+ *
+ * <p>Will also stop any pending recognitions.
+ *
+ * @return {@link SoundTrigger#STATUS_OK} if the model was unloaded successfully, error code
+ * otherwise
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public int unloadSoundModel(UUID soundModelId) {
+ @UnsupportedAppUsage
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
+ public int unloadSoundModel(@NonNull UUID soundModelId) {
if (mSoundTriggerSession == null) {
throw new IllegalStateException("No underlying SoundTriggerModule available");
}
@@ -566,12 +581,13 @@
}
/**
- * Returns true if the given model has had detection started on it.
- * @hide
+ * Returns whether the given model has had detection started on it.
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
@UnsupportedAppUsage
- public boolean isRecognitionActive(UUID soundModelId) {
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
+ public boolean isRecognitionActive(@NonNull UUID soundModelId) {
if (soundModelId == null || mSoundTriggerSession == null) {
return false;
}
@@ -599,14 +615,16 @@
}
/**
- * Asynchronously get state of the indicated model. The model state is returned as
- * a recognition event in the callback that was registered in the startRecognition
- * method.
- * @hide
+ * Asynchronously gets state of the indicated model.
+ *
+ * <p>The model state is returned as a recognition event in the callback that was registered
+ * in the {@link #startRecognition(UUID, Bundle, ComponentName, RecognitionConfig)} method.
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
@UnsupportedAppUsage
- public int getModelState(UUID soundModelId) {
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
+ public int getModelState(@NonNull UUID soundModelId) {
if (mSoundTriggerSession == null) {
throw new IllegalStateException("No underlying SoundTriggerModule available");
}
@@ -621,7 +639,7 @@
}
/**
- * Get the hardware sound trigger module properties currently loaded.
+ * Gets the hardware sound trigger module properties currently loaded.
*
* @return The properties currently loaded. Returns null if no supported hardware loaded.
*/
@@ -639,9 +657,11 @@
}
/**
- * Set a model specific {@link ModelParams} with the given value. This
- * parameter will keep its value for the duration the model is loaded regardless of starting and
- * stopping recognition. Once the model is unloaded, the value will be lost.
+ * Sets a model specific {@link ModelParams} with the given value.
+ *
+ * <p>This parameter will keep its value for the duration the model is loaded regardless of
+ * starting and stopping recognition. Once the model is unloaded, the value will be lost.
+ *
* {@link SoundTriggerManager#queryParameter} should be checked first before calling this
* method.
*
@@ -671,12 +691,15 @@
}
/**
- * Get a model specific {@link ModelParams}. This parameter will keep its value
- * for the duration the model is loaded regardless of starting and stopping recognition.
- * Once the model is unloaded, the value will be lost. If the value is not set, a default
- * value is returned. See {@link ModelParams} for parameter default values.
- * {@link SoundTriggerManager#queryParameter} should be checked first before
- * calling this method. Otherwise, an exception can be thrown.
+ * Gets a model specific {@link ModelParams}.
+ *
+ * <p>This parameter will keep its value for the duration the model is loaded regardless
+ * of starting and stopping recognition. Once the model is unloaded, the value will be lost.
+ * If the value is not set, a default value is returned. See {@link ModelParams} for
+ * parameter default values.
+ *
+ * {@link SoundTriggerManager#queryParameter} should be checked first before calling this
+ * method. Otherwise, an exception can be thrown.
*
* @param soundModelId UUID of model to get parameter
* @param modelParam {@link ModelParams}
@@ -697,9 +720,10 @@
}
/**
- * Determine if parameter control is supported for the given model handle.
- * This method should be checked prior to calling {@link SoundTriggerManager#setParameter} or
- * {@link SoundTriggerManager#getParameter}.
+ * Determines if parameter control is supported for the given model handle.
+ *
+ * <p>This method should be checked prior to calling {@link SoundTriggerManager#setParameter}
+ * or {@link SoundTriggerManager#getParameter}.
*
* @param soundModelId handle of model to get parameter
* @param modelParam {@link ModelParams}
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index 0a5eb9b..d4851e1 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -32,6 +32,7 @@
"SettingsLibBannerMessagePreference",
"SettingsLibBarChartPreference",
"SettingsLibButtonPreference",
+ "SettingsLibBulletPreference",
"SettingsLibCollapsingToolbarBaseActivity",
"SettingsLibDeviceStateRotationLock",
"SettingsLibDisplayUtils",
diff --git a/packages/SettingsLib/BulletPreference/Android.bp b/packages/SettingsLib/BulletPreference/Android.bp
new file mode 100644
index 0000000..3ea0b2b
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/Android.bp
@@ -0,0 +1,33 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+ name: "SettingsLibBulletPreference",
+ use_resource_processor: true,
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ resource_dirs: ["res"],
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.preference_preference",
+ "SettingsLibSettingsTheme",
+ ],
+ sdk_version: "system_current",
+ min_sdk_version: "21",
+ apex_available: [
+ "//apex_available:platform",
+ ],
+}
diff --git a/packages/SettingsLib/BulletPreference/AndroidManifest.xml b/packages/SettingsLib/BulletPreference/AndroidManifest.xml
new file mode 100644
index 0000000..c7495ef
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.widget.preference.bullet">
+
+ <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_icon_frame.xml b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_icon_frame.xml
new file mode 100644
index 0000000..030f024
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_icon_frame.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minWidth="@dimen/settingslib_expressive_space_medium4"
+ android:gravity="top|start"
+ android:layout_marginTop="@dimen/settingslib_expressive_space_small1"
+ android:paddingStart="@dimen/settingslib_expressive_space_extrasmall4"
+ android:paddingEnd="@dimen/settingslib_expressive_space_small1">
+
+ <androidx.preference.internal.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:maxWidth="@dimen/settingslib_expressive_space_small4"
+ app:maxHeight="@dimen/settingslib_expressive_space_small4"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_preference.xml b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_preference.xml
new file mode 100644
index 0000000..3f37f6c
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_preference.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:background="?android:attr/selectableItemBackground"
+ android:clipToPadding="false"
+ android:baselineAligned="false">
+
+ <include layout="@layout/settingslib_expressive_bullet_icon_frame"/>
+
+ <include layout="@layout/settingslib_expressive_preference_text_frame"/>
+
+ <!-- Preference should place its actual preference widget here. -->
+ <LinearLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="end|center_vertical"
+ android:paddingStart="@dimen/settingslib_expressive_space_small1"
+ android:paddingEnd="0dp"
+ android:orientation="vertical"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/BulletPreference/src/com/android/settingslib/widget/BulletPreference.kt b/packages/SettingsLib/BulletPreference/src/com/android/settingslib/widget/BulletPreference.kt
new file mode 100644
index 0000000..45e2e9a
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/src/com/android/settingslib/widget/BulletPreference.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.settingslib.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+
+import com.android.settingslib.widget.preference.bullet.R
+
+/**
+ * The BulletPreference shows a text which describe a feature.
+ */
+class BulletPreference @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+ init {
+ layoutResource = R.layout.settingslib_expressive_bullet_preference
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+ holder.isDividerAllowedAbove = false
+ holder.isDividerAllowedBelow = false
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v35/settingslib_expressive_icon_back.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v35/settingslib_expressive_icon_back.xml
new file mode 100644
index 0000000..ccbe20e
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v35/settingslib_expressive_icon_back.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:start="16dp"
+ android:end="16dp"
+ android:top="4dp"
+ android:bottom="4dp">
+ <shape>
+ <size android:width="32dp" android:height="40dp" />
+ <solid android:color="@color/settingslib_materialColorSurfaceContainerHighest" />
+ <corners
+ android:radius="100dp" />
+ </shape>
+ </item>
+
+ <item
+ android:width="24dp"
+ android:height="24dp"
+ android:gravity="center"
+ android:start="16dp"
+ android:end="16dp"
+ android:top="4dp"
+ android:bottom="4dp">
+ <vector
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="@color/settingslib_materialColorOnSurfaceVariant"
+ android:autoMirrored="true">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/>
+ </vector>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_base_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_base_layout.xml
new file mode 100644
index 0000000..b881c57
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_base_layout.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content_parent"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <include layout="@layout/settingslib_expressive_collapsing_toolbar_content_layout"/>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_content_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_content_layout.xml
new file mode 100644
index 0000000..6221659
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_content_layout.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<merge
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/app_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true"
+ android:outlineAmbientShadowColor="@android:color/transparent"
+ android:outlineSpotShadowColor="@android:color/transparent"
+ android:background="@android:color/transparent"
+ app:expanded="false"
+ android:theme="@style/SettingsLibTheme.CollapsingToolbar.Expressive">
+
+ <com.google.android.material.appbar.CollapsingToolbarLayout
+ android:id="@+id/collapsing_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/settingslib_toolbar_layout_height"
+ app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
+ app:toolbarId="@id/action_bar"
+ style="@style/SettingsLibCollapsingToolbarLayoutStyle.Expressive">
+
+ <Toolbar
+ android:id="@+id/action_bar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:theme="?android:attr/actionBarTheme"
+ android:transitionName="shared_element_view"
+ app:layout_collapseMode="pin"/>
+
+ </com.google.android.material.appbar.CollapsingToolbarLayout>
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <FrameLayout
+ android:id="@+id/content_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
+</merge>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/styles_expressive.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/styles_expressive.xml
new file mode 100644
index 0000000..d58c2c2
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/styles_expressive.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <style name="SettingsLibCollapsingToolbarLayoutStyle">
+ <item name="expandedTitleTextAppearance">@style/SettingsLibCollapsingToolbarTitle.Expanded</item>
+ <item name="collapsedTitleTextAppearance">@style/SettingsLibCollapsingToolbarTitle.Collapsed</item>
+ <item name="expandedTitleMarginStart">@dimen/settingslib_expressive_space_small4</item>
+ <item name="expandedTitleMarginEnd">@dimen/settingslib_expressive_space_small4</item>
+ <item name="expandedTitleMarginBottom">@dimen/settingslib_expressive_space_medium1</item>
+ <item name="maxLines">3</item>
+ <item name="scrimVisibleHeightTrigger">@dimen/settingslib_scrim_visible_height_trigger</item>
+ <item name="contentScrim">@color/settingslib_materialColorSurfaceVariant</item>
+ <item name="statusBarScrim">@null</item>
+ <item name="scrimAnimationDuration">50</item>
+ <item name="collapsedTitleTextColor">@color/settingslib_materialColorOnSurface</item>
+ <item name="expandedTitleTextColor">@color/settingslib_materialColorOnSurface</item>
+ </style>
+ <style name="SettingsLibCollapsingToolbarLayoutStyle.Expressive">
+ <item name="contentScrim">@color/settingslib_materialColorSurfaceContainer</item>
+ </style>
+
+ <style name="SettingsLibCollapsingToolbarTitle.Collapsed" parent="@android:style/TextAppearance.DeviceDefault.Headline">
+ <!--set dp because we don't want size adjust when font size change-->
+ <item name="android:textSize">20dp</item>
+ </style>
+
+ <style name="SettingsLibCollapsingToolbarTitle.Expanded" parent="CollapsingToolbarTitle.Collapsed">
+ <item name="android:textSize">36dp</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/themes_expressive.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/themes_expressive.xml
new file mode 100644
index 0000000..ca1904a
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/themes_expressive.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <style name="SettingsLibTheme.CollapsingToolbar.Expressive" parent="@style/Theme.MaterialComponents.DayNight"/>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java
index f46f110..feacecb 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java
@@ -27,6 +27,8 @@
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
+import com.android.settingslib.widget.SettingsThemeHelper;
+
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.google.android.material.color.DynamicColors;
@@ -69,7 +71,10 @@
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
DynamicColors.applyToActivityIfAvailable(this);
}
- setTheme(com.android.settingslib.widget.theme.R.style.Theme_SubSettingsBase);
+ int resId = SettingsThemeHelper.isExpressiveTheme(this)
+ ? com.android.settingslib.widget.theme.R.style.Theme_SubSettingsBase_Expressive
+ : com.android.settingslib.widget.theme.R.style.Theme_SubSettingsBase;
+ setTheme(resId);
if (mCustomizeLayoutResId > 0 && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
super.setContentView(mCustomizeLayoutResId);
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
index 16ed5a8..0f9d94e 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
@@ -28,6 +28,8 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
+import com.android.settingslib.widget.SettingsThemeHelper;
+
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
@@ -59,6 +61,11 @@
protected void onCreate(@Nullable Bundle savedInstanceState) {
EdgeToEdgeUtils.enable(this);
super.onCreate(savedInstanceState);
+
+ if (SettingsThemeHelper.isExpressiveTheme(this)) {
+ setTheme(com.android.settingslib.widget.theme.R.style.Theme_SubSettingsBase_Expressive);
+ }
+
// for backward compatibility on R devices or wearable devices due to small device size.
if (mCustomizeLayoutResId > 0 && (Build.VERSION.SDK_INT < Build.VERSION_CODES.S
|| isWatch())) {
@@ -66,7 +73,7 @@
return;
}
- View view = getToolbarDelegate().onCreateView(getLayoutInflater(), null);
+ View view = getToolbarDelegate().onCreateView(getLayoutInflater(), null, this);
super.setContentView(view);
}
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
index 2ab2abd..01ecb66 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
@@ -20,6 +20,7 @@
import android.app.ActionBar;
import android.app.Activity;
+import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
@@ -37,6 +38,8 @@
import androidx.appcompat.app.AppCompatActivity;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import com.android.settingslib.widget.SettingsThemeHelper;
+
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
@@ -84,6 +87,8 @@
private boolean mUseCollapsingToolbar;
+ private boolean mIsExpressiveTheme;
+
public CollapsingToolbarDelegate(@NonNull HostCallback hostCallback,
boolean useCollapsingToolbar) {
mHostCallback = hostCallback;
@@ -103,11 +108,16 @@
int layoutId;
boolean useCollapsingToolbar =
mUseCollapsingToolbar || Build.VERSION.SDK_INT < Build.VERSION_CODES.S;
+ Context context = (activity != null) ? activity : inflater.getContext();
+ mIsExpressiveTheme = SettingsThemeHelper.isExpressiveTheme(context);
if (useCollapsingToolbar) {
- layoutId = R.layout.collapsing_toolbar_base_layout;
+ layoutId = mIsExpressiveTheme
+ ? R.layout.settingslib_expressive_collapsing_toolbar_base_layout
+ : R.layout.collapsing_toolbar_base_layout;
} else {
layoutId = R.layout.non_collapsing_toolbar_base_layout;
}
+
final View view = inflater.inflate(layoutId, container, false);
if (view instanceof CoordinatorLayout) {
mCoordinatorLayout = (CoordinatorLayout) view;
@@ -155,6 +165,9 @@
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
+ if (mIsExpressiveTheme) {
+ actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+ }
actionBar.setDisplayShowTitleEnabled(true);
}
}
@@ -174,6 +187,9 @@
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
+ if (mIsExpressiveTheme) {
+ actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+ }
actionBar.setDisplayShowTitleEnabled(true);
}
}
@@ -188,6 +204,9 @@
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
+ if (mIsExpressiveTheme) {
+ actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+ }
actionBar.setDisplayShowTitleEnabled(true);
}
}
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
index f70add9..51d7504 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
@@ -39,6 +39,7 @@
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.android.settingslib.collapsingtoolbar.R;
+import com.android.settingslib.widget.SettingsThemeHelper;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
@@ -105,7 +106,10 @@
}
private void init() {
- inflate(getContext(), R.layout.collapsing_toolbar_content_layout, this);
+ int resId = SettingsThemeHelper.isExpressiveTheme(getContext())
+ ? R.layout.settingslib_expressive_collapsing_toolbar_content_layout
+ : R.layout.collapsing_toolbar_content_layout;
+ inflate(getContext(), resId, this);
mCollapsingToolbarLayout = findViewById(R.id.collapsing_toolbar);
mAppBarLayout = findViewById(R.id.app_bar);
if (mCollapsingToolbarLayout != null) {
@@ -172,6 +176,9 @@
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayShowTitleEnabled(true);
+ if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+ actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+ }
}
}
@@ -202,6 +209,9 @@
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayShowTitleEnabled(true);
+ if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+ actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+ }
}
}
diff --git a/packages/SettingsLib/MainSwitchPreference/res/drawable-v35/settingslib_expressive_switch_bar_bg.xml b/packages/SettingsLib/MainSwitchPreference/res/drawable-v35/settingslib_expressive_switch_bar_bg.xml
new file mode 100644
index 0000000..3999e76
--- /dev/null
+++ b/packages/SettingsLib/MainSwitchPreference/res/drawable-v35/settingslib_expressive_switch_bar_bg.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/settingslib_materialColorPrimaryContainer"/>
+ <corners android:radius="@dimen/settingslib_expressive_radius_extralarge3"/>
+ </shape>
+ </item>
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_bar.xml
new file mode 100644
index 0000000..4425ef0
--- /dev/null
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_bar.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingVertical="@dimen/settingslib_expressive_space_small1"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/frame"
+ android:minHeight="@dimen/settingslib_expressive_space_large3"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:paddingStart="@dimen/settingslib_expressive_space_medium1"
+ android:paddingEnd="@dimen/settingslib_expressive_space_small3"
+ android:background="@drawable/settingslib_expressive_switch_bar_bg">
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginEnd="@dimen/settingslib_expressive_space_extrasmall4"
+ android:layout_marginVertical="@dimen/settingslib_expressive_space_small4"
+ android:layout_weight="1">
+
+ <TextView
+ android:id="@+id/switch_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:ellipsize="end"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:textColor="@color/settingslib_main_switch_text_color" />
+
+ <TextView
+ android:id="@+id/switch_summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignLeft="@id/switch_text"
+ android:layout_alignStart="@id/switch_text"
+ android:layout_below="@id/switch_text"
+ android:layout_gravity="start"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"
+ android:textColor="@color/settingslib_main_switch_text_color"
+ android:visibility="gone" />
+ </RelativeLayout>
+
+ <com.google.android.material.materialswitch.MaterialSwitch
+ android:theme="@style/Theme.Material3.DynamicColors.DayNight"
+ android:id="@android:id/switch_widget"
+ style="@style/SettingslibMainSwitchStyle.Expressive" />
+ </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
index e6f61a8..106802e 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
@@ -22,6 +22,7 @@
import android.os.Build.VERSION_CODES;
import android.os.Parcel;
import android.os.Parcelable;
+import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -31,6 +32,7 @@
import android.widget.TextView;
import androidx.annotation.ColorInt;
+import androidx.annotation.RequiresApi;
import com.android.settingslib.widget.mainswitch.R;
@@ -52,6 +54,7 @@
private int mBackgroundActivatedColor;
protected TextView mTextView;
+ protected TextView mSummaryView;
protected CompoundButton mSwitch;
private final View mFrameView;
@@ -71,7 +74,11 @@
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- LayoutInflater.from(context).inflate(R.layout.settingslib_main_switch_bar, this);
+ boolean isExpressive = SettingsThemeHelper.isExpressiveTheme(context);
+ int resId = isExpressive
+ ? R.layout.settingslib_expressive_main_switch_bar
+ : R.layout.settingslib_main_switch_bar;
+ LayoutInflater.from(context).inflate(resId, this);
if (Build.VERSION.SDK_INT < VERSION_CODES.S) {
TypedArray a;
@@ -93,6 +100,9 @@
mFrameView = findViewById(R.id.frame);
mTextView = findViewById(R.id.switch_text);
+ if (isExpressive) {
+ mSummaryView = findViewById(R.id.switch_summary);
+ }
mSwitch = findViewById(android.R.id.switch_widget);
addOnSwitchChangeListener((switchView, isChecked) -> setChecked(isChecked));
@@ -109,6 +119,12 @@
final CharSequence title = a.getText(
androidx.preference.R.styleable.Preference_android_title);
setTitle(title);
+ //TODO(b/369470034): update to next version
+ if (isExpressive && Build.VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) {
+ CharSequence summary = a.getText(
+ androidx.preference.R.styleable.Preference_android_summary);
+ setSummary(summary);
+ }
a.recycle();
}
@@ -153,6 +169,18 @@
}
/**
+ * Set the summary text
+ */
+ @RequiresApi(VERSION_CODES.VANILLA_ICE_CREAM)
+ //TODO(b/369470034): update to next version
+ public void setSummary(CharSequence text) {
+ if (mSummaryView != null) {
+ mSummaryView.setText(text);
+ mSummaryView.setVisibility(TextUtils.isEmpty(text) ? GONE : VISIBLE);
+ }
+ }
+
+ /**
* Set icon space reserved for title
*/
public void setIconSpaceReserved(boolean iconSpaceReserved) {
@@ -219,6 +247,12 @@
mFrameView.setEnabled(enabled);
mFrameView.setActivated(isChecked());
}
+
+ if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+ if (mSummaryView != null) {
+ mSummaryView.setEnabled(enabled);
+ }
+ }
}
private void propagateChecked(boolean isChecked) {
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
index b294d4e..d895c87 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.content.res.TypedArray;
+import android.os.Build;
import android.util.AttributeSet;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
@@ -89,6 +90,10 @@
androidx.preference.R.styleable.Preference_android_title);
setTitle(title);
+ CharSequence summary = a.getText(
+ androidx.preference.R.styleable.Preference_android_summary);
+ setSummary(summary);
+
final boolean bIconSpaceReserved = a.getBoolean(
androidx.preference.R.styleable.Preference_android_iconSpaceReserved, true);
setIconSpaceReserved(bIconSpaceReserved);
@@ -113,6 +118,15 @@
}
@Override
+ public void setSummary(CharSequence summary) {
+ super.setSummary(summary);
+ if (mMainSwitchBar != null
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ mMainSwitchBar.setSummary(summary);
+ }
+ }
+
+ @Override
public void setIconSpaceReserved(boolean iconSpaceReserved) {
super.setIconSpaceReserved(iconSpaceReserved);
if (mMainSwitchBar != null) {
diff --git a/packages/SettingsLib/Service/Android.bp b/packages/SettingsLib/Service/Android.bp
new file mode 100644
index 0000000..65d0e4c
--- /dev/null
+++ b/packages/SettingsLib/Service/Android.bp
@@ -0,0 +1,20 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "SettingsLibService-srcs",
+ srcs: ["src/**/*.kt"],
+}
+
+android_library {
+ name: "SettingsLibService",
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+ srcs: [":SettingsLibService-srcs"],
+ static_libs: [
+ "SettingsLibGraph",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Service/AndroidManifest.xml b/packages/SettingsLib/Service/AndroidManifest.xml
new file mode 100644
index 0000000..56d7818
--- /dev/null
+++ b/packages/SettingsLib/Service/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.service">
+
+ <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
new file mode 100644
index 0000000..95661c9
--- /dev/null
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.settingslib.service
+
+import android.app.Application
+import com.android.settingslib.graph.GetPreferenceGraphApiHandler
+import com.android.settingslib.graph.GetPreferenceGraphRequest
+
+/** Api to get preference graph. */
+internal class PreferenceGraphApi(activityClasses: Set<String>) :
+ GetPreferenceGraphApiHandler(activityClasses) {
+ override val id: Int
+ get() = API_GET_PREFERENCE_GRAPH
+
+ override fun hasPermission(
+ application: Application,
+ myUid: Int,
+ callingUid: Int,
+ request: GetPreferenceGraphRequest,
+ ): Boolean {
+ return true // TODO: add permission check
+ }
+}
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
new file mode 100644
index 0000000..d382dad
--- /dev/null
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.settingslib.service
+
+import com.android.settingslib.ipc.ApiHandler
+import com.android.settingslib.ipc.MessengerService
+import com.android.settingslib.ipc.PermissionChecker
+
+/**
+ * Preference service providing a bunch of APIs.
+ *
+ * In AndroidManifest.xml, the <service> must specify <intent-filter> with action
+ * [PREFERENCE_SERVICE_ACTION].
+ */
+open class PreferenceService(
+ permissionChecker: PermissionChecker,
+ name: String = "PreferenceService",
+) :
+ MessengerService(
+ listOf<ApiHandler<*, *>>(PreferenceGraphApi(setOf())),
+ permissionChecker,
+ name,
+ )
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
new file mode 100644
index 0000000..8f03111
--- /dev/null
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.settingslib.service
+
+const val PREFERENCE_SERVICE_ACTION = "com.android.settingslib.PREFERENCE_SERVICE"
+
+internal const val API_GET_PREFERENCE_GRAPH = 1
diff --git a/packages/SettingsLib/SettingsTheme/res/color-v35/settingslib_expressive_color_main_switch_track.xml b/packages/SettingsLib/SettingsTheme/res/color-v35/settingslib_expressive_color_main_switch_track.xml
new file mode 100644
index 0000000..a6885a4
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/color-v35/settingslib_expressive_color_main_switch_track.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- State Disabled, Unchecked -->
+ <item
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+ android:state_enabled="false" android:state_checked="false"/>
+ <!-- State Disabled, Checked -->
+ <item
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@color/settingslib_materialColorPrimary"
+ android:state_enabled="false" android:state_checked="true"/>
+ <!-- State Checked -->
+ <item
+ android:color="@color/settingslib_materialColorPrimary"
+ android:state_checked="true"/>
+
+ <!-- State Unchecked -->
+ <item
+ android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+ android:state_hovered="true" android:state_checked="false"/>
+ <item
+ android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+ android:state_focused="true" android:state_checked="false"/>
+ <item
+ android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+ android:state_pressed="true" android:state_checked="false"/>
+ <item
+ android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+ android:state_checked="false"/>
+</selector>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_icon_bullet_start.xml b/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_icon_bullet_start.xml
new file mode 100644
index 0000000..9216c96
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_icon_bullet_start.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?android:attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M354,673L480,597L606,674L573,530L684,434L538,421L480,285L422,420L276,433L387,530L354,673ZM233,840L298,559L80,370L368,345L480,80L592,345L880,370L662,559L727,840L480,691L233,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml
index dda7517c..952562e 100644
--- a/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml
@@ -31,37 +31,7 @@
<include layout="@layout/settingslib_icon_frame"/>
- <RelativeLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:paddingTop="16dp"
- android:paddingBottom="16dp">
-
- <TextView
- android:id="@android:id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="start"
- android:textAlignment="viewStart"
- android:maxLines="2"
- android:textAppearance="?android:attr/textAppearanceListItem"
- android:ellipsize="marquee"/>
-
- <TextView
- android:id="@android:id/summary"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@android:id/title"
- android:layout_alignLeft="@android:id/title"
- android:layout_alignStart="@android:id/title"
- android:layout_gravity="start"
- android:textAlignment="viewStart"
- android:textColor="?android:attr/textColorSecondary"
- android:maxLines="10"
- style="@style/PreferenceSummaryTextStyle"/>
-
- </RelativeLayout>
+ <include layout="@layout/settingslib_preference_frame"/>
<!-- Preference should place its actual preference widget here. -->
<LinearLayout
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference_frame.xml b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference_frame.xml
new file mode 100644
index 0000000..433d264
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference_frame.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:textAlignment="viewStart"
+ android:text="Title"
+ android:maxLines="2"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:ellipsize="marquee"/>
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/title"
+ android:layout_alignLeft="@android:id/title"
+ android:layout_alignStart="@android:id/title"
+ android:text="Summary summary summary"
+ android:layout_gravity="start"
+ android:textAlignment="viewStart"
+ android:textColor="?android:attr/textColorSecondary"
+ android:maxLines="10"
+ style="@style/PreferenceSummaryTextStyle"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml
index fedcc77..4e23b65 100644
--- a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml
@@ -31,41 +31,7 @@
<include layout="@layout/settingslib_icon_frame"/>
- <RelativeLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:paddingTop="16dp"
- android:paddingBottom="16dp">
-
- <TextView
- android:id="@android:id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="start"
- android:textAlignment="viewStart"
- android:maxLines="2"
- android:hyphenationFrequency="normalFast"
- android:lineBreakWordStyle="phrase"
- android:textAppearance="?android:attr/textAppearanceListItem"
- android:ellipsize="marquee"/>
-
- <TextView
- android:id="@android:id/summary"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@android:id/title"
- android:layout_alignLeft="@android:id/title"
- android:layout_alignStart="@android:id/title"
- android:layout_gravity="start"
- android:textAlignment="viewStart"
- android:textColor="?android:attr/textColorSecondary"
- android:maxLines="10"
- android:hyphenationFrequency="normalFast"
- android:lineBreakWordStyle="phrase"
- style="@style/PreferenceSummaryTextStyle"/>
-
- </RelativeLayout>
+ <include layout="@layout/settingslib_preference_frame"/>
<!-- Preference should place its actual preference widget here. -->
<LinearLayout
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference_frame.xml b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference_frame.xml
new file mode 100644
index 0000000..f93e1b9
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference_frame.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:textAlignment="viewStart"
+ android:text="Title"
+ android:maxLines="2"
+ android:hyphenationFrequency="normalFast"
+ android:lineBreakWordStyle="phrase"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:ellipsize="marquee"/>
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/title"
+ android:layout_alignLeft="@android:id/title"
+ android:layout_alignStart="@android:id/title"
+ android:layout_gravity="start"
+ android:textAlignment="viewStart"
+ android:textColor="?android:attr/textColorSecondary"
+ android:maxLines="10"
+ android:text="Summary summary summary"
+ android:hyphenationFrequency="normalFast"
+ android:lineBreakWordStyle="phrase"
+ style="@style/PreferenceSummaryTextStyle"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
index 442def9..816433c 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
@@ -188,4 +188,9 @@
<item name="iconTint">@color/settingslib_materialColorOnSecondaryContainer</item>
<item name="iconSize">@dimen/settingslib_expressive_space_small4</item>
</style>
+
+ <style name="SettingslibMainSwitchStyle.Expressive" parent="SettingslibSwitchStyle.Expressive">
+ <item name="android:layout_gravity">center</item>
+ <item name="trackTint">@color/settingslib_expressive_color_main_switch_track</item>
+ </style>
</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml
index d01c0b9..272dc2d 100644
--- a/packages/SettingsLib/Spa/gradle/libs.versions.toml
+++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml
@@ -15,7 +15,7 @@
#
[versions]
-agp = "8.6.0"
+agp = "8.6.1"
compose-compiler = "1.5.11"
dexmaker-mockito = "2.28.3"
jvm = "17"
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10-bin.zip b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10-bin.zip
deleted file mode 100644
index 50432f3..0000000
--- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10-bin.zip
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10.2-bin.zip b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10.2-bin.zip
new file mode 100644
index 0000000..45f0424
--- /dev/null
+++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10.2-bin.zip
Binary files differ
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
index 9a7f4b6..1c25e974 100644
--- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
+++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
@@ -16,6 +16,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=gradle-8.10-bin.zip
+distributionUrl=gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts
index 790aa9f..8f8275b 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/spa/build.gradle.kts
@@ -58,12 +58,12 @@
api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion")
api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion")
api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion")
+ api("androidx.graphics:graphics-shapes-android:1.0.1")
api("androidx.lifecycle:lifecycle-livedata-ktx")
api("androidx.lifecycle:lifecycle-runtime-compose")
api("androidx.navigation:navigation-compose:2.8.1")
api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha")
- api("com.google.android.material:material:1.11.0")
- api("androidx.graphics:graphics-shapes-android:1.0.1")
+ api("com.google.android.material:material:1.12.0")
debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion")
implementation("com.airbnb.android:lottie-compose:6.4.0")
@@ -84,7 +84,6 @@
// Excludes files forked from AndroidX.
"com/android/settingslib/spa/widget/scaffold/CustomizedAppBar*",
- "com/android/settingslib/spa/widget/scaffold/TopAppBarColors*",
// Excludes files forked from Accompanist.
"com/android/settingslib/spa/framework/compose/DrawablePainter*",
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
index 712ddc8..5eeb49a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -167,6 +167,13 @@
return this;
}
+ public TestModeBuilder setVisualEffect(int effect, boolean allowed) {
+ ZenPolicy newPolicy = new ZenPolicy.Builder(mRule.getZenPolicy())
+ .showVisualEffect(effect, allowed).build();
+ setZenPolicy(newPolicy);
+ return this;
+ }
+
public TestModeBuilder setEnabled(boolean enabled) {
return setEnabled(enabled, /* byUser= */ false);
}
diff --git a/packages/SystemUI/animation/lib/Android.bp b/packages/SystemUI/animation/lib/Android.bp
index 4324d463..d9230ec 100644
--- a/packages/SystemUI/animation/lib/Android.bp
+++ b/packages/SystemUI/animation/lib/Android.bp
@@ -33,6 +33,20 @@
],
}
+// This is the core animation library written in java and can be depended by java sdk libraries.
+// Please don't introduce kotlin code in this target since kotlin is incompatible with sdk
+// libraries.
+java_library {
+ name: "PlatformAnimationLib-core",
+ srcs: [
+ "src/com/android/systemui/animation/*.java",
+ ":PlatformAnimationLib-aidl",
+ ],
+ static_libs: [
+ "WindowManager-Shell-shared",
+ ],
+}
+
filegroup {
name: "PlatformAnimationLib-aidl",
srcs: [
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
new file mode 100644
index 0000000..64bedd3
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
@@ -0,0 +1,281 @@
+/*
+ * 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.animation;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.app.ActivityOptions.LaunchCookie;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.RemoteException;
+import android.util.Log;
+import android.window.IRemoteTransition;
+import android.window.RemoteTransition;
+
+import com.android.systemui.animation.shared.IOriginTransitions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * A session object that holds origin transition states for starting an activity from an on-screen
+ * UI component and smoothly transitioning back from the activity to the same UI component.
+ */
+public class OriginTransitionSession {
+ private static final String TAG = "OriginTransitionSession";
+ static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG);
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {NOT_STARTED, STARTED, CANCELLED})
+ private @interface State {}
+
+ @State private static final int NOT_STARTED = 0;
+ @State private static final int STARTED = 1;
+ @State private static final int CANCELLED = 5;
+
+ private final String mName;
+ @Nullable private final IOriginTransitions mOriginTransitions;
+ private final Predicate<RemoteTransition> mIntentStarter;
+ @Nullable private final IRemoteTransition mEntryTransition;
+ @Nullable private final IRemoteTransition mExitTransition;
+ private final AtomicInteger mState = new AtomicInteger(NOT_STARTED);
+
+ @Nullable private RemoteTransition mOriginTransition;
+
+ private OriginTransitionSession(
+ String name,
+ @Nullable IOriginTransitions originTransitions,
+ Predicate<RemoteTransition> intentStarter,
+ @Nullable IRemoteTransition entryTransition,
+ @Nullable IRemoteTransition exitTransition) {
+ mName = name;
+ mOriginTransitions = originTransitions;
+ mIntentStarter = intentStarter;
+ mEntryTransition = entryTransition;
+ mExitTransition = exitTransition;
+ if (hasExitTransition() && !hasEntryTransition()) {
+ throw new IllegalArgumentException(
+ "Entry transition must be supplied if you want to play an exit transition!");
+ }
+ }
+
+ /**
+ * Launch the target intent with the supplied entry transition. After this method, the entry
+ * transition is expected to receive callbacks. The exit transition will be registered and
+ * triggered when the system detects a return from the launched activity to the launching
+ * activity.
+ */
+ public boolean start() {
+ if (!mState.compareAndSet(NOT_STARTED, STARTED)) {
+ logE("start: illegal state - " + stateToString(mState.get()));
+ return false;
+ }
+
+ RemoteTransition remoteTransition = null;
+ if (hasEntryTransition() && hasExitTransition()) {
+ logD("start: starting with entry and exit transition.");
+ try {
+ remoteTransition =
+ mOriginTransition =
+ mOriginTransitions.makeOriginTransition(
+ new RemoteTransition(mEntryTransition, mName + "-entry"),
+ new RemoteTransition(mExitTransition, mName + "-exit"));
+ } catch (RemoteException e) {
+ logE("Unable to create origin transition!", e);
+ }
+ } else if (hasEntryTransition()) {
+ logD("start: starting with entry transition.");
+ remoteTransition = new RemoteTransition(mEntryTransition, mName + "-entry");
+
+ } else {
+ logD("start: starting without transition.");
+ }
+ if (mIntentStarter.test(remoteTransition)) {
+ return true;
+ } else {
+ // Animation is cancelled by intent starter.
+ logD("start: cancelled by intent starter!");
+ cancel();
+ return false;
+ }
+ }
+
+ /**
+ * Cancel the current transition and the registered exit transition if it exists. After this
+ * method, this session object can no longer be used. Clients need to create a new session
+ * object if they want to launch another intent with origin transition.
+ */
+ public void cancel() {
+ final int lastState = mState.getAndSet(CANCELLED);
+ if (lastState == CANCELLED || lastState == NOT_STARTED) {
+ return;
+ }
+ logD("cancel: cancelled transition. Last state: " + stateToString(lastState));
+ if (mOriginTransition != null) {
+ try {
+ mOriginTransitions.cancelOriginTransition(mOriginTransition);
+ mOriginTransition = null;
+ } catch (RemoteException e) {
+ logE("Unable to cancel origin transition!", e);
+ }
+ }
+ }
+
+ private boolean hasEntryTransition() {
+ return mEntryTransition != null;
+ }
+
+ private boolean hasExitTransition() {
+ return mOriginTransitions != null && mExitTransition != null;
+ }
+
+ private void logD(String msg) {
+ if (DEBUG) {
+ Log.d(TAG, "Session[" + mName + "] - " + msg);
+ }
+ }
+
+ private void logE(String msg) {
+ Log.e(TAG, "Session[" + mName + "] - " + msg);
+ }
+
+ private void logE(String msg, Throwable e) {
+ Log.e(TAG, "Session[" + mName + "] - " + msg, e);
+ }
+
+ private static String stateToString(@State int state) {
+ switch (state) {
+ case NOT_STARTED:
+ return "NOT_STARTED";
+ case STARTED:
+ return "STARTED";
+ case CANCELLED:
+ return "CANCELLED";
+ default:
+ return "UNKNOWN(" + state + ")";
+ }
+ }
+
+ /** A builder to build a {@link OriginTransitionSession}. */
+ public static class Builder {
+ private final Context mContext;
+ @Nullable private final IOriginTransitions mOriginTransitions;
+ @Nullable private Supplier<IRemoteTransition> mEntryTransitionSupplier;
+ @Nullable private Supplier<IRemoteTransition> mExitTransitionSupplier;
+ private String mName;
+ @Nullable private Predicate<RemoteTransition> mIntentStarter;
+
+ /** Create a builder that only supports entry transition. */
+ public Builder(Context context) {
+ this(context, /* originTransitions= */ null);
+ }
+
+ /** Create a builder that supports both entry and return transition. */
+ public Builder(Context context, @Nullable IOriginTransitions originTransitions) {
+ mContext = context;
+ mOriginTransitions = originTransitions;
+ mName = context.getPackageName();
+ }
+
+ /** Specify a name that is used in logging. */
+ public Builder withName(String name) {
+ mName = name;
+ return this;
+ }
+
+ /** Specify an intent that will be launched when the session started. */
+ public Builder withIntent(Intent intent) {
+ return withIntentStarter(
+ transition -> {
+ mContext.startActivity(
+ intent, createDefaultActivityOptions(transition).toBundle());
+ return true;
+ });
+ }
+
+ /** Specify a pending intent that will be launched when the session started. */
+ public Builder withPendingIntent(PendingIntent pendingIntent) {
+ return withIntentStarter(
+ transition -> {
+ try {
+ pendingIntent.send(createDefaultActivityOptions(transition).toBundle());
+ return true;
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "Failed to launch pending intent!", e);
+ return false;
+ }
+ });
+ }
+
+ private static ActivityOptions createDefaultActivityOptions(
+ @Nullable RemoteTransition transition) {
+ ActivityOptions options =
+ transition == null
+ ? ActivityOptions.makeBasic()
+ : ActivityOptions.makeRemoteTransition(transition);
+ LaunchCookie cookie = new LaunchCookie();
+ options.setLaunchCookie(cookie);
+ return options;
+ }
+
+ /**
+ * Specify an intent starter function that will be called to start an activity. The function
+ * accepts an optional {@link RemoteTransition} object which can be used to create an {@link
+ * ActivityOptions} for the activity launch. The function can also return a {@code false}
+ * result to cancel the session.
+ *
+ * <p>Note: it's encouraged to use {@link #withIntent(Intent)} or {@link
+ * #withPendingIntent(PendingIntent)} instead of this method. Use it only if the default
+ * activity launch code doesn't satisfy your requirement.
+ */
+ public Builder withIntentStarter(Predicate<RemoteTransition> intentStarter) {
+ mIntentStarter = intentStarter;
+ return this;
+ }
+
+ /** Add an entry transition to the builder. */
+ public Builder withEntryTransition(IRemoteTransition transition) {
+ mEntryTransitionSupplier = () -> transition;
+ return this;
+ }
+
+ /** Add an exit transition to the builder. */
+ public Builder withExitTransition(IRemoteTransition transition) {
+ mExitTransitionSupplier = () -> transition;
+ return this;
+ }
+
+ /** Build an {@link OriginTransitionSession}. */
+ public OriginTransitionSession build() {
+ if (mIntentStarter == null) {
+ throw new IllegalArgumentException("No intent, pending intent, or intent starter!");
+ }
+ return new OriginTransitionSession(
+ mName,
+ mOriginTransitions,
+ mIntentStarter,
+ mEntryTransitionSupplier == null ? null : mEntryTransitionSupplier.get(),
+ mExitTransitionSupplier == null ? null : mExitTransitionSupplier.get());
+ }
+ }
+}
diff --git a/packages/SystemUI/animation/lib/tests/Android.bp b/packages/SystemUI/animation/lib/tests/Android.bp
new file mode 100644
index 0000000..c1a3e84
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/Android.bp
@@ -0,0 +1,47 @@
+// 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 {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
+ default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_test {
+ name: "PlatformAnimationLibCoreTests",
+
+ defaults: [
+ "platform_app_defaults",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+
+ dxflags: ["--multi-dex"],
+ platform_apis: true,
+ test_suites: ["device-tests"],
+ static_libs: [
+ "PlatformAnimationLib-core",
+ "platform-test-rules",
+ "testables",
+ ],
+ compile_multilib: "both",
+ libs: [
+ "android.test.runner.stubs.system",
+ "android.test.base.stubs.system",
+ ],
+
+ certificate: "platform",
+
+ manifest: "AndroidManifest.xml",
+}
diff --git a/packages/SystemUI/animation/lib/tests/AndroidManifest.xml b/packages/SystemUI/animation/lib/tests/AndroidManifest.xml
new file mode 100644
index 0000000..1788abf
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:sharedUserId="android.uid.system"
+ package="com.android.systemui.animation.core.tests" >
+
+ <application android:debuggable="true" android:testOnly="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="android.testing.TestableInstrumentation"
+ android:targetPackage="com.android.systemui.animation.core.tests"
+ android:label="Tests for PlatformAnimationLib-core" />
+</manifest>
diff --git a/packages/SystemUI/animation/lib/tests/AndroidTest.xml b/packages/SystemUI/animation/lib/tests/AndroidTest.xml
new file mode 100644
index 0000000..0f37d7a
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/AndroidTest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<configuration description="Runs Tests for PlatformAnimationLib-core.">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="PlatformAnimationLibCoreTests.apk" />
+ <option name="install-arg" value="-t" />
+ </target_preparer>
+
+ <!-- Among other reasons, root access is needed for screen recording artifacts. -->
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
+ <option name="force-root" value="true" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.systemui.animation.core.tests" />
+ <option name="runner" value="android.testing.TestableInstrumentation" />
+ <option name="test-filter-dir" value="/data/data/com.android.systemui.animation.core.tests" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys" value="/data/user/0/com.android.systemui.animation.core.tests/files" />
+ <option name="collect-on-run-ended-only" value="false" />
+ </metrics_collector>
+</configuration>
diff --git a/packages/SystemUI/animation/lib/tests/src/com/android/systemui/animation/OriginTransitionSessionTest.java b/packages/SystemUI/animation/lib/tests/src/com/android/systemui/animation/OriginTransitionSessionTest.java
new file mode 100644
index 0000000..287e53b
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/src/com/android/systemui/animation/OriginTransitionSessionTest.java
@@ -0,0 +1,406 @@
+/*
+ * 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.animation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.Nullable;
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
+import android.window.TransitionInfo;
+import android.window.WindowAnimationState;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.systemui.animation.shared.IOriginTransitions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Map;
+import java.util.function.Predicate;
+
+/** Unit tests for {@link OriginTransitionSession}. */
+@SmallTest
+@RunWith(JUnit4.class)
+public final class OriginTransitionSessionTest {
+ private static final ComponentName TEST_ACTIVITY_1 = new ComponentName("test", "Activity1");
+ private static final ComponentName TEST_ACTIVITY_2 = new ComponentName("test", "Activity2");
+ private static final ComponentName TEST_ACTIVITY_3 = new ComponentName("test", "Activity3");
+
+ private FakeIOriginTransitions mIOriginTransitions;
+ private Instrumentation mInstrumentation;
+ private FakeIntentStarter mIntentStarter;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mContext = mInstrumentation.getTargetContext();
+ mIOriginTransitions = new FakeIOriginTransitions();
+ mIntentStarter = new FakeIntentStarter(TEST_ACTIVITY_1, TEST_ACTIVITY_2);
+ }
+
+ @Test
+ public void sessionStart_withEntryAndExitTransition_transitionsPlayed() {
+ FakeRemoteTransition entry = new FakeRemoteTransition();
+ FakeRemoteTransition exit = new FakeRemoteTransition();
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .withEntryTransition(entry)
+ .withExitTransition(exit)
+ .build();
+
+ session.start();
+
+ assertThat(mIntentStarter.hasLaunched()).isTrue();
+ assertThat(entry.started()).isTrue();
+
+ runReturnTransition(mIntentStarter);
+
+ assertThat(exit.started()).isTrue();
+ }
+
+ @Test
+ public void sessionStart_withEntryTransition_transitionPlayed() {
+ FakeRemoteTransition entry = new FakeRemoteTransition();
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .withEntryTransition(entry)
+ .build();
+
+ session.start();
+
+ assertThat(mIntentStarter.hasLaunched()).isTrue();
+ assertThat(entry.started()).isTrue();
+ assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+ }
+
+ @Test
+ public void sessionStart_withoutTransition_launchedIntent() {
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .build();
+
+ session.start();
+
+ assertThat(mIntentStarter.hasLaunched()).isTrue();
+ assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+ }
+
+ @Test
+ public void sessionStart_cancelledByIntentStarter_transitionNotPlayed() {
+ FakeRemoteTransition entry = new FakeRemoteTransition();
+ FakeRemoteTransition exit = new FakeRemoteTransition();
+ mIntentStarter =
+ new FakeIntentStarter(TEST_ACTIVITY_1, TEST_ACTIVITY_2, /* result= */ false);
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .withEntryTransition(entry)
+ .withExitTransition(exit)
+ .build();
+
+ session.start();
+
+ assertThat(mIntentStarter.hasLaunched()).isFalse();
+ assertThat(entry.started()).isFalse();
+ assertThat(exit.started()).isFalse();
+ assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+ }
+
+ @Test
+ public void sessionStart_alreadyStarted_noOp() {
+ FakeRemoteTransition entry = new FakeRemoteTransition();
+ FakeRemoteTransition exit = new FakeRemoteTransition();
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .withEntryTransition(entry)
+ .withExitTransition(exit)
+ .build();
+ session.start();
+ entry.reset();
+ mIntentStarter.reset();
+
+ session.start();
+
+ assertThat(mIntentStarter.hasLaunched()).isFalse();
+ assertThat(entry.started()).isFalse();
+ }
+
+ @Test
+ public void sessionStart_alreadyCancelled_noOp() {
+ FakeRemoteTransition entry = new FakeRemoteTransition();
+ FakeRemoteTransition exit = new FakeRemoteTransition();
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .withEntryTransition(entry)
+ .withExitTransition(exit)
+ .build();
+ session.cancel();
+
+ session.start();
+
+ assertThat(mIntentStarter.hasLaunched()).isFalse();
+ assertThat(entry.started()).isFalse();
+ assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+ }
+
+ @Test
+ public void sessionCancelled_returnTransitionNotPlayed() {
+ FakeRemoteTransition entry = new FakeRemoteTransition();
+ FakeRemoteTransition exit = new FakeRemoteTransition();
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .withEntryTransition(entry)
+ .withExitTransition(exit)
+ .build();
+
+ session.start();
+ session.cancel();
+
+ assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+ }
+
+ @Test
+ public void multipleSessionsStarted_allTransitionsPlayed() {
+ FakeRemoteTransition entry1 = new FakeRemoteTransition();
+ FakeRemoteTransition exit1 = new FakeRemoteTransition();
+ FakeIntentStarter starter1 = mIntentStarter;
+ OriginTransitionSession session1 =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(starter1)
+ .withEntryTransition(entry1)
+ .withExitTransition(exit1)
+ .build();
+ FakeRemoteTransition entry2 = new FakeRemoteTransition();
+ FakeRemoteTransition exit2 = new FakeRemoteTransition();
+ FakeIntentStarter starter2 = new FakeIntentStarter(TEST_ACTIVITY_2, TEST_ACTIVITY_3);
+ OriginTransitionSession session2 =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(starter2)
+ .withEntryTransition(entry2)
+ .withExitTransition(exit2)
+ .build();
+
+ session1.start();
+
+ assertThat(starter1.hasLaunched()).isTrue();
+ assertThat(entry1.started()).isTrue();
+
+ session2.start();
+
+ assertThat(starter2.hasLaunched()).isTrue();
+ assertThat(entry2.started()).isTrue();
+
+ runReturnTransition(starter2);
+
+ assertThat(exit2.started()).isTrue();
+
+ runReturnTransition(starter1);
+
+ assertThat(exit1.started()).isTrue();
+ }
+
+ private void runReturnTransition(FakeIntentStarter intentStarter) {
+ TransitionInfo info =
+ buildTransitionInfo(intentStarter.getToActivity(), intentStarter.getFromActivity());
+ mIOriginTransitions.runReturnTransition(intentStarter.getTransitionOfLastLaunch(), info);
+ }
+
+ private static TransitionInfo buildTransitionInfo(ComponentName from, ComponentName to) {
+ TransitionInfo info = new TransitionInfo(WindowManager.TRANSIT_OPEN, /* flags= */ 0);
+ TransitionInfo.Change c1 =
+ new TransitionInfo.Change(/* container= */ null, /* leash= */ null);
+ c1.setMode(WindowManager.TRANSIT_OPEN);
+ c1.setActivityComponent(to);
+ TransitionInfo.Change c2 =
+ new TransitionInfo.Change(/* container= */ null, /* leash= */ null);
+ c2.setMode(WindowManager.TRANSIT_CLOSE);
+ c2.setActivityComponent(from);
+ info.addChange(c2);
+ info.addChange(c1);
+ return info;
+ }
+
+ private static class FakeIntentStarter implements Predicate<RemoteTransition> {
+ private final ComponentName mFromActivity;
+ private final ComponentName mToActivity;
+ private final boolean mResult;
+
+ @Nullable private RemoteTransition mTransition;
+ private boolean mLaunched;
+
+ FakeIntentStarter(ComponentName from, ComponentName to) {
+ this(from, to, /* result= */ true);
+ }
+
+ FakeIntentStarter(ComponentName from, ComponentName to, boolean result) {
+ mFromActivity = from;
+ mToActivity = to;
+ mResult = result;
+ }
+
+ @Override
+ public boolean test(RemoteTransition transition) {
+ if (mResult) {
+ mLaunched = true;
+ mTransition = transition;
+ if (mTransition != null) {
+ TransitionInfo info = buildTransitionInfo(mFromActivity, mToActivity);
+ try {
+ transition
+ .getRemoteTransition()
+ .startAnimation(
+ new Binder(),
+ info,
+ new SurfaceControl.Transaction(),
+ new FakeFinishCallback());
+ } catch (RemoteException e) {
+
+ }
+ }
+ }
+ return mResult;
+ }
+
+ @Nullable
+ public RemoteTransition getTransitionOfLastLaunch() {
+ return mTransition;
+ }
+
+ public ComponentName getFromActivity() {
+ return mFromActivity;
+ }
+
+ public ComponentName getToActivity() {
+ return mToActivity;
+ }
+
+ public boolean hasLaunched() {
+ return mLaunched;
+ }
+
+ public void reset() {
+ mTransition = null;
+ mLaunched = false;
+ }
+ }
+
+ private static class FakeIOriginTransitions extends IOriginTransitions.Stub {
+ private final Map<RemoteTransition, RemoteTransition> mRecords = new ArrayMap<>();
+
+ @Override
+ public RemoteTransition makeOriginTransition(
+ RemoteTransition launchTransition, RemoteTransition returnTransition) {
+ mRecords.put(launchTransition, returnTransition);
+ return launchTransition;
+ }
+
+ @Override
+ public void cancelOriginTransition(RemoteTransition originTransition) {
+ mRecords.remove(originTransition);
+ }
+
+ public void runReturnTransition(RemoteTransition originTransition, TransitionInfo info) {
+ RemoteTransition transition = mRecords.remove(originTransition);
+ try {
+ transition
+ .getRemoteTransition()
+ .startAnimation(
+ new Binder(),
+ info,
+ new SurfaceControl.Transaction(),
+ new FakeFinishCallback());
+ } catch (RemoteException e) {
+
+ }
+ }
+
+ public boolean hasPendingReturnTransitions() {
+ return !mRecords.isEmpty();
+ }
+ }
+
+ private static class FakeFinishCallback extends IRemoteTransitionFinishedCallback.Stub {
+ @Override
+ public void onTransitionFinished(
+ WindowContainerTransaction wct, SurfaceControl.Transaction sct) {}
+ }
+
+ private static class FakeRemoteTransition extends IRemoteTransition.Stub {
+ private boolean mStarted;
+
+ @Override
+ public void startAnimation(
+ IBinder token,
+ TransitionInfo info,
+ SurfaceControl.Transaction t,
+ IRemoteTransitionFinishedCallback finishCallback)
+ throws RemoteException {
+ mStarted = true;
+ finishCallback.onTransitionFinished(null, null);
+ }
+
+ @Override
+ public void mergeAnimation(
+ IBinder transition,
+ TransitionInfo info,
+ SurfaceControl.Transaction t,
+ IBinder mergeTarget,
+ IRemoteTransitionFinishedCallback finishCallback) {}
+
+ @Override
+ public void takeOverAnimation(
+ IBinder transition,
+ TransitionInfo info,
+ SurfaceControl.Transaction t,
+ IRemoteTransitionFinishedCallback finishCallback,
+ WindowAnimationState[] states) {}
+
+ @Override
+ public void onTransitionConsumed(IBinder transition, boolean aborted) {}
+
+ public boolean started() {
+ return mStarted;
+ }
+
+ public void reset() {
+ mStarted = false;
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index df101c5..dc9e267 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -126,17 +126,18 @@
awaitFirstDown(false)
viewModel.onSceneContainerUserInputStarted()
}
- },
+ }
) {
SceneTransitionLayout(
state = state,
modifier = modifier.fillMaxSize(),
swipeSourceDetector = viewModel.edgeDetector,
+ gestureFilter = viewModel::shouldFilterGesture,
) {
sceneByKey.forEach { (sceneKey, scene) ->
scene(
key = sceneKey,
- userActions = userActionsByContentKey.getOrDefault(sceneKey, emptyMap())
+ userActions = userActionsByContentKey.getOrDefault(sceneKey, emptyMap()),
) {
// Activate the scene.
LaunchedEffect(scene) { scene.activate() }
@@ -144,7 +145,7 @@
// Render the scene.
with(scene) {
this@scene.Content(
- modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize(),
+ modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize()
)
}
}
@@ -152,7 +153,7 @@
overlayByKey.forEach { (overlayKey, overlay) ->
overlay(
key = overlayKey,
- userActions = userActionsByContentKey.getOrDefault(overlayKey, emptyMap())
+ userActions = userActionsByContentKey.getOrDefault(overlayKey, emptyMap()),
) {
// Activate the overlay.
LaunchedEffect(overlay) { overlay.activate() }
@@ -164,12 +165,7 @@
}
BottomRightCornerRibbon(
- content = {
- Text(
- text = "flexi\uD83E\uDD43",
- color = Color.White,
- )
- },
+ content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) },
modifier = Modifier.align(Alignment.BottomEnd),
)
}
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 f38a310..9891025 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
@@ -123,6 +123,10 @@
overSlop: Float,
pointersDown: Int,
): DragController {
+ if (startedPosition != null && layoutImpl.gestureFilter(startedPosition)) {
+ return NoOpDragController
+ }
+
if (overSlop == 0f) {
val oldDragController = dragController
check(oldDragController != null && oldDragController.isDrivingTransition) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index cec8883..6e89814 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -47,6 +47,9 @@
* @param state the state of this layout.
* @param swipeSourceDetector the edge detector used to detect which edge a swipe is started from,
* if any.
+ * @param gestureFilter decides whether a drag gesture that started at the given start position
+ * should be filtered. If the lambda returns `true`, the drag gesture will be ignored. If it
+ * returns `false`, the drag gesture will be handled.
* @param transitionInterceptionThreshold used during a scene transition. For the scene to be
* intercepted, the progress value must be above the threshold, and below (1 - threshold).
* @param builder the configuration of the different scenes and overlays of this layout.
@@ -57,6 +60,7 @@
modifier: Modifier = Modifier,
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
+ gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
builder: SceneTransitionLayoutScope.() -> Unit,
) {
@@ -65,6 +69,7 @@
modifier,
swipeSourceDetector,
swipeDetector,
+ gestureFilter,
transitionInterceptionThreshold,
onLayoutImpl = null,
builder,
@@ -616,6 +621,7 @@
modifier: Modifier = Modifier,
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
+ gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
transitionInterceptionThreshold: Float = 0f,
onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
builder: SceneTransitionLayoutScope.() -> Unit,
@@ -632,6 +638,7 @@
transitionInterceptionThreshold = transitionInterceptionThreshold,
builder = builder,
animationScope = animationScope,
+ gestureFilter = gestureFilter,
)
.also { onLayoutImpl?.invoke(it) }
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 65c4043..9e7be37 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -31,6 +31,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.ApproachLayoutModifierNode
import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LookaheadScope
@@ -70,6 +71,7 @@
* animations.
*/
internal val animationScope: CoroutineScope,
+ internal val gestureFilter: (startedPosition: Offset) -> Boolean,
) {
/**
* The map of [Scene]s.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
index 54ee783..f758102 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene
import androidx.compose.runtime.Stable
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
/** {@link SwipeDetector} helps determine whether a swipe gestured has occurred. */
@@ -31,6 +32,8 @@
val DefaultSwipeDetector = PassthroughSwipeDetector()
+val DefaultGestureFilter = { _: Offset -> false }
+
/** An {@link SwipeDetector} implementation that recognizes a swipe on any input. */
class PassthroughSwipeDetector : SwipeDetector {
override fun detectSwipe(change: PointerInputChange): Boolean {
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 fca92ca..b64b8be 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
@@ -108,6 +108,8 @@
val transitionInterceptionThreshold = 0.05f
+ var gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter
+
private val layoutImpl =
SceneTransitionLayoutImpl(
state = layoutState,
@@ -120,6 +122,7 @@
// Use testScope and not backgroundScope here because backgroundScope does not
// work well with advanceUntilIdle(), which is used by some tests.
animationScope = testScope,
+ gestureFilter = { startedPosition -> gestureFilter.invoke(startedPosition) },
)
.apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) }
@@ -317,6 +320,13 @@
}
@Test
+ fun onDragStarted_doesNotStartTransition_whenGestureFiltered() = runGestureTest {
+ gestureFilter = { _ -> true }
+ onDragStarted(overSlop = down(fractionOfScreen = 0.1f), expectedConsumedOverSlop = 0f)
+ assertIdle(currentScene = SceneA)
+ }
+
+ @Test
fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
assertTransition(currentScene = SceneA)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilterTest.kt
new file mode 100644
index 0000000..efde1ec
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilterTest.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.ui.viewmodel
+
+import android.graphics.Region
+import android.view.setSystemGestureExclusionRegion
+import androidx.compose.ui.geometry.Offset
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.scene.sceneContainerGestureFilterFactory
+import com.android.systemui.settings.displayTracker
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SceneContainerGestureFilterTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val displayId = kosmos.displayTracker.defaultDisplayId
+
+ private val underTest = kosmos.sceneContainerGestureFilterFactory.create(displayId)
+ private val activationJob = Job()
+
+ @Test
+ fun shouldFilterGesture_whenNoRegion_returnsFalse() =
+ testScope.runTest {
+ activate()
+ setSystemGestureExclusionRegion(displayId, null)
+ runCurrent()
+
+ assertThat(underTest.shouldFilterGesture(Offset(100f, 100f))).isFalse()
+ }
+
+ @Test
+ fun shouldFilterGesture_whenOutsideRegion_returnsFalse() =
+ testScope.runTest {
+ activate()
+ setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
+ runCurrent()
+
+ assertThat(underTest.shouldFilterGesture(Offset(300f, 100f))).isFalse()
+ }
+
+ @Test
+ fun shouldFilterGesture_whenInsideRegion_returnsTrue() =
+ testScope.runTest {
+ activate()
+ setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
+ runCurrent()
+
+ assertThat(underTest.shouldFilterGesture(Offset(100f, 100f))).isTrue()
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun shouldFilterGesture_beforeActivation_throws() =
+ testScope.runTest {
+ setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
+ runCurrent()
+
+ underTest.shouldFilterGesture(Offset(100f, 100f))
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun shouldFilterGesture_afterCancellation_throws() =
+ testScope.runTest {
+ activate()
+ setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
+ runCurrent()
+
+ cancel()
+
+ underTest.shouldFilterGesture(Offset(100f, 100f))
+ }
+
+ private fun TestScope.activate() {
+ underTest.activateIn(testScope, activationJob)
+ runCurrent()
+ }
+
+ private fun TestScope.cancel() {
+ activationJob.cancel()
+ runCurrent()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index a0bb017..e60e742 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -36,10 +36,12 @@
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.fakeOverlaysByKeys
import com.android.systemui.scene.sceneContainerConfig
+import com.android.systemui.scene.sceneContainerGestureFilterFactory
import com.android.systemui.scene.shared.logger.sceneLogger
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.settings.displayTracker
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.shared.flag.DualShade
@@ -86,6 +88,8 @@
shadeInteractor = kosmos.shadeInteractor,
splitEdgeDetector = kosmos.splitEdgeDetector,
logger = kosmos.sceneLogger,
+ gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
+ displayId = kosmos.displayTracker.defaultDisplayId,
motionEventHandlerReceiver = { motionEventHandler ->
this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler
},
@@ -283,10 +287,7 @@
fakeSceneDataSource.showOverlay(Overlays.NotificationsShade)
assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
assertThat(currentOverlays)
- .containsExactly(
- Overlays.QuickSettingsShade,
- Overlays.NotificationsShade,
- )
+ .containsExactly(Overlays.QuickSettingsShade, Overlays.NotificationsShade)
val actionableContentKey =
underTest.getActionableContentKey(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
new file mode 100644
index 0000000..f9b7769
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel
+
+import android.app.Flags
+import android.app.NotificationManager.Policy
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
+import android.provider.Settings
+import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
+import androidx.test.filters.SmallTest
+import com.android.settingslib.notification.data.repository.updateNotificationPolicy
+import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(ParameterizedAndroidJunit4::class)
+@SmallTest
+@EnableFlags(FooterViewRefactor.FLAG_NAME)
+class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val zenModeRepository = kosmos.zenModeRepository
+ private val activeNotificationListRepository = kosmos.activeNotificationListRepository
+
+ private val underTest = kosmos.emptyShadeViewModel
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
+ @Test
+ fun areNotificationsHiddenInShade_true() =
+ testScope.runTest {
+ val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+ zenModeRepository.updateNotificationPolicy(
+ suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ )
+ zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+ runCurrent()
+
+ assertThat(hidden).isTrue()
+ }
+
+ @Test
+ fun areNotificationsHiddenInShade_false() =
+ testScope.runTest {
+ val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+ zenModeRepository.updateNotificationPolicy(
+ suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ )
+ zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
+ runCurrent()
+
+ assertThat(hidden).isFalse()
+ }
+
+ @Test
+ fun hasFilteredOutSeenNotifications_true() =
+ testScope.runTest {
+ val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
+ runCurrent()
+
+ assertThat(hasFilteredNotifs).isTrue()
+ }
+
+ @Test
+ fun hasFilteredOutSeenNotifications_false() =
+ testScope.runTest {
+ val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+ runCurrent()
+
+ assertThat(hasFilteredNotifs).isFalse()
+ }
+
+ @Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+ @DisableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ fun text_changesWhenNotifsHiddenInShade() =
+ testScope.runTest {
+ val text by collectLastValue(underTest.text)
+
+ zenModeRepository.updateNotificationPolicy(
+ suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ )
+ zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
+ runCurrent()
+
+ assertThat(text).isEqualTo("No notifications")
+
+ zenModeRepository.updateNotificationPolicy(
+ suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ )
+ zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+ runCurrent()
+
+ assertThat(text).isEqualTo("Notifications paused by Do Not Disturb")
+ }
+
+ @Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ fun text_reflectsModesHidingNotifications() =
+ testScope.runTest {
+ val text by collectLastValue(underTest.text)
+
+ assertThat(text).isEqualTo("No notifications")
+
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setId("Do not disturb")
+ .setName("Do not disturb")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build()
+ )
+ runCurrent()
+ assertThat(text).isEqualTo("Notifications paused by Do not disturb")
+
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setId("Work")
+ .setName("Work")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build()
+ )
+ runCurrent()
+ assertThat(text).isEqualTo("Notifications paused by Do not disturb and one other mode")
+
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setId("Gym")
+ .setName("Gym")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build()
+ )
+ runCurrent()
+ assertThat(text).isEqualTo("Notifications paused by Do not disturb and 2 other modes")
+
+ zenModeRepository.deactivateMode("Do not disturb")
+ zenModeRepository.deactivateMode("Work")
+ runCurrent()
+ assertThat(text).isEqualTo("Notifications paused by Gym")
+ }
+
+ @Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+ fun footer_isVisibleWhenSeenNotifsAreFilteredOut() =
+ testScope.runTest {
+ val footerVisible by collectLastValue(underTest.footer.isVisible)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+ runCurrent()
+
+ assertThat(footerVisible).isFalse()
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
+ runCurrent()
+
+ assertThat(footerVisible).isTrue()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 26e1a4d..d12d6f6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -18,12 +18,9 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
-import android.app.NotificationManager.Policy
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
-import android.provider.Settings
import androidx.test.filters.SmallTest
-import com.android.settingslib.notification.data.repository.updateNotificationPolicy
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.DisableSceneContainer
@@ -46,7 +43,6 @@
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
-import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
import com.android.systemui.statusbar.policy.fakeConfigurationController
import com.android.systemui.testKosmos
import com.android.systemui.util.ui.isAnimating
@@ -79,7 +75,6 @@
private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository
private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository
private val headsUpRepository = kosmos.headsUpNotificationRepository
- private val zenModeRepository = kosmos.zenModeRepository
private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
@@ -266,56 +261,6 @@
}
@Test
- fun areNotificationsHiddenInShade_true() =
- testScope.runTest {
- val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
-
- zenModeRepository.updateNotificationPolicy(
- suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
- )
- zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
- runCurrent()
-
- assertThat(hidden).isTrue()
- }
-
- @Test
- fun areNotificationsHiddenInShade_false() =
- testScope.runTest {
- val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
-
- zenModeRepository.updateNotificationPolicy(
- suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
- )
- zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
- runCurrent()
-
- assertThat(hidden).isFalse()
- }
-
- @Test
- fun hasFilteredOutSeenNotifications_true() =
- testScope.runTest {
- val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
-
- activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
- runCurrent()
-
- assertThat(hasFilteredNotifs).isTrue()
- }
-
- @Test
- fun hasFilteredOutSeenNotifications_false() =
- testScope.runTest {
- val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
-
- activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
- runCurrent()
-
- assertThat(hasFilteredNotifs).isFalse()
- }
-
- @Test
fun shouldIncludeFooterView_trueWhenShade() =
testScope.runTest {
val shouldIncludeFooterView by collectFooterViewVisibility()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
index 2c8cc1a..3053672 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
@@ -47,7 +47,6 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.time.SystemClock
-import com.google.common.truth.Truth.assertThat
import junit.framework.Assert
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -248,35 +247,32 @@
@Test
@EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testShowNotification_removeWhenReorderingAllowedTrue() {
- whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
- val hmp = createHeadsUpManagerPhone()
-
- val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- hmp.showNotification(notifEntry)
- assertThat(hmp.mEntriesToRemoveWhenReorderingAllowed.contains(notifEntry)).isTrue();
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testShowNotification_reorderNotAllowed_seenInShadeTrue() {
+ fun testShowNotification_reorderNotAllowed_notPulsing_seenInShadeTrue() {
whenever(mVSProvider.isReorderingAllowed).thenReturn(false)
val hmp = createHeadsUpManagerPhone()
val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val row = mock<ExpandableNotificationRow>()
+ whenever(row.showingPulsing()).thenReturn(false)
+ notifEntry.row = row
+
hmp.showNotification(notifEntry)
- assertThat(notifEntry.isSeenInShade).isTrue();
+ Assert.assertTrue(notifEntry.isSeenInShade)
}
@Test
@EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testShowNotification_reorderAllowed_seenInShadeFalse() {
+ fun testShowNotification_reorderAllowed_notPulsing_seenInShadeFalse() {
whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
val hmp = createHeadsUpManagerPhone()
val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val row = mock<ExpandableNotificationRow>()
+ whenever(row.showingPulsing()).thenReturn(false)
+ notifEntry.row = row
+
hmp.showNotification(notifEntry)
- assertThat(notifEntry.isSeenInShade).isFalse();
+ Assert.assertFalse(notifEntry.isSeenInShade)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 0f6dc07..c5ccf9e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -25,6 +25,7 @@
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
import android.service.notification.SystemZenRules
+import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.R
@@ -34,6 +35,7 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
import com.android.systemui.testKosmos
@@ -379,4 +381,46 @@
assertThat(dndMode!!.isActive).isTrue()
}
+
+ @Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ fun modesHidingNotifications_onlyIncludesModesWithNotifListSuppression() =
+ testScope.runTest {
+ val modesHidingNotifications by collectLastValue(underTest.modesHidingNotifications)
+
+ zenModeRepository.addModes(
+ listOf(
+ TestModeBuilder()
+ .setName("Not active, no list suppression")
+ .setActive(false)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ true)
+ .build(),
+ TestModeBuilder()
+ .setName("Not active, has list suppression")
+ .setActive(false)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build(),
+ TestModeBuilder()
+ .setName("No list suppression")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ true)
+ .build(),
+ TestModeBuilder()
+ .setName("Has list suppression 1")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build(),
+ TestModeBuilder()
+ .setName("Has list suppression 2")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build(),
+ )
+ )
+ runCurrent()
+
+ assertThat(modesHidingNotifications?.map { it.name })
+ .containsExactly("Has list suppression 1", "Has list suppression 2")
+ .inOrder()
+ }
}
diff --git a/packages/SystemUI/res/layout/status_bar_no_notifications.xml b/packages/SystemUI/res/layout/status_bar_no_notifications.xml
index 248e611..e26b855 100644
--- a/packages/SystemUI/res/layout/status_bar_no_notifications.xml
+++ b/packages/SystemUI/res/layout/status_bar_no_notifications.xml
@@ -26,6 +26,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
+ android:paddingHorizontal="8dp"
>
<TextView
android:id="@+id/no_notifications"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 24b6579..75389b1 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1473,6 +1473,16 @@
<!-- The text to show in the notifications shade when dnd is suppressing notifications. [CHAR LIMIT=100] -->
<string name="dnd_suppressing_shade_text">Notifications paused by Do Not Disturb</string>
+ <!-- The text to show in the notifications shade when a mode is suppressing notifications. [CHAR LIMIT=100] -->
+ <string name="modes_suppressing_shade_text">
+ {count, plural, offset:1
+ =0 {No notifications}
+ =1 {Notifications paused by {mode}}
+ =2 {Notifications paused by {mode} and one other mode}
+ other {Notifications paused by {mode} and # other modes}
+ }
+ </string>
+
<!-- Media projection permission dialog action text. [CHAR LIMIT=60] -->
<string name="media_projection_action_text">Start now</string>
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 0b775ab..820c102 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -35,6 +35,8 @@
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
@@ -56,6 +58,8 @@
NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
NotificationMinimalismPrototype.token dependsOn NotificationThrottleHun.token
+ ModesEmptyShadeFix.token dependsOn FooterViewRefactor.token
+ ModesEmptyShadeFix.token dependsOn modesUi
// SceneContainer dependencies
SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 6a8cc17..4f3ea83 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -126,12 +126,12 @@
private val overlayColorActive =
Utils.applyAlpha(
/* alpha= */ 0.11f,
- Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive)
+ Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive),
)
private val overlayColorInactive =
Utils.applyAlpha(
/* alpha= */ 0.08f,
- Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive)
+ Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive),
)
private val colorLabelActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive)
@@ -188,10 +188,7 @@
private var lastState = INVALID
private var lastIconTint = 0
private val launchableViewDelegate =
- LaunchableViewDelegate(
- this,
- superSetVisibility = { super.setVisibility(it) },
- )
+ LaunchableViewDelegate(this, superSetVisibility = { super.setVisibility(it) })
private var lastDisabledByPolicy = false
private val locInScreen = IntArray(2)
@@ -418,7 +415,7 @@
initLongPressEffectCallback()
init(
{ _: View -> longPressEffect.onTileClick() },
- null, // Haptics and long-clicks will be handled by the [QSLongPressEffect]
+ { _: View -> true }, // Haptics and long-clicks are handled by [QSLongPressEffect]
)
} else {
val expandable = Expandable.fromView(this)
@@ -583,7 +580,7 @@
AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
resources.getString(
R.string.accessibility_tile_disabled_by_policy_action_description
- )
+ ),
)
)
} else {
@@ -591,7 +588,7 @@
info.addAction(
AccessibilityNodeInfo.AccessibilityAction(
AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
- resources.getString(R.string.accessibility_long_click_tile)
+ resources.getString(R.string.accessibility_long_click_tile),
)
)
}
@@ -716,35 +713,35 @@
state.spec,
state.state,
state.disabledByPolicy,
- getBackgroundColorForState(state.state, state.disabledByPolicy)
+ getBackgroundColorForState(state.state, state.disabledByPolicy),
)
if (allowAnimations) {
singleAnimator.setValues(
colorValuesHolder(
BACKGROUND_NAME,
backgroundColor,
- getBackgroundColorForState(state.state, state.disabledByPolicy)
+ getBackgroundColorForState(state.state, state.disabledByPolicy),
),
colorValuesHolder(
LABEL_NAME,
label.currentTextColor,
- getLabelColorForState(state.state, state.disabledByPolicy)
+ getLabelColorForState(state.state, state.disabledByPolicy),
),
colorValuesHolder(
SECONDARY_LABEL_NAME,
secondaryLabel.currentTextColor,
- getSecondaryLabelColorForState(state.state, state.disabledByPolicy)
+ getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
),
colorValuesHolder(
CHEVRON_NAME,
chevronView.imageTintList?.defaultColor ?: 0,
- getChevronColorForState(state.state, state.disabledByPolicy)
+ getChevronColorForState(state.state, state.disabledByPolicy),
),
colorValuesHolder(
OVERLAY_NAME,
backgroundOverlayColor,
- getOverlayColorForState(state.state)
- )
+ getOverlayColorForState(state.state),
+ ),
)
singleAnimator.start()
} else {
@@ -753,7 +750,7 @@
getLabelColorForState(state.state, state.disabledByPolicy),
getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
getChevronColorForState(state.state, state.disabledByPolicy),
- getOverlayColorForState(state.state)
+ getOverlayColorForState(state.state),
)
}
}
@@ -1077,7 +1074,7 @@
backgroundColor,
label.currentTextColor,
secondaryLabel.currentTextColor,
- chevronView.imageTintList?.defaultColor ?: 0
+ chevronView.imageTintList?.defaultColor ?: 0,
)
inner class StateChangeRunnable(private val state: QSTile.State) : Runnable {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepository.kt
new file mode 100644
index 0000000..a8d0777
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.scene.data.repository
+
+import android.graphics.Region
+import android.view.ISystemGestureExclusionListener
+import android.view.IWindowManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class SystemGestureExclusionRepository
+@Inject
+constructor(private val windowManager: IWindowManager) {
+
+ /**
+ * Returns [Flow] of the [Region] in which system gestures should be excluded on the display
+ * identified with [displayId].
+ */
+ fun exclusionRegion(displayId: Int): Flow<Region?> {
+ return conflatedCallbackFlow {
+ val listener =
+ object : ISystemGestureExclusionListener.Stub() {
+ override fun onSystemGestureExclusionChanged(
+ displayId: Int,
+ restrictedRegion: Region?,
+ unrestrictedRegion: Region?,
+ ) {
+ trySend(restrictedRegion)
+ }
+ }
+ windowManager.registerSystemGestureExclusionListener(listener, displayId)
+
+ awaitClose {
+ windowManager.unregisterSystemGestureExclusionListener(listener, displayId)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractor.kt
new file mode 100644
index 0000000..4cee874
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractor.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.scene.domain.interactor
+
+import android.graphics.Region
+import com.android.systemui.scene.data.repository.SystemGestureExclusionRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+class SystemGestureExclusionInteractor
+@Inject
+constructor(private val repository: SystemGestureExclusionRepository) {
+
+ /**
+ * Returns [Flow] of the [Region] in which system gestures should be excluded on the display
+ * identified with [displayId].
+ */
+ fun exclusionRegion(displayId: Int): Flow<Region?> {
+ return repository.exclusionRegion(displayId)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index 7f35d73..a7e7d8b 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -107,7 +107,9 @@
view.viewModel(
traceName = "SceneWindowRootViewBinder",
minWindowLifecycleState = WindowLifecycleState.ATTACHED,
- factory = { viewModelFactory.create(motionEventHandlerReceiver) },
+ factory = {
+ viewModelFactory.create(view.context.displayId, motionEventHandlerReceiver)
+ },
) { viewModel ->
try {
view.setViewTreeOnBackPressedDispatcherOwner(
@@ -184,7 +186,7 @@
PlatformTheme {
ScreenDecorProvider(
displayCutout = displayCutoutFromWindowInsets(scope, context, windowInsets),
- screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+ screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context),
) {
SceneContainer(
viewModel = viewModel,
@@ -205,9 +207,7 @@
): ComposeView {
return ComposeView(context).apply {
setContent {
- AlternateBouncer(
- alternateBouncerDependencies = alternateBouncerDependencies,
- )
+ AlternateBouncer(alternateBouncerDependencies = alternateBouncerDependencies)
}
}
}
@@ -234,14 +234,7 @@
else -> CutoutLocation.CENTER
}
val viewDisplayCutout = it?.displayCutout
- DisplayCutout(
- left,
- top,
- right,
- bottom,
- location,
- viewDisplayCutout,
- )
+ DisplayCutout(left, top, right, bottom, location, viewDisplayCutout)
}
.stateIn(scope, SharingStarted.WhileSubscribed(), DisplayCutout())
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilter.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilter.kt
new file mode 100644
index 0000000..a1d915a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilter.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.scene.ui.viewmodel
+
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.geometry.Offset
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
+import com.android.systemui.scene.domain.interactor.SystemGestureExclusionInteractor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlin.math.roundToInt
+
+/** Decides whether drag gestures should be filtered out in the scene container framework. */
+class SceneContainerGestureFilter
+@AssistedInject
+constructor(interactor: SystemGestureExclusionInteractor, @Assisted displayId: Int) :
+ ExclusiveActivatable() {
+
+ private val hydrator = Hydrator("SceneContainerGestureFilter.hydrator")
+ private val exclusionRegion by
+ hydrator.hydratedStateOf(
+ traceName = "exclusionRegion",
+ initialValue = null,
+ source = interactor.exclusionRegion(displayId),
+ )
+
+ override suspend fun onActivated(): Nothing {
+ hydrator.activate()
+ }
+
+ /**
+ * Returns `true` if a drag gesture starting at [startPosition] should be filtered out (e.g.
+ * ignored, `false` otherwise.
+ *
+ * Invoke this and pass in the position of the `ACTION_DOWN` pointer event that began the
+ * gesture.
+ */
+ fun shouldFilterGesture(startPosition: Offset): Boolean {
+ check(isActive) { "Must be activated to use!" }
+
+ return exclusionRegion?.contains(startPosition.x.roundToInt(), startPosition.y.roundToInt())
+ ?: false
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(displayId: Int): SceneContainerGestureFilter
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index af1f5a7..0bf2d49 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -18,6 +18,7 @@
import android.view.MotionEvent
import androidx.compose.runtime.getValue
+import androidx.compose.ui.geometry.Offset
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.compose.animation.scene.ObservableTransitionState
@@ -41,9 +42,12 @@
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
/** Models UI state for the scene container. */
class SceneContainerViewModel
@@ -55,6 +59,8 @@
shadeInteractor: ShadeInteractor,
private val splitEdgeDetector: SplitEdgeDetector,
private val logger: SceneLogger,
+ gestureFilterFactory: SceneContainerGestureFilter.Factory,
+ @Assisted displayId: Int,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
) : ExclusiveActivatable() {
@@ -80,6 +86,8 @@
},
)
+ private val gestureFilter: SceneContainerGestureFilter = gestureFilterFactory.create(displayId)
+
override suspend fun onActivated(): Nothing {
try {
// Sends a MotionEventHandler to the owner of the view-model so they can report
@@ -96,7 +104,11 @@
}
)
- hydrator.activate()
+ coroutineScope {
+ launch { hydrator.activate() }
+ launch { gestureFilter.activate() }
+ }
+ awaitCancellation()
} finally {
// Clears the previously-sent MotionEventHandler so the owner of the view-model releases
// their reference to it.
@@ -243,6 +255,17 @@
}
}
+ /**
+ * Returns `true` if a drag gesture starting at [startPosition] should be filtered out (e.g.
+ * ignored, `false` otherwise.
+ *
+ * Invoke this and pass in the position of the `ACTION_DOWN` pointer event that began the
+ * gesture.
+ */
+ fun shouldFilterGesture(startPosition: Offset): Boolean {
+ return gestureFilter.shouldFilterGesture(startPosition)
+ }
+
/** Defines interface for classes that can handle externally-reported [MotionEvent]s. */
interface MotionEventHandler {
/** Notifies that a [MotionEvent] has occurred. */
@@ -258,7 +281,8 @@
@AssistedFactory
interface Factory {
fun create(
- motionEventHandlerReceiver: (MotionEventHandler?) -> Unit
+ displayId: Int,
+ motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
): SceneContainerViewModel
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
index 6907eef..1c840e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
@@ -103,8 +103,7 @@
private boolean mTrackingHeadsUp;
private final HashSet<String> mSwipedOutKeys = new HashSet<>();
private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
- @VisibleForTesting
- public final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
+ private final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
= new ArraySet<>();
private boolean mIsShadeOrQsExpanded;
private boolean mIsQsExpanded;
@@ -428,7 +427,7 @@
for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
if (isHeadsUpEntry(entry.getKey())) {
// Maybe the heads-up was removed already
- removeEntry(entry.getKey(), "allowReorder");
+ removeEntry(entry.getKey(), "mOnReorderingAllowedListener");
}
}
mEntriesToRemoveWhenReorderingAllowed.clear();
@@ -631,8 +630,11 @@
super.setEntry(entry, removeRunnable);
if (NotificationThrottleHun.isEnabled()) {
- mEntriesToRemoveWhenReorderingAllowed.add(entry);
- if (!mVisualStabilityProvider.isReorderingAllowed()) {
+ if (!mVisualStabilityProvider.isReorderingAllowed()
+ // We don't want to allow reordering while pulsing, but headsup need to
+ // time out anyway
+ && !entry.showingPulsing()) {
+ mEntriesToRemoveWhenReorderingAllowed.add(entry);
entry.setSeenInShade(true);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
similarity index 60%
rename from packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
index ec3c7d0..0f93b5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 The Android Open Source Project
+ * 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.
@@ -13,37 +13,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.systemui.statusbar.notification
-package com.android.systemui.statusbar.notification;
-
-import android.content.Intent;
-import android.view.View;
-
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import android.content.Intent
+import android.view.View
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
/**
* Component responsible for handling actions on a notification which cause activites to start.
* (e.g. clicking on a notification, tapping on the settings icon in the notification guts)
*/
-public interface NotificationActivityStarter {
+interface NotificationActivityStarter {
/** Called when the user clicks on the notification bubble icon. */
- void onNotificationBubbleIconClicked(NotificationEntry entry);
+ fun onNotificationBubbleIconClicked(entry: NotificationEntry?)
/** Called when the user clicks on the surface of a notification. */
- void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row);
+ fun onNotificationClicked(entry: NotificationEntry?, row: ExpandableNotificationRow?)
/** Called when the user clicks on a button in the notification guts which fires an intent. */
- void startNotificationGutsIntent(Intent intent, int appUid,
- ExpandableNotificationRow row);
+ fun startNotificationGutsIntent(intent: Intent?, appUid: Int, row: ExpandableNotificationRow?)
- /** Called when the user clicks "Manage" or "History" in the Shade. */
- void startHistoryIntent(View view, boolean showHistory);
+ /**
+ * Called when the user clicks "Manage" or "History" in the Shade, or the "No notifications"
+ * text.
+ */
+ fun startHistoryIntent(view: View?, showHistory: Boolean)
/** Called when the user succeed to drop notification to proper target view. */
- void onDragSuccess(NotificationEntry entry);
+ fun onDragSuccess(entry: NotificationEntry?)
- default boolean isCollapsingToShowActivityOverLockscreen() {
- return false;
- }
+ val isCollapsingToShowActivityOverLockscreen: Boolean
+ get() = false
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
index 0bbde21..82ce31b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
@@ -25,7 +25,6 @@
import androidx.annotation.NonNull;
import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.Flags;
import com.android.systemui.media.controls.util.MediaFeatureFlag;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -61,8 +60,27 @@
return false;
}
- if (!Flags.notificationsBackgroundIcons()) {
- inflateOrUpdateIcons(entry);
+ switch (mIconsState.getOrDefault(entry, STATE_ICONS_UNINFLATED)) {
+ case STATE_ICONS_UNINFLATED:
+ try {
+ mIconManager.createIcons(entry);
+ mIconsState.put(entry, STATE_ICONS_INFLATED);
+ } catch (InflationException e) {
+ reportInflationError(entry, e);
+ mIconsState.put(entry, STATE_ICONS_ERROR);
+ }
+ break;
+ case STATE_ICONS_INFLATED:
+ try {
+ mIconManager.updateIcons(entry, /* usingCache = */ false);
+ } catch (InflationException e) {
+ reportInflationError(entry, e);
+ mIconsState.put(entry, STATE_ICONS_ERROR);
+ }
+ break;
+ case STATE_ICONS_ERROR:
+ // do nothing
+ break;
}
return true;
@@ -72,19 +90,7 @@
private final NotifCollectionListener mCollectionListener = new NotifCollectionListener() {
@Override
public void onEntryInit(@NonNull NotificationEntry entry) {
- // We default to STATE_ICONS_UNINFLATED anyway, so there's no need to initialize it.
- if (!Flags.notificationsBackgroundIcons()) {
- mIconsState.put(entry, STATE_ICONS_UNINFLATED);
- }
- }
-
- @Override
- public void onEntryAdded(@NonNull NotificationEntry entry) {
- if (Flags.notificationsBackgroundIcons()) {
- if (isMediaNotification(entry.getSbn())) {
- inflateOrUpdateIcons(entry);
- }
- }
+ mIconsState.put(entry, STATE_ICONS_UNINFLATED);
}
@Override
@@ -93,12 +99,6 @@
// The update may have fixed the inflation error, so give it another chance.
mIconsState.put(entry, STATE_ICONS_UNINFLATED);
}
-
- if (Flags.notificationsBackgroundIcons()) {
- if (isMediaNotification(entry.getSbn())) {
- inflateOrUpdateIcons(entry);
- }
- }
}
@Override
@@ -107,31 +107,6 @@
}
};
- private void inflateOrUpdateIcons(NotificationEntry entry) {
- switch (mIconsState.getOrDefault(entry, STATE_ICONS_UNINFLATED)) {
- case STATE_ICONS_UNINFLATED:
- try {
- mIconManager.createIcons(entry);
- mIconsState.put(entry, STATE_ICONS_INFLATED);
- } catch (InflationException e) {
- reportInflationError(entry, e);
- mIconsState.put(entry, STATE_ICONS_ERROR);
- }
- break;
- case STATE_ICONS_INFLATED:
- try {
- mIconManager.updateIcons(entry, /* usingCache = */ false);
- } catch (InflationException e) {
- reportInflationError(entry, e);
- mIconsState.put(entry, STATE_ICONS_ERROR);
- }
- break;
- case STATE_ICONS_ERROR:
- // do nothing
- break;
- }
- }
-
private void reportInflationError(NotificationEntry entry, Exception e) {
// This is the same logic as in PreparationCoordinator; it doesn't handle media
// notifications when the media feature is enabled since they aren't displayed in the shade,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt
new file mode 100644
index 0000000..f1fc275
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.emptyshade.shared
+
+import android.app.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the modes_ui_empty_shade flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object ModesEmptyShadeFix {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_MODES_UI_EMPTY_SHADE
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.modesUiEmptyShade()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is not enabled to ensure that the refactor author catches issues in testing.
+ * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
+ */
+ @JvmStatic
+ inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java
index 850e944..73477da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java
@@ -22,6 +22,7 @@
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
+import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
@@ -29,26 +30,71 @@
import androidx.annotation.NonNull;
+import com.android.systemui.animation.LaunchableView;
+import com.android.systemui.animation.LaunchableViewDelegate;
import com.android.systemui.res.R;
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
-public class EmptyShadeView extends StackScrollerDecorView {
+import kotlin.Unit;
+
+import java.util.Objects;
+
+public class EmptyShadeView extends StackScrollerDecorView implements LaunchableView {
private TextView mEmptyText;
private TextView mEmptyFooterText;
- private @StringRes int mText = R.string.empty_shade_text;
+ private @StringRes int mTextId = R.string.empty_shade_text;
+ private String mTextString;
- private @DrawableRes int mFooterIcon = R.drawable.ic_friction_lock_closed;
- private @StringRes int mFooterText = R.string.unlock_to_see_notif_text;
+ private @DrawableRes int mFooterIcon;
+ private @StringRes int mFooterText;
+ // This view is initially gone in the xml.
private @Visibility int mFooterVisibility = View.GONE;
private int mSize;
+ private LaunchableViewDelegate mLaunchableViewDelegate = new LaunchableViewDelegate(this,
+ visibility -> {
+ super.setVisibility(visibility);
+ return Unit.INSTANCE;
+ });
+
public EmptyShadeView(Context context, AttributeSet attrs) {
super(context, attrs);
mSize = getResources().getDimensionPixelSize(
R.dimen.notifications_unseen_footer_icon_size);
+ if (ModesEmptyShadeFix.isEnabled()) {
+ mTextString = getContext().getString(R.string.empty_shade_text);
+ } else {
+ // These will be set by the binder when appropriate if ModesEmptyShadeFix is on.
+ mFooterIcon = R.drawable.ic_friction_lock_closed;
+ mFooterText = R.string.unlock_to_see_notif_text;
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ mLaunchableViewDelegate.setVisibility(visibility);
+ }
+
+ @Override
+ public void setShouldBlockVisibilityChanges(boolean block) {
+ /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode();
+ mLaunchableViewDelegate.setShouldBlockVisibilityChanges(block);
+ }
+
+ @Override
+ public void onActivityLaunchAnimationEnd() {
+ /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode();
+ }
+
+ @Override
+ @NonNull
+ public Rect getPaddingForLaunchAnimation() {
+ /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode();
+ return new Rect();
}
@Override
@@ -56,7 +102,11 @@
super.onConfigurationChanged(newConfig);
mSize = getResources().getDimensionPixelSize(
R.dimen.notifications_unseen_footer_icon_size);
- mEmptyText.setText(mText);
+ if (ModesEmptyShadeFix.isEnabled()) {
+ mEmptyText.setText(mTextString);
+ } else {
+ mEmptyText.setText(mTextId);
+ }
mEmptyFooterText.setVisibility(mFooterVisibility);
setFooterText(mFooterText);
setFooterIcon(mFooterIcon);
@@ -72,25 +122,45 @@
return findViewById(R.id.no_notifications_footer);
}
+ /** Update view colors. */
public void setTextColors(@ColorInt int onSurface, @ColorInt int onSurfaceVariant) {
mEmptyText.setTextColor(onSurfaceVariant);
mEmptyFooterText.setTextColor(onSurface);
mEmptyFooterText.setCompoundDrawableTintList(ColorStateList.valueOf(onSurface));
}
+ /** Set the resource ID for the main text shown by the view. */
public void setText(@StringRes int text) {
- mText = text;
- mEmptyText.setText(mText);
+ ModesEmptyShadeFix.assertInLegacyMode();
+ mTextId = text;
+ mEmptyText.setText(mTextId);
}
+ /** Set the string for the main text shown by the view. */
+ public void setText(String text) {
+ if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode() || Objects.equals(mTextString, text)) {
+ return;
+ }
+ mTextString = text;
+ mEmptyText.setText(text);
+ }
+
+ /** Visibility for the footer (the additional icon+text shown below the main text). */
public void setFooterVisibility(@Visibility int visibility) {
+ if (ModesEmptyShadeFix.isEnabled() && mFooterVisibility == visibility) {
+ return; // nothing to change
+ }
mFooterVisibility = visibility;
setSecondaryVisible(/* visible = */ visibility == View.VISIBLE,
/* animate = */false,
/* onAnimationEnded = */ null);
}
+ /** Text resource ID for the footer (the additional icon+text shown below the main text). */
public void setFooterText(@StringRes int text) {
+ if (ModesEmptyShadeFix.isEnabled() && mFooterText == text) {
+ return; // nothing to change
+ }
mFooterText = text;
if (text != 0) {
mEmptyFooterText.setText(mFooterText);
@@ -99,7 +169,11 @@
}
}
+ /** Icon resource ID for the footer (the additional icon+text shown below the main text). */
public void setFooterIcon(@DrawableRes int icon) {
+ if (ModesEmptyShadeFix.isEnabled() && mFooterIcon == icon) {
+ return; // nothing to change
+ }
mFooterIcon = icon;
Drawable drawable;
if (icon == 0) {
@@ -111,18 +185,24 @@
mEmptyFooterText.setCompoundDrawablesRelative(drawable, null, null, null);
}
+ /** Get resource ID for main text. */
@StringRes
public int getTextResource() {
- return mText;
+ ModesEmptyShadeFix.assertInLegacyMode();
+ return mTextId;
}
+ /** Get resource ID for footer text. */
@StringRes
public int getFooterTextResource() {
+ ModesEmptyShadeFix.assertInLegacyMode();
return mFooterText;
}
+ /** Get resource ID for footer icon. */
@DrawableRes
public int getFooterIconResource() {
+ ModesEmptyShadeFix.assertInLegacyMode();
return mFooterIcon;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt
new file mode 100644
index 0000000..102a11c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.emptyshade.ui.viewbinder
+
+import android.view.View
+import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+object EmptyShadeViewBinder {
+ suspend fun bind(
+ view: EmptyShadeView,
+ viewModel: EmptyShadeViewModel,
+ launchNotificationSettings: View.OnClickListener,
+ launchNotificationHistory: View.OnClickListener,
+ ) = coroutineScope {
+ launch { viewModel.text.collect { view.setText(it) } }
+
+ launch {
+ viewModel.tappingShouldLaunchHistory.collect { shouldLaunchHistory ->
+ if (shouldLaunchHistory) {
+ view.setOnClickListener(launchNotificationHistory)
+ } else {
+ view.setOnClickListener(launchNotificationSettings)
+ }
+ }
+ }
+
+ launch { bindFooter(view, viewModel) }
+ }
+
+ private suspend fun bindFooter(view: EmptyShadeView, viewModel: EmptyShadeViewModel) =
+ coroutineScope {
+ // Bind the resource IDs
+ view.setFooterText(viewModel.footer.messageId)
+ view.setFooterIcon(viewModel.footer.iconId)
+
+ launch {
+ viewModel.footer.isVisible.collect { visible ->
+ view.setFooterVisibility(if (visible) View.VISIBLE else View.GONE)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
new file mode 100644
index 0000000..d5417e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel
+
+import android.content.Context
+import android.icu.text.MessageFormat
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.modes.shared.ModesUi
+import com.android.systemui.res.R
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterMessageViewModel
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.Locale
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/**
+ * ViewModel for the empty shade (aka the "No notifications" text shown when there are no
+ * notifications.
+ */
+class EmptyShadeViewModel
+@AssistedInject
+constructor(
+ private val context: Context,
+ zenModeInteractor: ZenModeInteractor,
+ seenNotificationsInteractor: SeenNotificationsInteractor,
+ notificationSettingsInteractor: NotificationSettingsInteractor,
+ dumpManager: DumpManager,
+) : FlowDumperImpl(dumpManager) {
+ val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ zenModeInteractor.areNotificationsHiddenInShade.dumpWhileCollecting(
+ "areNotificationsHiddenInShade"
+ )
+ }
+ }
+
+ val hasFilteredOutSeenNotifications: StateFlow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ MutableStateFlow(false)
+ } else {
+ seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpValue(
+ "hasFilteredOutSeenNotifications"
+ )
+ }
+ }
+
+ val text: Flow<String> by lazy {
+ if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
+ flowOf(context.getString(R.string.empty_shade_text))
+ } else {
+ // Note: Flag modes_ui_empty_shade includes two pieces: refactoring the empty shade to
+ // recommended architecture, and making it so it reacts to changes for the new Modes.
+ // The former does not depend on the modes flags being on, but the latter does.
+ if (ModesUi.isEnabled) {
+ zenModeInteractor.modesHidingNotifications.map { modes ->
+ // Create a string that is either "No notifications" if no modes are filtering
+ // them
+ // out, or something like "Notifications paused by SomeMode" otherwise.
+ val msgFormat =
+ MessageFormat(
+ context.getString(R.string.modes_suppressing_shade_text),
+ Locale.getDefault(),
+ )
+ val count = modes.count()
+ val args: MutableMap<String, Any> = HashMap()
+ args["count"] = count
+ if (count >= 1) {
+ args["mode"] = modes[0].name
+ }
+ msgFormat.format(args)
+ }
+ } else {
+ areNotificationsHiddenInShade.map { areNotificationsHiddenInShade ->
+ if (areNotificationsHiddenInShade) {
+ context.getString(R.string.dnd_suppressing_shade_text)
+ } else {
+ context.getString(R.string.empty_shade_text)
+ }
+ }
+ }
+ }
+ }
+
+ val footer: FooterMessageViewModel by lazy {
+ ModesEmptyShadeFix.assertInNewMode()
+ FooterMessageViewModel(
+ messageId = R.string.unlock_to_see_notif_text,
+ iconId = R.drawable.ic_friction_lock_closed,
+ isVisible = hasFilteredOutSeenNotifications,
+ )
+ }
+
+ val tappingShouldLaunchHistory by lazy {
+ ModesEmptyShadeFix.assertInNewMode()
+ notificationSettingsInteractor.isNotificationHistoryEnabled
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): EmptyShadeViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index 291dc13..cd228e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -72,14 +72,24 @@
}
/**
+ * See {@link #setVisible(boolean, boolean, Consumer)}.
+ */
+ public void setVisible(boolean visible, boolean animate) {
+ setVisible(visible, animate, null /* onAnimationEnded */);
+ }
+
+ /**
* Make this view visible. If {@code false} is passed, the view will fade out its content
* and set the view Visibility to GONE. If only the content should be changed,
* {@link #setContentVisibleAnimated(boolean)} can be used.
*
* @param visible True if the contents should be visible.
* @param animate True if we should fade to new visibility.
+ * @param onAnimationEnded Callback to run after visibility updates, takes a boolean as a
+ * parameter that represents whether the animation was cancelled.
*/
- public void setVisible(boolean visible, boolean animate) {
+ public void setVisible(boolean visible, boolean animate,
+ Consumer<Boolean> onAnimationEnded) {
if (mIsVisible != visible) {
mIsVisible = visible;
if (animate) {
@@ -90,10 +100,10 @@
} else {
setWillBeGone(true);
}
- setContentVisible(visible, true /* animate */, null /* onAnimationEnded */);
+ setContentVisible(visible, true /* animate */, onAnimationEnded);
} else {
setVisibility(visible ? VISIBLE : GONE);
- setContentVisible(visible, false /* animate */, null /* onAnimationEnded */);
+ setContentVisible(visible, false /* animate */, onAnimationEnded);
setWillBeGone(false);
notifyHeightChanged(false /* needsAnimation */);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index cd3516d..b2b2c2a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -104,6 +104,7 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView;
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
@@ -686,7 +687,9 @@
protected void onFinishInflate() {
super.onFinishInflate();
- inflateEmptyShadeView();
+ if (!ModesEmptyShadeFix.isEnabled()) {
+ inflateEmptyShadeView();
+ }
if (!FooterViewRefactor.isEnabled()) {
inflateFooterView();
}
@@ -729,7 +732,9 @@
inflateFooterView();
updateFooter();
}
- inflateEmptyShadeView();
+ if (!ModesEmptyShadeFix.isEnabled()) {
+ inflateEmptyShadeView();
+ }
mSectionsManager.reinflateViews();
}
@@ -4835,6 +4840,8 @@
/** Trigger an update for the empty shade resources and visibility. */
public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade,
boolean hasFilteredOutSeenNotifications) {
+ ModesEmptyShadeFix.assertInLegacyMode();
+
mEmptyShadeView.setVisible(visible, mIsExpanded && mAnimationsEnabled);
if (areNotificationsHiddenInShade) {
@@ -4853,6 +4860,8 @@
@StringRes int newTextRes,
@StringRes int newFooterTextRes,
@DrawableRes int newFooterIconRes) {
+ ModesEmptyShadeFix.assertInLegacyMode();
+
int oldTextRes = mEmptyShadeView.getTextResource();
if (oldTextRes != newTextRes) {
mEmptyShadeView.setText(newTextRes);
@@ -4874,6 +4883,9 @@
public boolean isEmptyShadeViewVisible() {
SceneContainerFlag.assertInLegacyMode();
+ if (mEmptyShadeView == null) {
+ return false;
+ }
return mEmptyShadeView.isVisible();
}
@@ -5361,7 +5373,7 @@
public float getOpeningHeight() {
SceneContainerFlag.assertInLegacyMode();
- if (mEmptyShadeView.getVisibility() == GONE) {
+ if (mEmptyShadeView == null || mEmptyShadeView.getVisibility() == GONE) {
return getMinExpansionHeight();
} else {
return FooterViewRefactor.isEnabled() ? getAppearEndPosition()
@@ -5710,6 +5722,8 @@
}
private void inflateEmptyShadeView() {
+ ModesEmptyShadeFix.assertInLegacyMode();
+
EmptyShadeView oldView = mEmptyShadeView;
EmptyShadeView view = (EmptyShadeView) LayoutInflater.from(mContext).inflate(
R.layout.status_bar_no_notifications, this, false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index dc9615c..3dad326 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.stack.ui.viewbinder
import android.view.LayoutInflater
+import android.view.View
import androidx.lifecycle.lifecycleScope
import com.android.app.tracing.TraceUtils.traceAsync
import com.android.internal.logging.MetricsLogger
@@ -25,6 +26,7 @@
import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.repeatWhenAttachedToWindow
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -32,6 +34,10 @@
import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController
import com.android.systemui.statusbar.notification.dagger.SilentHeader
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewbinder.EmptyShadeViewBinder
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
@@ -49,6 +55,7 @@
import com.android.systemui.util.kotlin.awaitCancellationThenDispose
import com.android.systemui.util.kotlin.getOrNull
import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.stopAnimating
import com.android.systemui.util.ui.value
import java.util.Optional
import javax.inject.Inject
@@ -84,7 +91,7 @@
fun bindWhileAttached(
view: NotificationStackScrollLayout,
- viewController: NotificationStackScrollLayoutController
+ viewController: NotificationStackScrollLayoutController,
) {
val shelf =
LayoutInflater.from(view.context)
@@ -103,7 +110,13 @@
val hasNonClearableSilentNotifications: StateFlow<Boolean> =
viewModel.hasNonClearableSilentNotifications.stateIn(this)
launch { reinflateAndBindFooter(view, hasNonClearableSilentNotifications) }
- launch { bindEmptyShade(view) }
+ launch {
+ if (ModesEmptyShadeFix.isEnabled) {
+ reinflateAndBindEmptyShade(view)
+ } else {
+ bindEmptyShadeLegacy(viewModel.emptyShadeViewFactory.create(), view)
+ }
+ }
launch {
bindSilentHeaderClickListener(view, hasNonClearableSilentNotifications)
}
@@ -121,17 +134,12 @@
}
private suspend fun bindShelf(shelf: NotificationShelf) {
- NotificationShelfViewBinder.bind(
- shelf,
- viewModel.shelf,
- falsingManager,
- nicBinder,
- )
+ NotificationShelfViewBinder.bind(shelf, viewModel.shelf, falsingManager, nicBinder)
}
private suspend fun reinflateAndBindFooter(
parentView: NotificationStackScrollLayout,
- hasNonClearableSilentNotifications: StateFlow<Boolean>
+ hasNonClearableSilentNotifications: StateFlow<Boolean>,
) {
viewModel.footer.getOrNull()?.let { footerViewModel ->
// The footer needs to be re-inflated every time the theme or the font size changes.
@@ -149,7 +157,7 @@
footerView,
footerViewModel,
parentView,
- hasNonClearableSilentNotifications
+ hasNonClearableSilentNotifications,
)
}
}
@@ -163,13 +171,13 @@
footerView: FooterView,
footerViewModel: FooterViewModel,
parentView: NotificationStackScrollLayout,
- hasNonClearableSilentNotifications: StateFlow<Boolean>
+ hasNonClearableSilentNotifications: StateFlow<Boolean>,
): Unit = coroutineScope {
val disposableHandle =
FooterViewBinder.bindWhileAttached(
footerView,
footerViewModel,
- clearAllNotifications = {
+ {
clearAllNotifications(
parentView,
// Hide the silent section header (if present) if there will be
@@ -177,16 +185,8 @@
hideSilentSection = !hasNonClearableSilentNotifications.value,
)
},
- launchNotificationSettings = { view ->
- notificationActivityStarter
- .get()
- .startHistoryIntent(view, /* showHistory= */ false)
- },
- launchNotificationHistory = { view ->
- notificationActivityStarter
- .get()
- .startHistoryIntent(view, /* showHistory= */ true)
- },
+ launchNotificationSettings,
+ launchNotificationHistory,
)
if (SceneContainerFlag.isEnabled) {
launch {
@@ -194,7 +194,9 @@
footerView.setVisible(
/* visible = */ animatedVisibility.value,
/* animate = */ animatedVisibility.isAnimating,
- )
+ ) {
+ animatedVisibility.stopAnimating()
+ }
}
}
} else {
@@ -211,20 +213,71 @@
disposableHandle.awaitCancellationThenDispose()
}
- private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) {
+ private val launchNotificationSettings: (View) -> Unit = { view: View ->
+ notificationActivityStarter.get().startHistoryIntent(view, /* showHistory= */ false)
+ }
+
+ private val launchNotificationHistory: (View) -> Unit = { view ->
+ notificationActivityStarter.get().startHistoryIntent(view, /* showHistory= */ true)
+ }
+
+ private suspend fun reinflateAndBindEmptyShade(parentView: NotificationStackScrollLayout) {
+ ModesEmptyShadeFix.assertInNewMode()
+ // The empty shade needs to be re-inflated every time the theme or the font size
+ // changes.
+ configuration
+ .inflateLayout<EmptyShadeView>(
+ R.layout.status_bar_no_notifications,
+ parentView,
+ attachToRoot = false,
+ )
+ .flowOn(backgroundDispatcher)
+ .collectLatest { emptyShadeView: EmptyShadeView ->
+ traceAsync("bind EmptyShadeView") {
+ parentView.setEmptyShadeView(emptyShadeView)
+ bindEmptyShade(emptyShadeView, viewModel.emptyShadeViewFactory.create())
+ }
+ }
+ }
+
+ private suspend fun bindEmptyShadeLegacy(
+ emptyShadeViewModel: EmptyShadeViewModel,
+ parentView: NotificationStackScrollLayout,
+ ) {
+ ModesEmptyShadeFix.assertInLegacyMode()
combine(
viewModel.shouldShowEmptyShadeView,
- viewModel.areNotificationsHiddenInShade,
- viewModel.hasFilteredOutSeenNotifications,
- ::Triple
+ emptyShadeViewModel.areNotificationsHiddenInShade,
+ emptyShadeViewModel.hasFilteredOutSeenNotifications,
+ ::Triple,
)
.collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
- parentView.updateEmptyShadeView(
- shouldShow,
- areNotifsHidden,
- hasFilteredNotifs,
+ parentView.updateEmptyShadeView(shouldShow, areNotifsHidden, hasFilteredNotifs)
+ }
+ }
+
+ private suspend fun bindEmptyShade(
+ emptyShadeView: EmptyShadeView,
+ emptyShadeViewModel: EmptyShadeViewModel,
+ ): Unit = coroutineScope {
+ ModesEmptyShadeFix.assertInNewMode()
+ launch {
+ emptyShadeView.repeatWhenAttachedToWindow {
+ EmptyShadeViewBinder.bind(
+ emptyShadeView,
+ emptyShadeViewModel,
+ launchNotificationSettings,
+ launchNotificationHistory,
)
}
+ }
+ launch {
+ viewModel.shouldShowEmptyShadeViewAnimated.collect { shouldShow ->
+ emptyShadeView.setVisible(shouldShow.value, shouldShow.isAnimating) {
+ shouldShow.stopAnimating()
+ }
+ }
+ }
}
private suspend fun bindSilentHeaderClickListener(
@@ -261,7 +314,7 @@
private fun clearSilentNotifications(
view: NotificationStackScrollLayout,
closeShade: Boolean,
- hideSilentSection: Boolean
+ hideSilentSection: Boolean,
) {
view.clearSilentNotifications(closeShade, hideSilentSection)
}
@@ -270,11 +323,7 @@
if (NotificationsLiveDataStoreRefactor.isEnabled) {
viewModel.logger.getOrNull()?.let { viewModel ->
loggerOptional.getOrNull()?.let { logger ->
- NotificationStatsLoggerBinder.bindLogger(
- view,
- logger,
- viewModel,
- )
+ NotificationStatsLoggerBinder.bindLogger(view, logger, viewModel)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 4e2a46d..935e2a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -23,14 +23,14 @@
import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor
import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
-import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
import com.android.systemui.util.kotlin.FlowDumperImpl
import com.android.systemui.util.kotlin.combine
import com.android.systemui.util.kotlin.sample
@@ -48,22 +48,24 @@
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
-/** ViewModel for the list of notifications. */
+/**
+ * ViewModel for the list of notifications, including child elements like the Clear all/Manage
+ * button at the bottom (the footer) and the "No notifications" text (the empty shade).
+ */
class NotificationListViewModel
@Inject
constructor(
val shelf: NotificationShelfViewModel,
val hideListViewModel: HideListViewModel,
val footer: Optional<FooterViewModel>,
+ val emptyShadeViewFactory: EmptyShadeViewModel.Factory,
val logger: Optional<NotificationLoggerViewModel>,
activeNotificationsInteractor: ActiveNotificationsInteractor,
notificationStackInteractor: NotificationStackInteractor,
private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
remoteInputInteractor: RemoteInputInteractor,
- seenNotificationsInteractor: SeenNotificationsInteractor,
shadeInteractor: ShadeInteractor,
userSetupInteractor: UserSetupInteractor,
- zenModeInteractor: ZenModeInteractor,
@Background bgDispatcher: CoroutineDispatcher,
dumpManager: DumpManager,
) : FlowDumperImpl(dumpManager) {
@@ -90,6 +92,7 @@
}
val shouldShowEmptyShadeView: Flow<Boolean> by lazy {
+ ModesEmptyShadeFix.assertInLegacyMode()
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
flowOf(false)
} else {
@@ -114,6 +117,45 @@
}
}
+ val shouldShowEmptyShadeViewAnimated: Flow<AnimatedValue<Boolean>> by lazy {
+ if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
+ flowOf(AnimatedValue.NotAnimating(false))
+ } else {
+ combine(
+ activeNotificationsInteractor.areAnyNotificationsPresent,
+ shadeInteractor.isQsFullscreen,
+ notificationStackInteractor.isShowingOnLockscreen,
+ ) { hasNotifications, isQsFullScreen, isShowingOnLockscreen ->
+ when {
+ hasNotifications -> false
+ isQsFullScreen -> false
+ // Do not show the empty shade if the lockscreen is visible (including AOD
+ // b/228790482 and bouncer b/267060171), except if the shade is opened on
+ // top.
+ isShowingOnLockscreen -> false
+ else -> true
+ }
+ }
+ .distinctUntilChanged()
+ .sample(
+ // TODO(b/322167853): This check is currently duplicated in FooterViewModel
+ // but instead it should be a field in ShadeAnimationInteractor.
+ combine(
+ shadeInteractor.isShadeFullyExpanded,
+ shadeInteractor.isShadeTouchable,
+ ::Pair,
+ )
+ .onStart { emit(Pair(false, false)) }
+ ) { visible, (isShadeFullyExpanded, animationsEnabled) ->
+ val shouldAnimate = isShadeFullyExpanded && animationsEnabled
+ AnimatableEvent(visible, shouldAnimate)
+ }
+ .toAnimatedValueFlow()
+ .dumpWhileCollecting("shouldShowEmptyShadeViewAnimated")
+ .flowOn(bgDispatcher)
+ }
+ }
+
/**
* Whether the footer should not be visible for the user, even if it's present in the list (as
* per [shouldIncludeFooterView] below).
@@ -154,7 +196,7 @@
userSetupInteractor.isUserSetUp,
notificationStackInteractor.isShowingOnLockscreen,
shadeInteractor.isQsFullscreen,
- remoteInputInteractor.isRemoteInputActive
+ remoteInputInteractor.isRemoteInputActive,
) {
hasNotifications,
isUserSetUp,
@@ -193,7 +235,7 @@
combine(
shadeInteractor.isShadeFullyExpanded,
shadeInteractor.isShadeTouchable,
- ::Pair
+ ::Pair,
)
.onStart { emit(Pair(false, false)) }
) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
@@ -263,7 +305,7 @@
combine(
shadeInteractor.isShadeFullyExpanded,
shadeInteractor.isShadeTouchable,
- ::Pair
+ ::Pair,
)
.onStart { emit(Pair(false, false)) }
) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
@@ -283,29 +325,7 @@
enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) {
DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false),
DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true),
- APPEAR_WITH_ANIMATION(visible = true, canAnimate = true)
- }
-
- // TODO(b/308591475): This should be tracked separately by the empty shade.
- val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
- if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
- flowOf(false)
- } else {
- zenModeInteractor.areNotificationsHiddenInShade.dumpWhileCollecting(
- "areNotificationsHiddenInShade"
- )
- }
- }
-
- // TODO(b/308591475): This should be tracked separately by the empty shade.
- val hasFilteredOutSeenNotifications: Flow<Boolean> by lazy {
- if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
- flowOf(false)
- } else {
- seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpWhileCollecting(
- "hasFilteredOutSeenNotifications"
- )
- }
+ APPEAR_WITH_ANIMATION(visible = true, canAnimate = true),
}
val hasClearableAlertingNotifications: Flow<Boolean> by lazy {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index caf09a3..674cbb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -46,8 +46,6 @@
private val tag = "AvalancheController"
private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG)
- var baseEntryMapStr : () -> String = { "baseEntryMapStr not initialized" }
-
var enableAtRuntime = true
set(value) {
if (!value) {
@@ -118,43 +116,32 @@
val key = getKey(entry)
if (runnable == null) {
- headsUpManagerLogger.logAvalancheUpdate(
- caller, isEnabled, key,
- "Runnable NULL, stop. ${getStateStr()}"
- )
+ headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, "Runnable NULL, stop")
return
}
if (!isEnabled) {
- headsUpManagerLogger.logAvalancheUpdate(
- caller, isEnabled, key,
- "NOT ENABLED, run runnable. ${getStateStr()}"
- )
+ headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key,
+ "NOT ENABLED, run runnable")
runnable.run()
return
}
if (entry == null) {
- headsUpManagerLogger.logAvalancheUpdate(
- caller, isEnabled, key,
- "Entry NULL, stop. ${getStateStr()}"
- )
+ headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, "Entry NULL, stop")
return
}
if (debug) {
debugRunnableLabelMap[runnable] = caller
}
- var stateAfter = ""
+ var outcome = ""
if (isShowing(entry)) {
+ outcome = "update showing"
runnable.run()
- stateAfter = "update showing"
-
} else if (entry in nextMap) {
+ outcome = "update next"
nextMap[entry]?.add(runnable)
- stateAfter = "update next"
-
} else if (headsUpEntryShowing == null) {
+ outcome = "show now"
showNow(entry, arrayListOf(runnable))
- stateAfter = "show now"
-
} else {
// Clean up invalid state when entry is in list but not map and vice versa
if (entry in nextMap) nextMap.remove(entry)
@@ -175,8 +162,8 @@
)
}
}
- stateAfter += getStateStr()
- headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled = true, key, stateAfter)
+ outcome += getStateStr()
+ headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, outcome)
}
@VisibleForTesting
@@ -194,40 +181,32 @@
val key = getKey(entry)
if (runnable == null) {
- headsUpManagerLogger.logAvalancheDelete(
- caller, isEnabled, key,
- "Runnable NULL, stop. ${getStateStr()}"
- )
+ headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key, "Runnable NULL, stop")
return
}
if (!isEnabled) {
+ headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key,
+ "NOT ENABLED, run runnable")
runnable.run()
- headsUpManagerLogger.logAvalancheDelete(
- caller, isEnabled = false, key,
- "NOT ENABLED, run runnable. ${getStateStr()}"
- )
return
}
if (entry == null) {
+ headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key,
+ "Entry NULL, run runnable")
runnable.run()
- headsUpManagerLogger.logAvalancheDelete(
- caller, isEnabled = true, key,
- "Entry NULL, run runnable. ${getStateStr()}"
- )
return
}
- val stateAfter: String
+ val outcome: String
if (entry in nextMap) {
+ outcome = "remove from next"
if (entry in nextMap) nextMap.remove(entry)
if (entry in nextList) nextList.remove(entry)
uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_REMOVED)
- stateAfter = "remove from next. ${getStateStr()}"
-
} else if (entry in debugDropSet) {
+ outcome = "remove from dropset"
debugDropSet.remove(entry)
- stateAfter = "remove from dropset. ${getStateStr()}"
-
} else if (isShowing(entry)) {
+ outcome = "remove showing"
previousHunKey = getKey(headsUpEntryShowing)
// Show the next HUN before removing this one, so that we don't tell listeners
// onHeadsUpPinnedModeChanged, which causes
@@ -235,13 +214,11 @@
// HUN is animating out, resulting in a flicker.
showNext()
runnable.run()
- stateAfter = "remove showing. ${getStateStr()}"
-
} else {
+ outcome = "run runnable for untracked shown"
runnable.run()
- stateAfter = "run runnable for untracked shown HUN. ${getStateStr()}"
}
- headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), stateAfter)
+ headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), outcome)
}
/**
@@ -423,14 +400,12 @@
}
private fun getStateStr(): String {
- return "\nAvalancheController:" +
+ return "\navalanche state:" +
"\n\tshowing: [${getKey(headsUpEntryShowing)}]" +
"\n\tprevious: [$previousHunKey]" +
"\n\tnext list: $nextListStr" +
"\n\tnext map: $nextMapStr" +
- "\n\tdropped: $dropSetStr" +
- "\nBHUM.mHeadsUpEntryMap: " +
- baseEntryMapStr()
+ "\n\tdropped: $dropSetStr"
}
private val dropSetStr: String
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 30524a5..f37393a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -116,7 +116,6 @@
mAccessibilityMgr = accessibilityManagerWrapper;
mUiEventLogger = uiEventLogger;
mAvalancheController = avalancheController;
- mAvalancheController.setBaseEntryMapStr(this::getEntryMapStr);
Resources resources = context.getResources();
mMinimumDisplayTime = NotificationThrottleHun.isEnabled()
? 500 : resources.getInteger(R.integer.heads_up_notification_minimum_time);
@@ -590,18 +589,6 @@
dumpInternal(pw, args);
}
- private String getEntryMapStr() {
- if (mHeadsUpEntryMap.isEmpty()) {
- return "EMPTY";
- }
- StringBuilder entryMapStr = new StringBuilder();
- for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
- entryMapStr.append("\n\t").append(
- entry.mEntry == null ? "null" : entry.mEntry.getKey());
- }
- return entryMapStr.toString();
- }
-
protected void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
@@ -1005,6 +992,7 @@
* Clear any pending removal runnables.
*/
public void cancelAutoRemovalCallbacks(@Nullable String reason) {
+ mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
Runnable runnable = () -> {
final boolean removed = cancelAutoRemovalCallbackInternal();
@@ -1013,7 +1001,6 @@
}
};
if (mEntry != null && isHeadsUpEntry(mEntry.getKey())) {
- mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks");
} else {
// Just removed
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index 41112cb..600270c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -52,7 +52,7 @@
caller: String,
isEnabled: Boolean,
notifEntryKey: String,
- stateAfter: String
+ outcome: String
) {
buffer.log(
TAG,
@@ -60,7 +60,7 @@
{
str1 = caller
str2 = notifEntryKey
- str3 = stateAfter
+ str3 = outcome
bool1 = isEnabled
},
{ "$str1\n\t=> AC[isEnabled:$bool1] update: $str2\n\t=> $str3" }
@@ -71,7 +71,7 @@
caller: String,
isEnabled: Boolean,
notifEntryKey: String,
- stateAfter: String
+ outcome: String
) {
buffer.log(
TAG,
@@ -79,7 +79,7 @@
{
str1 = caller
str2 = notifEntryKey
- str3 = stateAfter
+ str3 = outcome
bool1 = isEnabled
},
{ "$str1\n\t=> AC[isEnabled:$bool1] delete: $str2\n\t=> $str3" }
@@ -136,7 +136,7 @@
str1 = entry.logKey
str2 = reason ?: "unknown"
},
- { "$str2 => request: cancelAutoRemovalCallbacks: $str1" }
+ { "request: cancel auto remove of $str1 reason: $str2" }
)
}
@@ -148,7 +148,7 @@
str1 = entry.logKey
str2 = reason ?: "unknown"
},
- { "$str2 => cancel auto remove: $str1" }
+ { "cancel auto remove of $str1 reason: $str2" }
)
}
@@ -161,7 +161,7 @@
str2 = reason
bool1 = isWaiting
},
- { "request: $str2 => removeEntry: $str1 isWaiting: $isWaiting" }
+ { "request: $str2 => remove entry $str1 isWaiting: $isWaiting" }
)
}
@@ -174,7 +174,7 @@
str2 = reason
bool1 = isWaiting
},
- { "$str2 => removeEntry: $str1 isWaiting: $isWaiting" }
+ { "$str2 => remove entry $str1 isWaiting: $isWaiting" }
)
}
@@ -216,12 +216,12 @@
str1 = logKey(key)
str2 = reason
},
- { "remove notif $str1 when headsUpEntry is null, reason: $str2" }
+ { "remove notification $str1 when headsUpEntry is null, reason: $str2" }
)
}
fun logNotificationActuallyRemoved(entry: NotificationEntry) {
- buffer.log(TAG, INFO, { str1 = entry.logKey }, { "removed: $str1 " })
+ buffer.log(TAG, INFO, { str1 = entry.logKey }, { "notification removed $str1 " })
}
fun logUpdateNotificationRequest(key: String, alert: Boolean, hasEntry: Boolean) {
@@ -233,7 +233,7 @@
bool1 = alert
bool2 = hasEntry
},
- { "request: update notif $str1 alert: $bool1 hasEntry: $bool2" }
+ { "request: update notification $str1 alert: $bool1 hasEntry: $bool2" }
)
}
@@ -246,7 +246,7 @@
bool1 = alert
bool2 = hasEntry
},
- { "update notif $str1 alert: $bool1 hasEntry: $bool2" }
+ { "update notification $str1 alert: $bool1 hasEntry: $bool2" }
)
}
@@ -281,7 +281,7 @@
bool1 = isPinned
str2 = reason
},
- { "$str2 => setEntryPinned[$bool1]: $str1" }
+ { "$str2 => set entry pinned $str1 pinned: $bool1" }
)
}
@@ -290,7 +290,7 @@
TAG,
INFO,
{ bool1 = hasPinnedNotification },
- { "hasPinnedNotification[$bool1]" }
+ { "has pinned notification changed to $bool1" }
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index ba45942..daba109 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -20,6 +20,7 @@
import android.provider.Settings
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
+import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
import android.util.Log
import androidx.concurrent.futures.await
import com.android.settingslib.notification.data.repository.ZenModeRepository
@@ -29,6 +30,7 @@
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.modes.shared.ModesUi
import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository
import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes
@@ -39,6 +41,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -54,8 +57,8 @@
private val notificationSettingsRepository: NotificationSettingsRepository,
@Background private val bgDispatcher: CoroutineDispatcher,
private val iconLoader: ZenIconLoader,
- private val deviceProvisioningRepository: DeviceProvisioningRepository,
- private val userSetupRepository: UserSetupRepository,
+ deviceProvisioningRepository: DeviceProvisioningRepository,
+ userSetupRepository: UserSetupRepository,
) {
val isZenAvailable: Flow<Boolean> =
combine(
@@ -126,6 +129,25 @@
val mainActiveMode: Flow<ZenModeInfo?> =
activeModes.map { a -> a.mainMode }.distinctUntilChanged()
+ val modesHidingNotifications: Flow<List<ZenMode>> by lazy {
+ if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode() || !ModesUi.isEnabled) {
+ flowOf(listOf())
+ } else {
+ modes
+ .map { modes ->
+ modes.filter { mode ->
+ mode.isActive &&
+ !mode.policy.isVisualEffectAllowed(
+ /* effect = */ VISUAL_EFFECT_NOTIFICATION_LIST,
+ /* defaultVal = */ true,
+ )
+ }
+ }
+ .flowOn(bgDispatcher)
+ .distinctUntilChanged()
+ }
+ }
+
suspend fun getModeIcon(mode: ZenMode): ZenIcon {
return iconLoader.getIcon(context, mode).await()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
index 71afa62..5cc6454 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -62,6 +62,7 @@
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.scene.sceneContainerGestureFilterFactory
import com.android.systemui.scene.shared.logger.sceneLogger
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
@@ -70,6 +71,7 @@
import com.android.systemui.scene.ui.composable.SceneContainer
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.settings.displayTracker
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.testKosmos
import kotlin.time.Duration.Companion.seconds
@@ -133,6 +135,8 @@
shadeInteractor = kosmos.shadeInteractor,
splitEdgeDetector = kosmos.splitEdgeDetector,
logger = kosmos.sceneLogger,
+ gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
+ displayId = kosmos.displayTracker.defaultDisplayId,
motionEventHandlerReceiver = {},
)
.apply { setTransitionState(transitionState) }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
index 07c29a0..0c65c9c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
@@ -20,7 +20,6 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
@@ -30,15 +29,12 @@
import android.app.Notification.MediaStyle;
import android.media.session.MediaSession;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
import android.service.notification.NotificationListenerService;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.media.controls.util.MediaFeatureFlag;
import com.android.systemui.statusbar.notification.InflationException;
@@ -158,8 +154,7 @@
}
@Test
- @DisableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS)
- public void inflateMediaNotificationIconsMediaEnabled_old() throws InflationException {
+ public void inflateMediaNotificationIconsMediaEnabled() throws InflationException {
finishSetupWithMediaFeatureFlagEnabled(true);
mListener.onEntryInit(mMediaEntry);
@@ -187,37 +182,7 @@
}
@Test
- @EnableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS)
- public void inflateMediaNotificationIconsMediaEnabled_new() throws InflationException {
- finishSetupWithMediaFeatureFlagEnabled(true);
-
- mListener.onEntryInit(mMediaEntry);
- mListener.onEntryAdded(mMediaEntry);
- verify(mIconManager).createIcons(eq(mMediaEntry));
- verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
- clearInvocations(mIconManager);
-
- mFilter.shouldFilterOut(mMediaEntry, 0);
- verify(mIconManager, never()).createIcons(eq(mMediaEntry));
- verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
-
- mListener.onEntryUpdated(mMediaEntry);
- verify(mIconManager, never()).createIcons(eq(mMediaEntry));
- verify(mIconManager).updateIcons(eq(mMediaEntry), /* usingCache = */ eq(false));
-
- mListener.onEntryRemoved(mMediaEntry, NotificationListenerService.REASON_CANCEL);
- mListener.onEntryCleanUp(mMediaEntry);
- clearInvocations(mIconManager);
-
- mListener.onEntryInit(mMediaEntry);
- mListener.onEntryAdded(mMediaEntry);
- verify(mIconManager).createIcons(eq(mMediaEntry));
- verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
- }
-
- @Test
- @DisableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS)
- public void inflationException_old() throws InflationException {
+ public void inflationException() throws InflationException {
finishSetupWithMediaFeatureFlagEnabled(true);
mListener.onEntryInit(mMediaEntry);
@@ -244,31 +209,6 @@
verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
}
- @Test
- @EnableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS)
- public void inflationException_new() throws InflationException {
- finishSetupWithMediaFeatureFlagEnabled(true);
-
- doThrow(InflationException.class).when(mIconManager).createIcons(eq(mMediaEntry));
-
- mListener.onEntryInit(mMediaEntry);
- mListener.onEntryAdded(mMediaEntry);
- verify(mIconManager).createIcons(eq(mMediaEntry));
- verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
- clearInvocations(mIconManager);
-
- mListener.onEntryUpdated(mMediaEntry);
- verify(mIconManager).createIcons(eq(mMediaEntry));
- verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
- clearInvocations(mIconManager);
-
- doNothing().when(mIconManager).createIcons(eq(mMediaEntry));
-
- mListener.onEntryUpdated(mMediaEntry);
- verify(mIconManager).createIcons(eq(mMediaEntry));
- verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
- }
-
private void finishSetupWithMediaFeatureFlagEnabled(boolean mediaFeatureFlagEnabled) {
when(mMediaFeatureFlag.getEnabled()).thenReturn(mediaFeatureFlagEnabled);
mCoordinator = new MediaCoordinator(mMediaFeatureFlag, mStatusBarService, mIconManager);
diff --git a/packages/SystemUI/tests/utils/src/android/view/WindowManagerServiceKosmos.kt b/packages/SystemUI/tests/utils/src/android/view/WindowManagerServiceKosmos.kt
new file mode 100644
index 0000000..cd681a1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/view/WindowManagerServiceKosmos.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.view
+
+import android.graphics.Region
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.whenever
+
+val Kosmos.mockWindowManagerService: IWindowManager by
+ Kosmos.Fixture {
+ mock(IWindowManager::class.java).apply {
+ whenever(registerSystemGestureExclusionListener(any(), anyInt())).then { answer ->
+ val listener = answer.arguments[0] as ISystemGestureExclusionListener
+ val displayId = answer.arguments[1] as Int
+ exclusionListeners.getOrPut(displayId) { mutableListOf() }.add(listener)
+ listener.onSystemGestureExclusionChanged(
+ displayId,
+ restrictedRegionByDisplayId[displayId],
+ null,
+ )
+ }
+
+ whenever(unregisterSystemGestureExclusionListener(any(), anyInt())).then { answer ->
+ val listener = answer.arguments[0] as ISystemGestureExclusionListener
+ val displayId = answer.arguments[1] as Int
+ exclusionListeners[displayId]?.remove(listener)
+ }
+ }
+ }
+
+var Kosmos.windowManagerService: IWindowManager by Kosmos.Fixture { mockWindowManagerService }
+
+private var restrictedRegionByDisplayId = mutableMapOf<Int, Region?>()
+private var exclusionListeners = mutableMapOf<Int, MutableList<ISystemGestureExclusionListener>>()
+
+fun setSystemGestureExclusionRegion(displayId: Int, restrictedRegion: Region?) {
+ restrictedRegionByDisplayId[displayId] = restrictedRegion
+ exclusionListeners[displayId]?.forEach { listener ->
+ listener.onSystemGestureExclusionChanged(displayId, restrictedRegion, null)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 8744638..737aaf2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -6,13 +6,16 @@
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.interactor.systemGestureExclusionInteractor
import com.android.systemui.scene.shared.logger.sceneLogger
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.FakeOverlay
+import com.android.systemui.scene.ui.viewmodel.SceneContainerGestureFilter
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.settings.displayTracker
import com.android.systemui.shade.domain.interactor.shadeInteractor
import kotlinx.coroutines.flow.MutableStateFlow
@@ -30,10 +33,7 @@
val Kosmos.initialSceneKey by Fixture { Scenes.Lockscreen }
var Kosmos.overlayKeys by Fixture {
- listOf(
- Overlays.NotificationsShade,
- Overlays.QuickSettingsShade,
- )
+ listOf(Overlays.NotificationsShade, Overlays.QuickSettingsShade)
}
val Kosmos.fakeOverlaysByKeys by Fixture { overlayKeys.associateWith { FakeOverlay(it) } }
@@ -74,8 +74,21 @@
powerInteractor = powerInteractor,
shadeInteractor = shadeInteractor,
splitEdgeDetector = splitEdgeDetector,
+ gestureFilterFactory = sceneContainerGestureFilterFactory,
+ displayId = displayTracker.defaultDisplayId,
motionEventHandlerReceiver = {},
- logger = sceneLogger
+ logger = sceneLogger,
)
.apply { setTransitionState(transitionState) }
}
+
+val Kosmos.sceneContainerGestureFilterFactory by Fixture {
+ object : SceneContainerGestureFilter.Factory {
+ override fun create(displayId: Int): SceneContainerGestureFilter {
+ return SceneContainerGestureFilter(
+ interactor = systemGestureExclusionInteractor,
+ displayId = displayId,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepositoryKosmos.kt
new file mode 100644
index 0000000..15ed1b3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepositoryKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.scene.data.repository
+
+import android.view.windowManagerService
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.systemGestureExclusionRepository by Fixture {
+ SystemGestureExclusionRepository(windowManager = windowManagerService)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractorKosmos.kt
new file mode 100644
index 0000000..3e46c3f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractorKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.scene.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.scene.data.repository.systemGestureExclusionRepository
+
+val Kosmos.systemGestureExclusionInteractor by Fixture {
+ SystemGestureExclusionInteractor(repository = systemGestureExclusionRepository)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt
new file mode 100644
index 0000000..8fdb948
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shared.notifications.domain.interactor.notificationSettingsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+
+val Kosmos.emptyShadeViewModel by
+ Kosmos.Fixture {
+ EmptyShadeViewModel(
+ applicationContext,
+ zenModeInteractor,
+ seenNotificationsInteractor,
+ notificationSettingsInteractor,
+ dumpManager,
+ )
+ }
+
+val Kosmos.emptyShadeViewModelFactory: EmptyShadeViewModel.Factory by
+ Kosmos.Fixture {
+ object : EmptyShadeViewModel.Factory {
+ override fun create() = emptyShadeViewModel
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
index de8b350..c3bc744 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
@@ -23,13 +23,12 @@
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
-import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.emptyShadeViewModelFactory
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.footerViewModel
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.notificationShelfViewModel
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackInteractor
import com.android.systemui.statusbar.policy.domain.interactor.userSetupInteractor
-import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
import java.util.Optional
val Kosmos.notificationListViewModel by Fixture {
@@ -37,15 +36,14 @@
notificationShelfViewModel,
hideListViewModel,
Optional.of(footerViewModel),
+ emptyShadeViewModelFactory,
Optional.of(notificationListLoggerViewModel),
activeNotificationsInteractor,
notificationStackInteractor,
headsUpNotificationInteractor,
remoteInputInteractor,
- seenNotificationsInteractor,
shadeInteractor,
userSetupInteractor,
- zenModeInteractor,
testDispatcher,
dumpManager,
)
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 1e723b5..c8f8c2a 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -204,7 +204,8 @@
serviceIntent,
targetUser,
safeExecuteAppFunctionCallback,
- /* bindFlags= */ Context.BIND_AUTO_CREATE);
+ /* bindFlags= */ Context.BIND_AUTO_CREATE
+ | Context.BIND_FOREGROUND_SERVICE);
})
.exceptionally(
ex -> {
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 89dfc73..12e8c57 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -380,10 +380,12 @@
}
/** Gets transports that need to be marked as restricted by the VCN from CarrierConfig */
+ // TODO: b/262269892 This method was created to perform experiments before the relevant API
+ // was exposed. Now it is obsolete and should be removed.
@VisibleForTesting(visibility = Visibility.PRIVATE)
public Set<Integer> getRestrictedTransportsFromCarrierConfig(
ParcelUuid subGrp, TelephonySubscriptionSnapshot lastSnapshot) {
- if (!Build.IS_ENG && !Build.IS_USERDEBUG) {
+ if (!Build.isDebuggable()) {
return RESTRICTED_TRANSPORTS_DEFAULT;
}
diff --git a/services/core/java/com/android/server/WiredAccessoryManager.java b/services/core/java/com/android/server/WiredAccessoryManager.java
index b271d7e..ab69cd1 100644
--- a/services/core/java/com/android/server/WiredAccessoryManager.java
+++ b/services/core/java/com/android/server/WiredAccessoryManager.java
@@ -118,8 +118,11 @@
if (mInputManager.getSwitchState(-1, InputDevice.SOURCE_ANY, SW_LINEOUT_INSERT) == 1) {
switchValues |= SW_LINEOUT_INSERT_BIT;
}
- notifyWiredAccessoryChanged(0, switchValues,
- SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT | SW_LINEOUT_INSERT_BIT);
+ notifyWiredAccessoryChanged(
+ 0,
+ switchValues,
+ SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT | SW_LINEOUT_INSERT_BIT,
+ true /*isSynchronous*/);
}
@@ -135,7 +138,13 @@
}
@Override
- public void notifyWiredAccessoryChanged(long whenNanos, int switchValues, int switchMask) {
+ public void notifyWiredAccessoryChanged(
+ long whenNanos, int switchValues, int switchMask) {
+ notifyWiredAccessoryChanged(whenNanos, switchValues, switchMask, false /*isSynchronous*/);
+ }
+
+ public void notifyWiredAccessoryChanged(
+ long whenNanos, int switchValues, int switchMask, boolean isSynchronous) {
if (LOG) {
Slog.v(TAG, "notifyWiredAccessoryChanged: when=" + whenNanos
+ " bits=" + switchCodeToString(switchValues, switchMask)
@@ -172,8 +181,10 @@
break;
}
- updateLocked(NAME_H2W,
- (mHeadsetState & ~(BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) | headset);
+ updateLocked(
+ NAME_H2W,
+ (mHeadsetState & ~(BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) | headset,
+ isSynchronous);
}
}
@@ -195,8 +206,9 @@
*
* @param newName One of the NAME_xxx variables defined above.
* @param newState 0 or one of the BIT_xxx variables defined above.
+ * @param isSynchronous boolean to determine whether should happen sync or async
*/
- private void updateLocked(String newName, int newState) {
+ private void updateLocked(String newName, int newState, boolean isSynchronous) {
// Retain only relevant bits
int headsetState = newState & SUPPORTED_HEADSETS;
int usb_headset_anlg = headsetState & BIT_USB_HEADSET_ANLG;
@@ -234,12 +246,15 @@
return;
}
- mWakeLock.acquire();
-
- Log.i(TAG, "MSG_NEW_DEVICE_STATE");
- Message msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE, headsetState,
- mHeadsetState, "");
- mHandler.sendMessage(msg);
+ if (isSynchronous) {
+ setDevicesState(headsetState, mHeadsetState, "");
+ } else {
+ mWakeLock.acquire();
+ Log.i(TAG, "MSG_NEW_DEVICE_STATE");
+ Message msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE, headsetState,
+ mHeadsetState, "");
+ mHandler.sendMessage(msg);
+ }
mHeadsetState = headsetState;
}
@@ -439,7 +454,10 @@
for (int i = 0; i < mUEventInfo.size(); ++i) {
UEventInfo uei = mUEventInfo.get(i);
if (devPath.equals(uei.getDevPath())) {
- updateLocked(name, uei.computeNewHeadsetState(mHeadsetState, state));
+ updateLocked(
+ name,
+ uei.computeNewHeadsetState(mHeadsetState, state),
+ false /*isSynchronous*/);
return;
}
}
@@ -550,7 +568,10 @@
synchronized (mLock) {
int mask = maskAndState.first;
int state = maskAndState.second;
- updateLocked(name, mHeadsetState & ~(mask & ~state) | (mask & state));
+ updateLocked(
+ name,
+ mHeadsetState & ~(mask & ~state) | (mask & state),
+ false /*isSynchronous*/);
return;
}
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index a21a3f1..ad72941 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -262,7 +262,6 @@
import android.content.AttributionSource;
import android.content.AutofillOptions;
import android.content.BroadcastReceiver;
-import android.content.ClipData;
import android.content.ComponentCallbacks2;
import android.content.ComponentName;
import android.content.ContentCaptureOptions;
@@ -420,6 +419,7 @@
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.MemInfoReader;
import com.android.internal.util.Preconditions;
+import com.android.server.crashrecovery.CrashRecoveryHelper;
import com.android.server.AlarmManagerInternal;
import com.android.server.BootReceiver;
import com.android.server.DeviceIdleInternal;
@@ -439,7 +439,6 @@
import com.android.server.appop.AppOpsService;
import com.android.server.compat.PlatformCompat;
import com.android.server.contentcapture.ContentCaptureManagerInternal;
-import com.android.server.crashrecovery.CrashRecoveryHelper;
import com.android.server.criticalevents.CriticalEventLog;
import com.android.server.firewall.IntentFirewall;
import com.android.server.graphics.fonts.FontManagerInternal;
@@ -484,7 +483,6 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -502,7 +500,6 @@
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@@ -579,7 +576,7 @@
static final int RESERVED_BYTES_PER_LOGCAT_LINE = 100;
// How many seconds should the system wait before terminating the spawned logcat process.
- static final int LOGCAT_TIMEOUT_SEC = 10;
+ static final int LOGCAT_TIMEOUT_SEC = Flags.logcatLongerTimeout() ? 15 : 10;
// Necessary ApplicationInfo flags to mark an app as persistent
static final int PERSISTENT_MASK =
@@ -19100,87 +19097,4 @@
Freezer getFreezer() {
return mFreezer;
}
-
- // Set of IntentCreatorToken objects that are currently active.
- private static final Map<IntentCreatorToken.Key, WeakReference<IntentCreatorToken>>
- sIntentCreatorTokenCache = new ConcurrentHashMap<>();
-
- /**
- * A binder token used to keep track of which app created the intent. This token can be used to
- * defend against intent redirect attacks. It stores uid of the intent creator and key fields of
- * the intent to make it impossible for attacker to fake uid with a malicious intent.
- *
- * @hide
- */
- public static final class IntentCreatorToken extends Binder {
- @NonNull
- private final Key mKeyFields;
-
- public IntentCreatorToken(int creatorUid, Intent intent) {
- super();
- this.mKeyFields = new Key(creatorUid, intent);
- }
-
- public int getCreatorUid() {
- return mKeyFields.mCreatorUid;
- }
-
- /** {@hide} */
- public static boolean isValid(@NonNull Intent intent) {
- IBinder binder = intent.getCreatorToken();
- IntentCreatorToken token = null;
- if (binder instanceof IntentCreatorToken) {
- token = (IntentCreatorToken) binder;
- }
- return token != null && token.mKeyFields.equals(
- new Key(token.mKeyFields.mCreatorUid, intent));
- }
-
- private static class Key {
- private Key(int creatorUid, Intent intent) {
- this.mCreatorUid = creatorUid;
- this.mAction = intent.getAction();
- this.mData = intent.getData();
- this.mType = intent.getType();
- this.mPackage = intent.getPackage();
- this.mComponent = intent.getComponent();
- this.mFlags = intent.getFlags() & Intent.IMMUTABLE_FLAGS;
- ClipData clipData = intent.getClipData();
- if (clipData != null) {
- this.mClipDataUris = new ArrayList<>(clipData.getItemCount());
- for (int i = 0; i < clipData.getItemCount(); i++) {
- this.mClipDataUris.add(clipData.getItemAt(i).getUri());
- }
- }
- }
-
- private final int mCreatorUid;
- private final String mAction;
- private final Uri mData;
- private final String mType;
- private final String mPackage;
- private final ComponentName mComponent;
- private final int mFlags;
- private List<Uri> mClipDataUris;
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Key key = (Key) o;
- return mCreatorUid == key.mCreatorUid && mFlags == key.mFlags && Objects.equals(
- mAction, key.mAction) && Objects.equals(mData, key.mData)
- && Objects.equals(mType, key.mType) && Objects.equals(mPackage,
- key.mPackage) && Objects.equals(mComponent, key.mComponent)
- && Objects.equals(mClipDataUris, key.mClipDataUris);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mCreatorUid, mAction, mData, mType, mPackage, mComponent,
- mFlags,
- mClipDataUris);
- }
- }
- }
}
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 75e9fad..15277ce 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -437,7 +437,6 @@
mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context,
mPowerAttributor, mPowerProfile, mCpuScalingPolicies,
mPowerStatsStore, Clock.SYSTEM_CLOCK);
- mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore);
mDumpHelper = new BatteryStatsDumpHelperImpl(mBatteryUsageStatsProvider);
mCpuWakeupStats = new CpuWakeupStats(context, R.xml.irq_device_map, mHandler);
mConfigFile = new AtomicFile(new File(systemDir, "battery_usage_stats_config"));
@@ -504,6 +503,9 @@
}
public void systemServicesReady() {
+ mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore,
+ Flags.accumulateBatteryUsageStats());
+
MultiStatePowerAttributor attributor = (MultiStatePowerAttributor) mPowerAttributor;
mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CPU,
Flags.streamlinedBatteryStats());
@@ -1100,14 +1102,17 @@
DEVICE_CONFIG_NAMESPACE,
MIN_CONSUMED_POWER_THRESHOLD_KEY,
0);
- final BatteryUsageStatsQuery query =
- new BatteryUsageStatsQuery.Builder()
- .setMaxStatsAgeMs(0)
- .includeProcessStateData()
- .includeVirtualUids()
- .setMinConsumedPowerThreshold(minConsumedPowerThreshold)
- .build();
- bus = getBatteryUsageStats(List.of(query)).get(0);
+ BatteryUsageStatsQuery.Builder query = new BatteryUsageStatsQuery.Builder()
+ .setMaxStatsAgeMs(0)
+ .includeProcessStateData()
+ .includeVirtualUids()
+ .setMinConsumedPowerThreshold(minConsumedPowerThreshold);
+
+ if (Flags.accumulateBatteryUsageStats()) {
+ query.accumulated();
+ }
+
+ bus = getBatteryUsageStats(List.of(query.build())).get(0);
final int pullResult =
new StatsPerUidLogger(new FrameworkStatsLogger()).logStats(bus, data);
try {
@@ -3016,6 +3021,9 @@
if (Flags.streamlinedBatteryStats()) {
pw.println(" --sample: collect and dump a sample of stats for debugging purpose");
}
+ if (Flags.accumulateBatteryUsageStats()) {
+ pw.println(" --accumulated: continuously accumulated since setup or reset-all");
+ }
pw.println(" <package.name>: optional name of package to filter output by.");
pw.println(" -h: print this help text.");
pw.println("Battery stats (batterystats) commands:");
@@ -3083,7 +3091,7 @@
}
private void dumpUsageStats(FileDescriptor fd, PrintWriter pw, int model,
- boolean proto) {
+ boolean proto, boolean accumulated) {
awaitCompletion();
syncStats("dump", BatteryExternalStatsWorker.UPDATE_ALL);
@@ -3097,6 +3105,9 @@
if (model == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
builder.powerProfileModeledOnly();
}
+ if (accumulated) {
+ builder.accumulated();
+ }
BatteryUsageStatsQuery query = builder.build();
synchronized (mStats) {
mStats.prepareForDumpLocked();
@@ -3287,6 +3298,7 @@
} else if ("--usage".equals(arg)) {
int model = BatteryConsumer.POWER_MODEL_UNDEFINED;
boolean proto = false;
+ boolean accumulated = false;
for (int j = i + 1; j < args.length; j++) {
switch (args[j]) {
case "--proto":
@@ -3309,9 +3321,12 @@
}
break;
}
+ case "--accumulated":
+ accumulated = true;
+ break;
}
}
- dumpUsageStats(fd, pw, model, proto);
+ dumpUsageStats(fd, pw, model, proto, accumulated);
return;
} else if ("--wakeups".equals(arg)) {
mCpuWakeupStats.dump(new IndentingPrintWriter(pw, " "),
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index 3334393..9b51b6a 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -194,4 +194,15 @@
metadata {
purpose: PURPOSE_BUGFIX
}
+}
+
+flag {
+ name: "logcat_longer_timeout"
+ namespace: "backstage_power"
+ description: "Wait longer during the logcat gathering operation"
+ bug: "292533246"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index ce92dfb..b421264 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -605,7 +605,11 @@
break;
case BluetoothProfile.LE_AUDIO:
if (mLeAudio != null && mLeAudioCallback != null) {
- mLeAudio.unregisterCallback(mLeAudioCallback);
+ try {
+ mLeAudio.unregisterCallback(mLeAudioCallback);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while unregistering callback for LE audio", e);
+ }
}
mLeAudio = null;
mLeAudioCallback = null;
@@ -682,12 +686,21 @@
return;
}
if (mLeAudio != null && mLeAudioCallback != null) {
- mLeAudio.unregisterCallback(mLeAudioCallback);
+ try {
+ mLeAudio.unregisterCallback(mLeAudioCallback);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while unregistering callback for LE audio", e);
+ }
}
mLeAudio = (BluetoothLeAudio) proxy;
mLeAudioCallback = new MyLeAudioCallback();
- mLeAudio.registerCallback(
- mContext.getMainExecutor(), mLeAudioCallback);
+ try{
+ mLeAudio.registerCallback(
+ mContext.getMainExecutor(), mLeAudioCallback);
+ } catch (Exception e) {
+ mLeAudioCallback = null;
+ Log.e(TAG, "Exception while registering callback for LE audio", e);
+ }
break;
case BluetoothProfile.A2DP_SINK:
case BluetoothProfile.LE_AUDIO_BROADCAST:
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index dc263c5..af9c9ac 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -1080,7 +1080,7 @@
*/
public float[] getNits() {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mNits;
+ return mEvenDimmerBrightnessData.nits;
}
return mNits;
}
@@ -1093,7 +1093,7 @@
@VisibleForTesting
public float[] getBacklight() {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mBacklight;
+ return mEvenDimmerBrightnessData.backlight;
}
return mBacklight;
}
@@ -1107,7 +1107,7 @@
*/
public float getBacklightFromBrightness(float brightness) {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mBrightnessToBacklight.interpolate(brightness);
+ return mEvenDimmerBrightnessData.brightnessToBacklight.interpolate(brightness);
}
return mBrightnessToBacklightSpline.interpolate(brightness);
}
@@ -1120,7 +1120,7 @@
*/
public float getBrightnessFromBacklight(float backlight) {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mBacklightToBrightness.interpolate(backlight);
+ return mEvenDimmerBrightnessData.backlightToBrightness.interpolate(backlight);
}
return mBacklightToBrightnessSpline.interpolate(backlight);
}
@@ -1131,7 +1131,7 @@
*/
private Spline getBacklightToBrightnessSpline() {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mBacklightToBrightness;
+ return mEvenDimmerBrightnessData.backlightToBrightness;
}
return mBacklightToBrightnessSpline;
}
@@ -1144,11 +1144,11 @@
*/
public float getNitsFromBacklight(float backlight) {
if (mEvenDimmerBrightnessData != null) {
- if (mEvenDimmerBrightnessData.mBacklightToNits == null) {
+ if (mEvenDimmerBrightnessData.backlightToNits == null) {
return INVALID_NITS;
}
backlight = Math.max(backlight, mBacklightMinimum);
- return mEvenDimmerBrightnessData.mBacklightToNits.interpolate(backlight);
+ return mEvenDimmerBrightnessData.backlightToNits.interpolate(backlight);
}
if (mBacklightToNitsSpline == null) {
@@ -1165,14 +1165,14 @@
*/
public float getBacklightFromNits(float nits) {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mNitsToBacklight.interpolate(nits);
+ return mEvenDimmerBrightnessData.nitsToBacklight.interpolate(nits);
}
return mNitsToBacklightSpline.interpolate(nits);
}
private Spline getNitsToBacklightSpline() {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mNitsToBacklight;
+ return mEvenDimmerBrightnessData.nitsToBacklight;
}
return mNitsToBacklightSpline;
}
@@ -1186,7 +1186,7 @@
if (mEvenDimmerBrightnessData == null) {
return INVALID_NITS;
}
- return mEvenDimmerBrightnessData.mMinLuxToNits.interpolate(lux);
+ return mEvenDimmerBrightnessData.minLuxToNits.interpolate(lux);
}
/**
@@ -1197,7 +1197,7 @@
if (mEvenDimmerBrightnessData == null) {
return PowerManager.BRIGHTNESS_MIN;
}
- return mEvenDimmerBrightnessData.mTransitionPoint;
+ return mEvenDimmerBrightnessData.transitionPoint;
}
/**
@@ -1268,7 +1268,7 @@
*/
public float[] getBrightness() {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mBrightness;
+ return mEvenDimmerBrightnessData.brightness;
}
return mBrightness;
}
@@ -2617,13 +2617,13 @@
List<NonNegativeFloatToFloatPoint> points = map.getMap().getPoint();
for (NonNegativeFloatToFloatPoint point : points) {
float lux = point.getFirst().floatValue();
- float maxBrightness = point.getSecond().floatValue();
- if (maxBrightness > hbmTransitionPoint) {
+ float maxBacklight = point.getSecond().floatValue();
+ if (maxBacklight > hbmTransitionPoint) {
Slog.wtf(TAG,
- "Invalid NBM config: maxBrightness is greater than hbm"
+ "Invalid NBM config: maxBacklight is greater than hbm"
+ ".transitionPoint. type="
- + type + "; lux=" + lux + "; maxBrightness="
- + maxBrightness);
+ + type + "; lux=" + lux + "; maxBacklight="
+ + maxBacklight);
continue;
}
if (luxToTransitionPointMap.containsKey(lux)) {
@@ -2632,8 +2632,7 @@
+ lux);
continue;
}
- luxToTransitionPointMap.put(lux,
- getBrightnessFromBacklight(maxBrightness));
+ luxToTransitionPointMap.put(lux, getBrightnessFromBacklight(maxBacklight));
}
if (!luxToTransitionPointMap.isEmpty()) {
mLuxThrottlingData.put(mappedType, luxToTransitionPointMap);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 711bc79..62d8761 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -22,6 +22,7 @@
import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
+import static com.android.server.display.brightness.BrightnessEvent.FLAG_EVEN_DIMMER;
import static com.android.server.display.config.DisplayBrightnessMappingConfig.autoBrightnessPresetToString;
import android.animation.Animator;
@@ -1755,6 +1756,8 @@
final float brightnessOnAvailableScale = MathUtils.constrainedMap(0.0f, 1.0f,
clampedState.getMinBrightness(), clampedMax,
brightnessState);
+ final boolean evenDimmerModeOn =
+ mCdsi != null && mCdsi.getReduceBrightColorsActivatedForEvenDimmer();
mTempBrightnessEvent.setPercent(Math.round(
1000.0f * com.android.internal.display.BrightnessUtils.convertLinearToGamma(
brightnessOnAvailableScale) / 10)); // rounded to one dp
@@ -1769,7 +1772,8 @@
mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode());
mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags()
| (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0)
- | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0));
+ | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0)
+ | (evenDimmerModeOn ? FLAG_EVEN_DIMMER : 0));
mTempBrightnessEvent.setRbcStrength(mCdsi != null
? mCdsi.getReduceBrightColorsStrength() : -1);
mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor);
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 0570b2a..5edea0a 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -37,9 +37,9 @@
import android.os.Trace;
import android.util.DisplayUtils;
import android.util.LongSparseArray;
-import android.util.MathUtils;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.Spline;
import android.view.Display;
import android.view.DisplayAddress;
import android.view.DisplayCutout;
@@ -81,10 +81,6 @@
private static final String UNIQUE_ID_PREFIX = "local:";
private static final String PROPERTY_EMULATOR_CIRCULAR = "ro.boot.emulator.circular";
- // Min and max strengths for even dimmer feature.
- private static final float EVEN_DIMMER_MIN_STRENGTH = 0.0f;
- private static final float EVEN_DIMMER_MAX_STRENGTH = 90.0f;
- private static final float BRIGHTNESS_MIN = 0.0f;
private final LongSparseArray<LocalDisplayDevice> mDevices = new LongSparseArray<>();
@@ -99,7 +95,9 @@
private Context mOverlayContext;
private int mEvenDimmerStrength = -1;
+ private boolean mEvenDimmerEnabled = false;
private ColorDisplayService.ColorDisplayServiceInternal mCdsi;
+ private Spline mNitsToEvenDimmerStrength;
// Called with SyncRoot lock held.
LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context,
@@ -279,7 +277,7 @@
mIsFirstDisplay = isFirstDisplay;
updateDisplayPropertiesLocked(staticDisplayInfo, dynamicInfo, modeSpecs);
mSidekickInternal = LocalServices.getService(SidekickInternal.class);
- mBacklightAdapter = new BacklightAdapter(displayToken, isFirstDisplay,
+ mBacklightAdapter = mInjector.getBacklightAdapter(displayToken, isFirstDisplay,
mSurfaceControlProxy);
mActiveSfDisplayModeAtStartId = dynamicInfo.activeDisplayModeId;
}
@@ -998,26 +996,36 @@
}
private void applyColorMatrixBasedDimming(float brightnessState) {
- int strength = (int) (MathUtils.constrainedMap(
- // to this range:
- EVEN_DIMMER_MAX_STRENGTH, EVEN_DIMMER_MIN_STRENGTH,
- // from this range:
- BRIGHTNESS_MIN, mDisplayDeviceConfig.getEvenDimmerTransitionPoint(),
- // map this (+ rounded up):
- brightnessState) + 0.5);
-
- if (mEvenDimmerStrength < 0 // uninitialised
- || MathUtils.abs(mEvenDimmerStrength - strength) > 1
- || strength <= 1) {
- mEvenDimmerStrength = strength;
- }
- boolean enabled = mEvenDimmerStrength > 0.0f;
-
if (mCdsi == null) {
mCdsi = LocalServices.getService(
ColorDisplayService.ColorDisplayServiceInternal.class);
}
- if (mCdsi != null) {
+ if (mCdsi == null) {
+ return;
+ }
+
+ final float minHardwareNits = backlightToNits(brightnessToBacklight(
+ mDisplayDeviceConfig.getEvenDimmerTransitionPoint()));
+ final float requestedNits =
+ backlightToNits(brightnessToBacklight(brightnessState));
+ mNitsToEvenDimmerStrength =
+ mCdsi.fetchEvenDimmerSpline(minHardwareNits);
+
+ if (mNitsToEvenDimmerStrength == null) {
+ return;
+ }
+
+ // Find required dimming strength, rounded up.
+ int strength = Math.round(mNitsToEvenDimmerStrength
+ .interpolate(requestedNits));
+ boolean enabled = strength > 0.0f;
+ if (mEvenDimmerEnabled != enabled) {
+ Slog.i(TAG, "Setting Extra Dim; strength: " + strength
+ + ", " + (enabled ? "enabled" : "disabled"));
+ }
+ if (mEvenDimmerStrength != strength || mEvenDimmerEnabled != enabled) {
+ mEvenDimmerEnabled = enabled;
+ mEvenDimmerStrength = strength;
mCdsi.applyEvenDimmerColorChanges(enabled, strength);
}
}
@@ -1290,6 +1298,9 @@
pw.println("DisplayDeviceConfig: ");
pw.println("---------------------");
pw.println(mDisplayDeviceConfig);
+ pw.println("mEvenDimmerEnabled=" + mEvenDimmerEnabled);
+ pw.println("mEvenDimmerStrength=" + mEvenDimmerStrength);
+ pw.println("mNitsToEvenDimmerStrength=" + mNitsToEvenDimmerStrength);
}
private int findSfDisplayModeIdLocked(int displayModeId, int modeGroup) {
@@ -1461,6 +1472,12 @@
long physicalDisplayId, boolean isFirstDisplay, DisplayManagerFlags flags) {
return DisplayDeviceConfig.create(context, physicalDisplayId, isFirstDisplay, flags);
}
+
+ public BacklightAdapter getBacklightAdapter(IBinder displayToken, boolean isFirstDisplay,
+ SurfaceControlProxy surfaceControlProxy) {
+ return new BacklightAdapter(displayToken, isFirstDisplay, surfaceControlProxy);
+
+ }
}
public interface DisplayEventListener {
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
index ad57ebf..9e9b899 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
@@ -44,6 +44,7 @@
public static final int FLAG_DOZE_SCALE = 0x4;
public static final int FLAG_USER_SET = 0x8;
public static final int FLAG_LOW_POWER_MODE = 0x20;
+ public static final int FLAG_EVEN_DIMMER = 0x40;
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
@@ -492,6 +493,7 @@
+ ((mFlags & FLAG_RBC) != 0 ? "rbc " : "")
+ ((mFlags & FLAG_INVALID_LUX) != 0 ? "invalid_lux " : "")
+ ((mFlags & FLAG_DOZE_SCALE) != 0 ? "doze_scale " : "")
- + ((mFlags & FLAG_LOW_POWER_MODE) != 0 ? "low_power_mode " : "");
+ + ((mFlags & FLAG_LOW_POWER_MODE) != 0 ? "low_power_mode " : "")
+ + ((mFlags & FLAG_EVEN_DIMMER) != 0 ? "even_dimmer " : "");
}
}
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index bd759f3..dc59e66 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -70,6 +70,7 @@
import android.util.MathUtils;
import android.util.Slog;
import android.util.SparseIntArray;
+import android.util.Spline;
import android.view.Display;
import android.view.SurfaceControl;
import android.view.accessibility.AccessibilityManager;
@@ -114,6 +115,8 @@
Matrix.setIdentityM(MATRIX_IDENTITY, 0);
}
+ private static final int EVEN_DIMMER_MAX_PERCENT_ALLOWED = 100;
+
private static final int MSG_USER_CHANGED = 0;
private static final int MSG_SET_UP = 1;
private static final int MSG_APPLY_NIGHT_DISPLAY_IMMEDIATE = 2;
@@ -193,6 +196,9 @@
private final boolean mVisibleBackgroundUsersEnabled;
private final UserManagerService mUserManager;
+ private Spline mEvenDimmerSpline;
+ private boolean mEvenDimmerActivated;
+
public ColorDisplayService(Context context) {
super(context);
mHandler = new TintHandler(DisplayThread.get().getLooper());
@@ -1625,6 +1631,16 @@
}
/**
+ * Gets the adjusted nits, given a strength and nits.
+ * @param strength of reduce bright colors
+ * @param nits target nits
+ * @return the actual nits that would be output, given input nits and rbc strength.
+ */
+ public float getAdjustedNitsForStrength(float nits, int strength) {
+ return mReduceBrightColorsTintController.getAdjustedNitsForStrength(nits, strength);
+ }
+
+ /**
* Sets the listener and returns whether reduce bright colors is currently enabled.
*/
public boolean setReduceBrightColorsListener(ReduceBrightColorsListener listener) {
@@ -1644,6 +1660,14 @@
}
/**
+ *
+ * @return whether reduce bright colors is on, due to even dimmer being activated
+ */
+ public boolean getReduceBrightColorsActivatedForEvenDimmer() {
+ return mEvenDimmerActivated;
+ }
+
+ /**
* Gets the computed brightness, in nits, when the reduce bright colors feature is applied
* at the current strength.
*
@@ -1667,10 +1691,42 @@
* Applies tint changes for even dimmer feature.
*/
public void applyEvenDimmerColorChanges(boolean enabled, int strength) {
+ mEvenDimmerActivated = enabled;
mReduceBrightColorsTintController.setActivated(enabled);
mReduceBrightColorsTintController.setMatrix(strength);
mHandler.sendEmptyMessage(MSG_APPLY_REDUCE_BRIGHT_COLORS);
}
+
+ /**
+ * Get spline to map between requested nits, and required even dimmer strength.
+ * @return nits to strength spline
+ */
+ public Spline fetchEvenDimmerSpline(float nits) {
+ if (mEvenDimmerSpline == null) {
+ mEvenDimmerSpline = createNitsToStrengthSpline(nits);
+ }
+ return mEvenDimmerSpline;
+ }
+
+ /**
+ * Creates a spline mapping requested nits values, for each resulting strength value
+ * (in percent) for even dimmer.
+ * Returns null if coefficients are not initialised.
+ * @return spline from nits to strength
+ */
+ private Spline createNitsToStrengthSpline(float nits) {
+ final float[] requestedNits = new float[EVEN_DIMMER_MAX_PERCENT_ALLOWED + 1];
+ final float[] resultingStrength = new float[EVEN_DIMMER_MAX_PERCENT_ALLOWED + 1];
+ for (int i = 0; i <= EVEN_DIMMER_MAX_PERCENT_ALLOWED; i++) {
+ resultingStrength[EVEN_DIMMER_MAX_PERCENT_ALLOWED - i] = i;
+ requestedNits[EVEN_DIMMER_MAX_PERCENT_ALLOWED - i] =
+ getAdjustedNitsForStrength(nits, i);
+ if (requestedNits[EVEN_DIMMER_MAX_PERCENT_ALLOWED - i] == 0) {
+ return null;
+ }
+ }
+ return new Spline.LinearSpline(requestedNits, resultingStrength);
+ }
}
/**
diff --git a/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java b/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java
index f529c4c..4f6fbd0 100644
--- a/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java
+++ b/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java
@@ -123,6 +123,14 @@
return computeComponentValue(mStrength) * nits;
}
+ /**
+ * Returns the effective brightness (in nits), which has been adjusted to account for the effect
+ * of the bright color reduction.
+ */
+ public float getAdjustedNitsForStrength(float nits, int strength) {
+ return computeComponentValue(strength) * nits;
+ }
+
private float computeComponentValue(int strengthLevel) {
final float percentageStrength = strengthLevel / 100f;
final float squaredPercentageStrength = percentageStrength * percentageStrength;
diff --git a/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java b/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java
index 7e2e10a..9a590a2 100644
--- a/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java
+++ b/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java
@@ -34,72 +34,76 @@
/**
* Brightness value at which even dimmer methods are used.
*/
- public final float mTransitionPoint;
+ public final float transitionPoint;
/**
* Nits array, maps to mBacklight
*/
- public final float[] mNits;
+ public final float[] nits;
/**
* Backlight array, maps to mBrightness and mNits
*/
- public final float[] mBacklight;
+ public final float[] backlight;
/**
* Brightness array, maps to mBacklight
*/
- public final float[] mBrightness;
+ public final float[] brightness;
+
/**
* Spline, mapping between backlight and nits
*/
- public final Spline mBacklightToNits;
+ public final Spline backlightToNits;
+
/**
* Spline, mapping between nits and backlight
*/
- public final Spline mNitsToBacklight;
+ public final Spline nitsToBacklight;
+
/**
* Spline, mapping between brightness and backlight
*/
- public final Spline mBrightnessToBacklight;
+ public final Spline brightnessToBacklight;
+
/**
* Spline, mapping between backlight and brightness
*/
- public final Spline mBacklightToBrightness;
+ public final Spline backlightToBrightness;
/**
* Spline, mapping the minimum nits for each lux condition.
*/
- public final Spline mMinLuxToNits;
+ public final Spline minLuxToNits;
@VisibleForTesting
public EvenDimmerBrightnessData(float transitionPoint, float[] nits,
float[] backlight, float[] brightness, Spline backlightToNits,
Spline nitsToBacklight, Spline brightnessToBacklight, Spline backlightToBrightness,
Spline minLuxToNits) {
- mTransitionPoint = transitionPoint;
- mNits = nits;
- mBacklight = backlight;
- mBrightness = brightness;
- mBacklightToNits = backlightToNits;
- mNitsToBacklight = nitsToBacklight;
- mBrightnessToBacklight = brightnessToBacklight;
- mBacklightToBrightness = backlightToBrightness;
- mMinLuxToNits = minLuxToNits;
+ this.transitionPoint = transitionPoint;
+ this.nits = nits;
+ this.backlight = backlight;
+ this.brightness = brightness;
+ this.backlightToNits = backlightToNits;
+ this.nitsToBacklight = nitsToBacklight;
+ this.brightnessToBacklight = brightnessToBacklight;
+ this.backlightToBrightness = backlightToBrightness;
+ this.minLuxToNits = minLuxToNits;
}
@Override
public String toString() {
return "EvenDimmerBrightnessData {"
- + "mTransitionPoint: " + mTransitionPoint
- + ", mNits: " + Arrays.toString(mNits)
- + ", mBacklight: " + Arrays.toString(mBacklight)
- + ", mBrightness: " + Arrays.toString(mBrightness)
- + ", mBacklightToNits: " + mBacklightToNits
- + ", mNitsToBacklight: " + mNitsToBacklight
- + ", mBrightnessToBacklight: " + mBrightnessToBacklight
- + ", mBacklightToBrightness: " + mBacklightToBrightness
- + ", mMinLuxToNits: " + mMinLuxToNits
+ + "transitionPoint: " + transitionPoint
+ + ", nits: " + Arrays.toString(nits)
+ + ", backlight: " + Arrays.toString(backlight)
+ + ", brightness: " + Arrays.toString(brightness)
+ + ", backlightToNits: " + backlightToNits
+ + ", nitsToBacklight: " + nitsToBacklight
+ + ", brightnessToBacklight: " + brightnessToBacklight
+ + ", backlightToBrightness: " + backlightToBrightness
+ + ", minLuxToNits: " + minLuxToNits
+ "} ";
}
@@ -122,7 +126,6 @@
if (map == null) {
return null;
}
- String interpolation = map.getInterpolation();
List<BrightnessPoint> brightnessPoints = map.getBrightnessPoint();
if (brightnessPoints.isEmpty()) {
@@ -169,22 +172,11 @@
++i;
}
- // Explicitly choose linear interpolation.
- if ("linear".equals(interpolation)) {
- return new EvenDimmerBrightnessData(transitionPoint, nits, backlight, brightness,
- new Spline.LinearSpline(backlight, nits),
- new Spline.LinearSpline(nits, backlight),
- new Spline.LinearSpline(brightness, backlight),
- new Spline.LinearSpline(backlight, brightness),
- new Spline.LinearSpline(minLux, minNits)
- );
- }
-
return new EvenDimmerBrightnessData(transitionPoint, nits, backlight, brightness,
- Spline.createSpline(backlight, nits),
- Spline.createSpline(nits, backlight),
- Spline.createSpline(brightness, backlight),
- Spline.createSpline(backlight, brightness),
+ new Spline.LinearSpline(backlight, nits),
+ new Spline.LinearSpline(nits, backlight),
+ new Spline.LinearSpline(brightness, backlight),
+ new Spline.LinearSpline(backlight, brightness),
Spline.createSpline(minLux, minNits)
);
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index fd7479e..909c47b 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -25,6 +25,7 @@
import static com.android.hardware.input.Flags.touchpadVisualizer;
import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
+import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER;
import android.Manifest;
import android.annotation.EnforcePermission;
@@ -117,6 +118,7 @@
import android.view.VerifiedInputEvent;
import android.view.ViewConfiguration;
import android.view.WindowManager;
+import android.view.WindowManagerPolicyConstants;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
@@ -325,6 +327,9 @@
// Manages Sticky modifier state
private final StickyModifierStateController mStickyModifierStateController;
private final KeyGestureController mKeyGestureController;
+ /** Fallback actions by key code */
+ private final SparseArray<KeyCharacterMap.FallbackAction> mFallbackActions =
+ new SparseArray<>();
// Manages Keyboard microphone mute led
private final KeyboardLedController mKeyboardLedController;
@@ -611,6 +616,7 @@
mKeyRemapper.systemRunning();
mPointerIconCache.systemRunning();
mKeyboardGlyphManager.systemRunning();
+ mKeyGestureController.systemRunning();
initKeyGestures();
}
@@ -2469,6 +2475,14 @@
mFocusEventDebugView.reportKeyEvent(event);
}
}
+ if (useKeyGestureEventHandler() && mKeyGestureController.interceptKeyBeforeQueueing(event,
+ policyFlags)) {
+ // If key gesture gets triggered, we send the event to policy with KEY_GESTURE flag
+ // indicating, the event is used in triggering a key gesture. We can't block event
+ // like Power or volume keys since policy might still want to handle it to change
+ // certain states.
+ policyFlags |= WindowManagerPolicyConstants.FLAG_KEY_GESTURE_TRIGGERED;
+ }
return mWindowManagerCallbacks.interceptKeyBeforeQueueing(event, policyFlags);
}
@@ -2511,6 +2525,74 @@
null, null, null) == PERMISSION_GRANTED;
}
+ // Native callback.
+ @SuppressWarnings("unused")
+ private KeyEvent dispatchUnhandledKey(IBinder focus, KeyEvent event, int policyFlags) {
+ if (interceptUnhandledKey(event, focus)) {
+ return null;
+ }
+ // TODO(b/358569822): Move fallback logic to KeyGestureController
+ if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) {
+ return null;
+ }
+ final KeyCharacterMap kcm = event.getKeyCharacterMap();
+ final int keyCode = event.getKeyCode();
+ final int metaState = event.getMetaState();
+ final boolean initialDown = event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getRepeatCount() == 0;
+
+ // Check for fallback actions specified by the key character map.
+ final KeyCharacterMap.FallbackAction fallbackAction;
+ if (initialDown) {
+ fallbackAction = kcm.getFallbackAction(keyCode, metaState);
+ } else {
+ fallbackAction = mFallbackActions.get(keyCode);
+ }
+
+ if (fallbackAction == null) {
+ return null;
+ }
+ KeyEvent fallbackEvent = null;
+ final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK;
+ fallbackEvent = KeyEvent.obtain(
+ event.getDownTime(), event.getEventTime(),
+ event.getAction(), fallbackAction.keyCode,
+ event.getRepeatCount(), fallbackAction.metaState,
+ event.getDeviceId(), event.getScanCode(),
+ flags, event.getSource(), event.getDisplayId(), null);
+
+ if (!interceptFallback(focus, fallbackEvent, policyFlags)) {
+ fallbackEvent.recycle();
+ fallbackEvent = null;
+ }
+
+ if (initialDown) {
+ mFallbackActions.put(keyCode, fallbackAction);
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ mFallbackActions.remove(keyCode);
+ fallbackAction.recycle();
+ }
+ return fallbackEvent;
+ }
+
+ private boolean interceptFallback(IBinder focusedToken, KeyEvent fallbackEvent,
+ int policyFlags) {
+ int actions = interceptKeyBeforeQueueing(fallbackEvent, policyFlags);
+ if ((actions & ACTION_PASS_TO_USER) == 0) {
+ return false;
+ }
+ long delayMillis = interceptKeyBeforeDispatching(focusedToken, fallbackEvent, policyFlags);
+ return delayMillis == 0 && !interceptUnhandledKey(fallbackEvent, focusedToken);
+ }
+
+ private boolean interceptUnhandledKey(KeyEvent event, IBinder focus) {
+ if (useKeyGestureEventHandler() && mKeyGestureController.interceptUnhandledKey(event,
+ focus)) {
+ return true;
+ }
+ return mWindowManagerCallbacks.interceptUnhandledKey(event, focus);
+ }
+
private void initKeyGestures() {
if (!useKeyGestureEventHandler()) {
return;
@@ -2566,12 +2648,6 @@
// Native callback.
@SuppressWarnings("unused")
- private KeyEvent dispatchUnhandledKey(IBinder focus, KeyEvent event, int policyFlags) {
- return mWindowManagerCallbacks.dispatchUnhandledKey(focus, event, policyFlags);
- }
-
- // Native callback.
- @SuppressWarnings("unused")
private void onPointerDownOutsideFocus(IBinder touchedToken) {
mWindowManagerCallbacks.onPointerDownOutsideFocus(touchedToken);
}
@@ -2986,9 +3062,9 @@
long interceptKeyBeforeDispatching(IBinder token, KeyEvent event, int policyFlags);
/**
- * Dispatch unhandled key
+ * Intercept unhandled key
*/
- KeyEvent dispatchUnhandledKey(IBinder token, KeyEvent event, int policyFlags);
+ boolean interceptUnhandledKey(KeyEvent event, IBinder token);
int getPointerLayer();
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 4538b49..b488db5 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -16,14 +16,23 @@
package com.android.server.input;
+import static android.content.pm.PackageManager.FEATURE_LEANBACK;
+import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE;
+
+import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
+import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
import static com.android.server.flags.Flags.newBugreportKeyboardShortcut;
import android.annotation.BinderThread;
import android.annotation.MainThread;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
+import android.content.ContentResolver;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
+import android.database.ContentObserver;
import android.hardware.input.AidlKeyGestureEvent;
import android.hardware.input.IKeyGestureEventListener;
import android.hardware.input.IKeyGestureHandler;
@@ -35,18 +44,23 @@
import android.os.Message;
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;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.InputDevice;
+import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.policy.KeyCombinationManager;
import java.util.ArrayDeque;
import java.util.HashSet;
@@ -82,20 +96,36 @@
private static final int SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY = 1;
private static final int LAST_SEARCH_KEY_BEHAVIOR = SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY;
+ // must match: config_keyChordPowerVolumeUp in config.xml
+ static final int POWER_VOLUME_UP_BEHAVIOR_NOTHING = 0;
+ static final int POWER_VOLUME_UP_BEHAVIOR_MUTE = 1;
+ static final int POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS = 2;
+
private final Context mContext;
private final Handler mHandler;
private final int mSystemPid;
+ private final KeyCombinationManager mKeyCombinationManager;
+ private final SettingsObserver mSettingsObserver;
// Pending actions
private boolean mPendingMetaAction;
private boolean mPendingCapsLockToggle;
private boolean mPendingHideRecentSwitcher;
- // Key behaviors
+ // Platform behaviors
private boolean mEnableBugReportKeyboardShortcut;
+ private boolean mHasFeatureWatch;
+ private boolean mHasFeatureLeanback;
+
+ // Key behaviors
private int mSearchKeyBehavior;
private int mSettingsKeyBehavior;
+ // Settings behaviors
+ private int mRingerToggleChord = Settings.Secure.VOLUME_HUSH_OFF;
+ private int mPowerVolUpBehavior;
+
+
// List of currently registered key gesture event listeners keyed by process pid
@GuardedBy("mKeyGestureEventListenerRecords")
private final SparseArray<KeyGestureEventListenerRecord>
@@ -128,12 +158,19 @@
return Integer.compare(p1, p2);
}
});
+ mKeyCombinationManager = new KeyCombinationManager(mHandler);
+ mSettingsObserver = new SettingsObserver(mHandler);
initBehaviors();
+ initKeyCombinationRules();
}
private void initBehaviors() {
mEnableBugReportKeyboardShortcut = "1".equals(SystemProperties.get("ro.debuggable"));
+ PackageManager pm = mContext.getPackageManager();
+ mHasFeatureWatch = pm.hasSystemFeature(FEATURE_WATCH);
+ mHasFeatureLeanback = pm.hasSystemFeature(FEATURE_LEANBACK);
+
Resources res = mContext.getResources();
mSearchKeyBehavior = res.getInteger(R.integer.config_searchKeyBehavior);
if (mSearchKeyBehavior < SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH
@@ -145,6 +182,251 @@
|| mSettingsKeyBehavior > LAST_SETTINGS_KEY_BEHAVIOR) {
mSettingsKeyBehavior = SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY;
}
+
+ mHandler.post(this::initBehaviorsFromSettings);
+ }
+
+ private void initBehaviorsFromSettings() {
+ ContentResolver resolver = mContext.getContentResolver();
+ mRingerToggleChord = Settings.Secure.getIntForUser(resolver,
+ Settings.Secure.VOLUME_HUSH_GESTURE, Settings.Secure.VOLUME_HUSH_OFF,
+ UserHandle.USER_CURRENT);
+
+ mPowerVolUpBehavior = Settings.Global.getInt(resolver,
+ Settings.Global.KEY_CHORD_POWER_VOLUME_UP,
+ mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_keyChordPowerVolumeUp));
+ }
+
+ private void initKeyCombinationRules() {
+ if (!useKeyGestureEventHandler() || !useKeyGestureEventHandlerMultiPressGestures()) {
+ return;
+ }
+ // TODO(b/358569822): Handle Power, Back key properly since key combination gesture is
+ // captured here and rest of the Power, Back key behaviors are handled in PWM
+ final boolean screenshotChordEnabled = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_enableScreenshotChord);
+
+ if (screenshotChordEnabled) {
+ mKeyCombinationManager.addRule(
+ new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_VOLUME_DOWN,
+ KeyEvent.KEYCODE_POWER) {
+ @Override
+ public boolean preCondition() {
+ return isKeyGestureSupported(
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
+ }
+
+ @Override
+ public void execute() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER},
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_START, 0);
+ }
+
+ @Override
+ public void cancel() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER},
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+ KeyGestureEvent.FLAG_CANCELLED);
+ }
+ });
+
+ if (mHasFeatureWatch) {
+ mKeyCombinationManager.addRule(
+ new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_POWER,
+ KeyEvent.KEYCODE_STEM_PRIMARY) {
+ @Override
+ public boolean preCondition() {
+ return isKeyGestureSupported(
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
+ }
+
+ @Override
+ public void execute() {
+ handleKeyGesture(new int[]{KeyEvent.KEYCODE_POWER,
+ KeyEvent.KEYCODE_STEM_PRIMARY},
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_START, 0);
+ }
+ @Override
+ public void cancel() {
+ handleKeyGesture(new int[]{KeyEvent.KEYCODE_POWER,
+ KeyEvent.KEYCODE_STEM_PRIMARY},
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+ KeyGestureEvent.FLAG_CANCELLED);
+ }
+ });
+ }
+ }
+
+ mKeyCombinationManager.addRule(
+ new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_VOLUME_DOWN,
+ KeyEvent.KEYCODE_VOLUME_UP) {
+ @Override
+ public boolean preCondition() {
+ return isKeyGestureSupported(
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD);
+ }
+
+ @Override
+ public void execute() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP},
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_START, 0);
+ }
+
+ @Override
+ public void cancel() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP},
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+ KeyGestureEvent.FLAG_CANCELLED);
+ }
+ });
+
+ // Volume up + power can either be the "ringer toggle chord" or as another way to
+ // launch GlobalActions. This behavior can change at runtime so we must check behavior
+ // inside the TwoKeysCombinationRule.
+ mKeyCombinationManager.addRule(
+ new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_VOLUME_UP,
+ KeyEvent.KEYCODE_POWER) {
+ @Override
+ public boolean preCondition() {
+ if (!isKeyGestureSupported(getGestureType())) {
+ return false;
+ }
+ switch (mPowerVolUpBehavior) {
+ case POWER_VOLUME_UP_BEHAVIOR_MUTE:
+ return mRingerToggleChord != Settings.Secure.VOLUME_HUSH_OFF;
+ default:
+ return true;
+ }
+ }
+ @Override
+ public void execute() {
+ int gestureType = getGestureType();
+ if (gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED) {
+ return;
+ }
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_POWER},
+ gestureType, KeyGestureEvent.ACTION_GESTURE_START, 0);
+ }
+ @Override
+ public void cancel() {
+ int gestureType = getGestureType();
+ if (gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED) {
+ return;
+ }
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_POWER},
+ gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+ KeyGestureEvent.FLAG_CANCELLED);
+ }
+
+ @KeyGestureEvent.KeyGestureType
+ private int getGestureType() {
+ switch (mPowerVolUpBehavior) {
+ case POWER_VOLUME_UP_BEHAVIOR_MUTE -> {
+ return KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD;
+ }
+ case POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS -> {
+ return KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS;
+ }
+ default -> {
+ return KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED;
+ }
+ }
+ }
+ });
+
+ if (mHasFeatureLeanback) {
+ mKeyCombinationManager.addRule(
+ new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_BACK,
+ KeyEvent.KEYCODE_DPAD_DOWN) {
+ @Override
+ public boolean preCondition() {
+ return isKeyGestureSupported(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD);
+ }
+
+ @Override
+ public void execute() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_START, 0);
+ }
+
+ @Override
+ public void cancel() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+ KeyGestureEvent.FLAG_CANCELLED);
+ }
+ @Override
+ public long getKeyInterceptDelayMs() {
+ // Use a timeout of 0 to prevent additional latency in processing of
+ // this key. This will potentially cause some unwanted UI actions if the
+ // user does end up triggering the key combination later, but in most
+ // cases, the user will simply hit a single key, and this will allow us
+ // to process it without first waiting to see if the combination is
+ // going to be triggered.
+ return 0;
+ }
+ });
+
+ mKeyCombinationManager.addRule(
+ new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_BACK,
+ KeyEvent.KEYCODE_DPAD_CENTER) {
+ @Override
+ public boolean preCondition() {
+ return isKeyGestureSupported(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
+ }
+
+ @Override
+ public void execute() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
+ KeyGestureEvent.ACTION_GESTURE_START, 0);
+ }
+ @Override
+ public void cancel() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+ KeyGestureEvent.FLAG_CANCELLED);
+ }
+ @Override
+ public long getKeyInterceptDelayMs() {
+ return 0;
+ }
+ });
+ }
+ }
+
+ public void systemRunning() {
+ mSettingsObserver.observe();
+ }
+
+ public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
+ final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
+ if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
+ return mKeyCombinationManager.interceptKey(event, interactive);
+ }
+ return false;
}
public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
@@ -153,9 +435,22 @@
// KeyGestureHandler (PWM is one of the handlers)
final int keyCode = event.getKeyCode();
final int deviceId = event.getDeviceId();
+ final int flags = event.getFlags();
final long keyConsumed = -1;
final long keyNotConsumed = 0;
+ 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;
+ }
+ }
+
Set<Integer> consumedKeys = mConsumedKeysForDevice.get(deviceId);
if (consumedKeys == null) {
consumedKeys = new HashSet<>();
@@ -577,10 +872,71 @@
return false;
}
+ boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
+ final int keyCode = event.getKeyCode();
+ final int repeatCount = event.getRepeatCount();
+ final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
+ final int metaState = event.getModifiers();
+ final int deviceId = event.getDeviceId();
+ final int displayId = event.getDisplayId();
+
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_SPACE:
+ if (down && repeatCount == 0) {
+ // Handle keyboard layout switching. (CTRL + SPACE)
+ if (KeyEvent.metaStateHasModifiers(metaState & ~KeyEvent.META_SHIFT_MASK,
+ KeyEvent.META_CTRL_ON)) {
+ return handleKeyGesture(deviceId, new int[]{keyCode},
+ KeyEvent.META_CTRL_ON | (event.isShiftPressed()
+ ? KeyEvent.META_SHIFT_ON : 0),
+ KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+ focusedToken, /* flags = */0);
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_Z:
+ if (down && KeyEvent.metaStateHasModifiers(metaState,
+ KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON)) {
+ // Intercept the Accessibility keychord (CTRL + ALT + Z) for keyboard users.
+ return handleKeyGesture(deviceId, new int[]{keyCode},
+ KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+ focusedToken, /* flags = */0);
+ }
+ break;
+ case KeyEvent.KEYCODE_SYSRQ:
+ if (down && repeatCount == 0) {
+ return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+ focusedToken, /* flags = */0);
+ }
+ break;
+ case KeyEvent.KEYCODE_ESCAPE:
+ if (down && KeyEvent.metaStateHasNoModifiers(metaState) && repeatCount == 0) {
+ return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
+ KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+ focusedToken, /* flags = */0);
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ private boolean handleKeyGesture(int[] keycodes,
+ @KeyGestureEvent.KeyGestureType int gestureType, int action, int flags) {
+ return handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, keycodes, /* modifierState= */0,
+ gestureType, action, Display.DEFAULT_DISPLAY, /* focusedToken = */null, flags);
+ }
+
@VisibleForTesting
boolean handleKeyGesture(int deviceId, int[] keycodes, int modifierState,
@KeyGestureEvent.KeyGestureType int gestureType, int action, int displayId,
- IBinder focusedToken, int flags) {
+ @Nullable IBinder focusedToken, int flags) {
return handleKeyGesture(createKeyGestureEvent(deviceId, keycodes,
modifierState, gestureType, action, displayId, flags), focusedToken);
}
@@ -628,7 +984,7 @@
@MainThread
private void notifyKeyGestureEvent(AidlKeyGestureEvent event) {
InputDevice device = getInputDevice(event.deviceId);
- if (device == null || device.isVirtual()) {
+ if (device == null) {
return;
}
if (event.action == KeyGestureEvent.ACTION_GESTURE_COMPLETE) {
@@ -822,6 +1178,27 @@
}
}
+ private class SettingsObserver extends ContentObserver {
+ private SettingsObserver(Handler handler) {
+ super(handler);
+ }
+
+ private void observe() {
+ ContentResolver resolver = mContext.getContentResolver();
+ resolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.VOLUME_HUSH_GESTURE), false, this,
+ UserHandle.USER_ALL);
+ resolver.registerContentObserver(Settings.Global.getUriFor(
+ Settings.Global.KEY_CHORD_POWER_VOLUME_UP), false, this,
+ UserHandle.USER_ALL);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ initBehaviorsFromSettings();
+ }
+ }
+
@Nullable
private InputDevice getInputDevice(int deviceId) {
InputManager inputManager = mContext.getSystemService(InputManager.class);
@@ -851,6 +1228,8 @@
ipw.println("mPendingHideRecentSwitcher = " + mPendingHideRecentSwitcher);
ipw.println("mSearchKeyBehavior = " + mSearchKeyBehavior);
ipw.println("mSettingsKeyBehavior = " + mSettingsKeyBehavior);
+ ipw.println("mRingerToggleChord = " + mRingerToggleChord);
+ ipw.println("mPowerVolUpBehavior = " + mPowerVolUpBehavior);
ipw.print("mKeyGestureEventListenerRecords = {");
synchronized (mKeyGestureEventListenerRecords) {
int size = mKeyGestureEventListenerRecords.size();
@@ -881,5 +1260,6 @@
ipw.println(ev);
}
ipw.decreaseIndent();
+ mKeyCombinationManager.dump("", ipw);
}
}
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
index a1e5ebc..cf0c5b0 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
@@ -180,7 +180,8 @@
@Override
public boolean onTouchEvent(MotionEvent event) {
- if (event.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) {
+ if (event.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
+ || event.getClassification() == MotionEvent.CLASSIFICATION_PINCH) {
return false;
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 2bb2b7b..f0fb33e 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -4799,7 +4799,8 @@
userData.mImeBindingState.mFocusedWindowEditorInfo,
info.focusedWindowName, userData.mImeBindingState.mFocusedWindowSoftInputMode,
reason, userData.mInFullscreenMode, info.requestWindowName,
- info.imeControlTargetName, info.imeLayerTargetName, info.imeSurfaceParentName));
+ info.imeControlTargetName, info.imeLayerTargetName, info.imeSurfaceParentName,
+ userId));
if (statsToken != null) {
mImeTrackerService.onImmsUpdate(statsToken, info.requestWindowName);
@@ -6132,8 +6133,7 @@
dumpAsStringNoCheckForUser(userData, fd, pw, args, isCritical);
}
- // TODO(b/365868861): Make StartInputHistory, SoftInputShowHideHistory and ImeTracker per
- // user.
+ // TODO(b/365868861): Make StartInputHistory and ImeTracker multi-user aware.
synchronized (ImfLock.class) {
p.println(" mStartInputHistory:");
mStartInputHistory.dump(pw, " ");
diff --git a/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java b/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
index 3023603..8445632 100644
--- a/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
+++ b/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.os.SystemClock;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
@@ -62,6 +63,8 @@
final String mImeTargetNameFromWm;
@Nullable
final String mImeSurfaceParentName;
+ @UserIdInt
+ final int mImeUserId;
Entry(ClientState client, EditorInfo editorInfo,
String focusedWindowName,
@@ -69,7 +72,7 @@
@SoftInputShowHideReason int reason,
boolean inFullscreenMode, String requestWindowName,
@Nullable String imeControlTargetName, @Nullable String imeTargetName,
- @Nullable String imeSurfaceParentName) {
+ @Nullable String imeSurfaceParentName, @UserIdInt int imeUserId) {
mClientState = client;
mEditorInfo = editorInfo;
mFocusedWindowName = focusedWindowName;
@@ -82,6 +85,7 @@
mImeControlTargetName = imeControlTargetName;
mImeTargetNameFromWm = imeTargetName;
mImeSurfaceParentName = imeSurfaceParentName;
+ mImeUserId = imeUserId;
}
}
@@ -102,7 +106,8 @@
continue;
}
pw.print(prefix);
- pw.println("SoftInputShowHide #" + entry.mSequenceNumber + ":");
+ pw.println("SoftInputShowHide[" + entry.mImeUserId + "] #"
+ + entry.mSequenceNumber + ":");
pw.print(prefix);
pw.println(" time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime))
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index bcc2019..c3a714b 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -4136,14 +4136,18 @@
}
+ /**
+ * Any changes from SystemUI or Settings should be fromUser == true. Any changes the
+ * allowlist should be fromUser == false.
+ */
@Override
@FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
- public void setCanBePromoted(String pkg, int uid, boolean promote) {
+ public void setCanBePromoted(String pkg, int uid, boolean promote, boolean fromUser) {
checkCallerIsSystemOrSystemUiOrShell();
if (!android.app.Flags.apiRichOngoing()) {
return;
}
- boolean changed = mPreferencesHelper.setCanBePromoted(pkg, uid, promote);
+ boolean changed = mPreferencesHelper.setCanBePromoted(pkg, uid, promote, fromUser);
if (changed) {
// check for pending/posted notifs from this app and update the flag
synchronized (mNotificationLock) {
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index fdb9f67..85c3957 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -40,6 +40,7 @@
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
+import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_PROMOTABLE;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
@@ -192,10 +193,13 @@
/**
* All user-lockable fields for a given application.
*/
- @IntDef({LockableAppFields.USER_LOCKED_IMPORTANCE})
+ @IntDef({LockableAppFields.USER_LOCKED_IMPORTANCE,
+ LockableAppFields.USER_LOCKED_BUBBLE,
+ LockableAppFields.USER_LOCKED_PROMOTABLE})
public @interface LockableAppFields {
int USER_LOCKED_IMPORTANCE = 0x00000001;
int USER_LOCKED_BUBBLE = 0x00000002;
+ int USER_LOCKED_PROMOTABLE = 0x00000004;
}
private final Object mLock = new Object();
@@ -858,13 +862,19 @@
}
@FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
- public boolean setCanBePromoted(String packageName, int uid, boolean promote) {
+ public boolean setCanBePromoted(String packageName, int uid, boolean promote,
+ boolean fromUser) {
boolean changed = false;
synchronized (mLock) {
PackagePreferences pkgPrefs = getOrCreatePackagePreferencesLocked(packageName, uid);
- if (pkgPrefs.canHavePromotedNotifs != promote) {
- pkgPrefs.canHavePromotedNotifs = promote;
- changed = true;
+ if (fromUser || ((pkgPrefs.lockedAppFields & USER_LOCKED_PROMOTABLE) == 0)) {
+ if (pkgPrefs.canHavePromotedNotifs != promote) {
+ pkgPrefs.canHavePromotedNotifs = promote;
+ if (fromUser) {
+ pkgPrefs.lockedAppFields |= USER_LOCKED_PROMOTABLE;
+ }
+ changed = true;
+ }
}
}
// no need to send a ranking update because we need to update the flag value on all pending
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index f6d9dc2..03a34f2 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -87,6 +87,7 @@
import com.android.internal.os.BackgroundThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
+import com.android.server.SystemService.TargetUser;
import com.android.server.ondeviceintelligence.callbacks.ListenableDownloadCallback;
import java.io.FileDescriptor;
@@ -194,9 +195,13 @@
mIsServiceEnabled = isServiceEnabled();
}
+ }
- //connect to remote services(if available) during boot phase.
- if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
+ @Override
+ public void onUserUnlocked(@NonNull TargetUser user) {
+ Slog.d(TAG, "onUserUnlocked: " + user.getUserHandle());
+ //connect to remote services(if available) during boot.
+ if(user.getUserHandle().equals(UserHandle.SYSTEM)) {
try {
ensureRemoteInferenceServiceInitialized();
ensureRemoteIntelligenceServiceInitialized();
diff --git a/services/core/java/com/android/server/pdb/PersistentDataBlockService.java b/services/core/java/com/android/server/pdb/PersistentDataBlockService.java
index 8410cff..fe9a859 100644
--- a/services/core/java/com/android/server/pdb/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/pdb/PersistentDataBlockService.java
@@ -171,7 +171,6 @@
static final int MAX_DATA_BLOCK_SIZE = 1024 * 100;
public static final int DIGEST_SIZE_BYTES = 32;
- private static final String OEM_UNLOCK_PROP = "sys.oem_unlock_allowed";
private static final String FLASH_LOCK_PROP = "ro.boot.flash.locked";
private static final String FLASH_LOCK_LOCKED = "1";
private static final String FLASH_LOCK_UNLOCKED = "0";
@@ -275,7 +274,6 @@
enforceChecksumValidity();
if (mFrpEnforced) {
automaticallyDeactivateFrpIfPossible();
- setOemUnlockEnabledProperty(doGetOemUnlockEnabled());
setOldSettingForBackworkCompatibility(mFrpActive);
} else {
formatIfOemUnlockEnabled();
@@ -303,10 +301,6 @@
}
}
- private void setOemUnlockEnabledProperty(boolean oemUnlockEnabled) {
- setProperty(OEM_UNLOCK_PROP, oemUnlockEnabled ? "1" : "0");
- }
-
@Override
public void onBootPhase(int phase) {
// Wait for initialization in onStart to finish
@@ -342,7 +336,6 @@
formatPartitionLocked(true);
}
}
- setOemUnlockEnabledProperty(enabled);
}
private void enforceOemUnlockReadPermission() {
@@ -814,17 +807,9 @@
channel.force(true);
} catch (IOException e) {
Slog.e(TAG, "unable to access persistent partition", e);
- return;
- } finally {
- setOemUnlockEnabledProperty(enabled);
}
}
- @VisibleForTesting
- void setProperty(String name, String value) {
- SystemProperties.set(name, value);
- }
-
private boolean doGetOemUnlockEnabled() {
DataInputStream inputStream;
try {
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 4665a72..89ced12 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -2208,10 +2208,10 @@
return true;
}
boolean permissionGranted = requireFullPermission ? hasPermission(
- Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
: (hasPermission(
- android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
- || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS));
+ android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
+ || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS, callingUid));
if (!permissionGranted) {
if (Process.isIsolatedUid(callingUid) && isKnownIsolatedComputeApp(callingUid)) {
return checkIsolatedOwnerHasPermission(callingUid, requireFullPermission);
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index 5653da0..8657de2 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -716,7 +716,7 @@
visiblePackages.add(info.getActivityInfo().packageName);
}
final List<ApplicationInfo> installedPackages =
- mPackageManagerInternal.getInstalledApplications(
+ mPackageManagerInternal.getInstalledApplicationsCrossUser(
/* flags= */ 0, user.getIdentifier(), callingUid);
for (ApplicationInfo applicationInfo : installedPackages) {
if (!visiblePackages.contains(applicationInfo.packageName)) {
diff --git a/services/core/java/com/android/server/policy/KeyCombinationManager.java b/services/core/java/com/android/server/policy/KeyCombinationManager.java
index 9dfaca8..1592ef3 100644
--- a/services/core/java/com/android/server/policy/KeyCombinationManager.java
+++ b/services/core/java/com/android/server/policy/KeyCombinationManager.java
@@ -65,21 +65,21 @@
* };
* </pre>
*/
- abstract static class TwoKeysCombinationRule {
+ public abstract static class TwoKeysCombinationRule {
private int mKeyCode1;
private int mKeyCode2;
- TwoKeysCombinationRule(int keyCode1, int keyCode2) {
+ public TwoKeysCombinationRule(int keyCode1, int keyCode2) {
mKeyCode1 = keyCode1;
mKeyCode2 = keyCode2;
}
- boolean preCondition() {
+ public boolean preCondition() {
return true;
}
boolean shouldInterceptKey(int keyCode) {
- return preCondition() && (keyCode == mKeyCode1 || keyCode == mKeyCode2);
+ return (keyCode == mKeyCode1 || keyCode == mKeyCode2) && preCondition();
}
boolean shouldInterceptKeys(SparseLongArray downTimes) {
@@ -94,12 +94,12 @@
}
// The excessive delay before it dispatching to client.
- long getKeyInterceptDelayMs() {
+ public long getKeyInterceptDelayMs() {
return COMBINE_KEY_DELAY_MILLIS;
}
- abstract void execute();
- abstract void cancel();
+ public abstract void execute();
+ public abstract void cancel();
@Override
public String toString() {
@@ -128,18 +128,18 @@
}
}
- KeyCombinationManager(Handler handler) {
+ public KeyCombinationManager(Handler handler) {
mHandler = handler;
}
- void addRule(TwoKeysCombinationRule rule) {
+ public void addRule(TwoKeysCombinationRule rule) {
if (mRules.contains(rule)) {
throw new IllegalArgumentException("Rule : " + rule + " already exists.");
}
mRules.add(rule);
}
- void removeRule(TwoKeysCombinationRule rule) {
+ public void removeRule(TwoKeysCombinationRule rule) {
mRules.remove(rule);
}
@@ -148,7 +148,7 @@
* to a window.
* Return true if any active rule could be triggered by the key event, otherwise false.
*/
- boolean interceptKey(KeyEvent event, boolean interactive) {
+ public boolean interceptKey(KeyEvent event, boolean interactive) {
synchronized (mLock) {
return interceptKeyLocked(event, interactive);
}
@@ -226,7 +226,7 @@
/**
* Return the interceptTimeout to tell InputDispatcher when is ready to deliver to window.
*/
- long getKeyInterceptTimeout(int keyCode) {
+ public long getKeyInterceptTimeout(int keyCode) {
synchronized (mLock) {
if (mDownTimes.get(keyCode) == 0) {
return 0;
@@ -246,7 +246,7 @@
/**
* True if the key event had been handled.
*/
- boolean isKeyConsumed(KeyEvent event) {
+ public boolean isKeyConsumed(KeyEvent event) {
synchronized (mLock) {
if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) {
return false;
@@ -258,7 +258,7 @@
/**
* True if power key is the candidate.
*/
- boolean isPowerKeyIntercepted() {
+ public boolean isPowerKeyIntercepted() {
synchronized (mLock) {
if (forAllActiveRules((rule) -> rule.shouldInterceptKey(KEYCODE_POWER))) {
// return false if only if power key pressed.
@@ -294,7 +294,7 @@
return false;
}
- void dump(String prefix, PrintWriter pw) {
+ public void dump(String prefix, PrintWriter pw) {
pw.println(prefix + "KeyCombination rules:");
forAllRules(mRules, (rule)-> {
pw.println(prefix + " " + rule);
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index ca60518..2284050 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -86,6 +86,7 @@
import static com.android.hardware.input.Flags.emojiAndScreenshotKeycodesAvailable;
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;
@@ -198,7 +199,6 @@
import android.view.IDisplayFoldListener;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
-import android.view.KeyCharacterMap.FallbackAction;
import android.view.KeyEvent;
import android.view.KeyboardShortcutGroup;
import android.view.MotionEvent;
@@ -698,10 +698,6 @@
// Maps global key codes to the components that will handle them.
private GlobalKeyManager mGlobalKeyManager;
- // Fallback actions by key code.
- private final SparseArray<KeyCharacterMap.FallbackAction> mFallbackActions =
- new SparseArray<KeyCharacterMap.FallbackAction>();
-
private final com.android.internal.policy.LogDecelerateInterpolator mLogDecelerateInterpolator
= new LogDecelerateInterpolator(100, 0);
private final DeferredKeyActionExecutor mDeferredKeyActionExecutor =
@@ -1056,7 +1052,8 @@
return handled;
}
- private void interceptPowerKeyDown(KeyEvent event, boolean interactive) {
+ private void interceptPowerKeyDown(KeyEvent event, boolean interactive,
+ boolean isKeyGestureTriggered) {
// Hold a wake lock until the power key is released.
if (!mPowerKeyWakeLock.isHeld()) {
mPowerKeyWakeLock.acquire();
@@ -1089,7 +1086,8 @@
// If the power key has still not yet been handled, then detect short
// press, long press, or multi press and decide what to do.
mPowerKeyHandled = mPowerKeyHandled || hungUp
- || handledByPowerManager || mKeyCombinationManager.isPowerKeyIntercepted();
+ || handledByPowerManager || isKeyGestureTriggered
+ || mKeyCombinationManager.isPowerKeyIntercepted();
if (!mPowerKeyHandled) {
if (!interactive) {
wakeUpFromWakeKey(event);
@@ -2465,6 +2463,9 @@
private void initKeyCombinationRules() {
mKeyCombinationManager = new KeyCombinationManager(mHandler);
+ if (useKeyGestureEventHandler() && useKeyGestureEventHandlerMultiPressGestures()) {
+ return;
+ }
final boolean screenshotChordEnabled = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_enableScreenshotChord);
@@ -2472,13 +2473,13 @@
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_POWER) {
@Override
- void execute() {
+ public void execute() {
mPowerKeyHandled = true;
interceptScreenshotChord(
SCREENSHOT_KEY_CHORD, getScreenshotChordLongPressDelay());
}
@Override
- void cancel() {
+ public void cancel() {
cancelPendingScreenshotChordAction();
}
});
@@ -2487,13 +2488,13 @@
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_POWER, KEYCODE_STEM_PRIMARY) {
@Override
- void execute() {
+ public void execute() {
mPowerKeyHandled = true;
interceptScreenshotChord(SCREENSHOT_KEY_CHORD,
getScreenshotChordLongPressDelay());
}
@Override
- void cancel() {
+ public void cancel() {
cancelPendingScreenshotChordAction();
}
});
@@ -2503,16 +2504,16 @@
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_VOLUME_UP) {
@Override
- boolean preCondition() {
+ public boolean preCondition() {
return mAccessibilityShortcutController
.isAccessibilityShortcutAvailable(isKeyguardLocked());
}
@Override
- void execute() {
+ public void execute() {
interceptAccessibilityShortcutChord();
}
@Override
- void cancel() {
+ public void cancel() {
cancelPendingAccessibilityShortcutAction();
}
});
@@ -2523,7 +2524,7 @@
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_VOLUME_UP, KEYCODE_POWER) {
@Override
- boolean preCondition() {
+ public boolean preCondition() {
switch (mPowerVolUpBehavior) {
case POWER_VOLUME_UP_BEHAVIOR_MUTE:
return mRingerToggleChord != VOLUME_HUSH_OFF;
@@ -2532,7 +2533,7 @@
}
}
@Override
- void execute() {
+ public void execute() {
switch (mPowerVolUpBehavior) {
case POWER_VOLUME_UP_BEHAVIOR_MUTE:
// no haptic feedback here since
@@ -2551,7 +2552,7 @@
}
}
@Override
- void cancel() {
+ public void cancel() {
switch (mPowerVolUpBehavior) {
case POWER_VOLUME_UP_BEHAVIOR_MUTE:
cancelPendingRingerToggleChordAction();
@@ -2567,16 +2568,16 @@
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_BACK, KEYCODE_DPAD_DOWN) {
@Override
- void execute() {
+ public void execute() {
mBackKeyHandled = true;
interceptAccessibilityGestureTv();
}
@Override
- void cancel() {
+ public void cancel() {
cancelAccessibilityGestureTv();
}
@Override
- long getKeyInterceptDelayMs() {
+ public long getKeyInterceptDelayMs() {
// Use a timeout of 0 to prevent additional latency in processing of
// this key. This will potentially cause some unwanted UI actions if the
// user does end up triggering the key combination later, but in most
@@ -2590,16 +2591,16 @@
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_DPAD_CENTER, KEYCODE_BACK) {
@Override
- void execute() {
+ public void execute() {
mBackKeyHandled = true;
interceptBugreportGestureTv();
}
@Override
- void cancel() {
+ public void cancel() {
cancelBugreportGestureTv();
}
@Override
- long getKeyInterceptDelayMs() {
+ public long getKeyInterceptDelayMs() {
return 0;
}
});
@@ -3403,15 +3404,18 @@
+ keyguardOn() + " canceled=" + event.isCanceled());
}
- if (mKeyCombinationManager.isKeyConsumed(event)) {
- return keyConsumed;
- }
+ if (!useKeyGestureEventHandler()) {
+ 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;
+ }
}
}
@@ -3453,8 +3457,16 @@
// NOTE: Please try not to add new Shortcut combinations here and instead use KeyGestureEvents.
// Add shortcut trigger logic in {@code KeyGestureController} and add handling logic in
// {@link handleKeyGesture()}
- @SuppressLint("MissingPermission")
private boolean interceptSystemKeysAndShortcuts(IBinder focusedToken, KeyEvent event) {
+ if (useKeyGestureEventHandler()) {
+ return interceptSystemKeysAndShortcutsNew(focusedToken, event);
+ } else {
+ return interceptSystemKeysAndShortcutsOld(focusedToken, event);
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private boolean interceptSystemKeysAndShortcutsOld(IBinder focusedToken, KeyEvent event) {
final boolean keyguardOn = keyguardOn();
final int keyCode = event.getKeyCode();
final int repeatCount = event.getRepeatCount();
@@ -3878,6 +3890,59 @@
return (metaState & KeyEvent.META_META_ON) != 0;
}
+ private boolean interceptSystemKeysAndShortcutsNew(IBinder focusedToken, KeyEvent event) {
+ final int keyCode = event.getKeyCode();
+ final int metaState = event.getMetaState();
+ final boolean keyguardOn = keyguardOn();
+
+ if (isUserSetupComplete() && !keyguardOn) {
+ if (mModifierShortcutManager.interceptKey(event)) {
+ dismissKeyboardShortcutsMenu();
+ return true;
+ }
+ }
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_HOME:
+ return handleHomeShortcuts(focusedToken, event);
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_VOLUME_MUTE:
+ if (mUseTvRouting || mHandleVolumeKeysInWM) {
+ // On TVs or when the configuration is enabled, volume keys never
+ // go to the foreground app.
+ dispatchDirectAudioEvent(event);
+ return true;
+ }
+
+ // If the device is in VR mode and keys are "internal" (e.g. on the side of the
+ // device), then drop the volume keys and don't forward it to the
+ // application/dispatch the audio event.
+ if (mDefaultDisplayPolicy.isPersistentVrModeEnabled()) {
+ final InputDevice d = event.getDevice();
+ if (d != null && !d.isExternal()) {
+ return true;
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_STEM_PRIMARY:
+ if (prepareToSendSystemKeyToApplication(focusedToken, event)) {
+ // Send to app.
+ return false;
+ } else {
+ // Intercepted.
+ sendSystemKeyToStatusBarAsync(event);
+ return true;
+ }
+ }
+ if (isValidGlobalKey(keyCode)
+ && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) {
+ return true;
+ }
+
+ // Reserve all the META modifier combos for system behavior
+ return (metaState & KeyEvent.META_META_ON) != 0;
+ }
+
@SuppressLint("MissingPermission")
private void initKeyGestures() {
if (!useKeyGestureEventHandler()) {
@@ -3887,7 +3952,13 @@
@Override
public boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
@Nullable IBinder focusedToken) {
- return PhoneWindowManager.this.handleKeyGestureEvent(event, focusedToken);
+ boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
+ focusedToken);
+ if (handled && Arrays.stream(event.getKeycodes()).anyMatch(
+ (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
+ mPowerKeyHandled = true;
+ }
+ return handled;
}
@Override
@@ -3917,7 +3988,20 @@
case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS:
case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH:
case KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH:
+ case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
+ case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
+ case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
+ case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
+ case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
+ return mDefaultDisplayPolicy.isAwake();
+ case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+ return mDefaultDisplayPolicy.isAwake() && mAccessibilityShortcutController
+ .isAccessibilityShortcutAvailable(isKeyguardLocked());
+ case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
+ return mDefaultDisplayPolicy.isAwake() && mAccessibilityShortcutController
+ .isAccessibilityShortcutAvailable(false);
default:
return false;
}
@@ -4075,6 +4159,66 @@
sendSwitchKeyboardLayout(displayId, focusedToken, direction);
}
return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
+ if (start) {
+ // Screenshot chord is pressed: Wait for long press delay before taking
+ // screenshot
+ interceptScreenshotChord(SCREENSHOT_KEY_CHORD,
+ getScreenshotChordLongPressDelay());
+ } else {
+ cancelPendingScreenshotChordAction();
+ }
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+ if (start) {
+ interceptAccessibilityShortcutChord();
+ } else {
+ cancelPendingAccessibilityShortcutAction();
+ }
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
+ if (start) {
+ interceptRingerToggleChord();
+ } else {
+ cancelPendingRingerToggleChordAction();
+ }
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
+ if (start) {
+ performHapticFeedback(
+ HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON,
+ "KEY_GESTURE_TYPE_GLOBAL_ACTIONS - Global Actions");
+ showGlobalActions();
+ } else {
+ cancelGlobalActionsAction();
+ }
+ return true;
+ // TODO (b/358569822): Consolidate TV and non-TV gestures into same KeyGestureEvent
+ case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
+ if (start) {
+ interceptAccessibilityGestureTv();
+ } else {
+ cancelAccessibilityGestureTv();
+ }
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
+ if (start) {
+ interceptBugreportGestureTv();
+ } else {
+ cancelBugreportGestureTv();
+ }
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
+ if (complete && mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
+ isKeyguardLocked())) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT));
+ }
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
+ if (complete) {
+ mContext.closeSystemDialogs();
+ }
+ return true;
}
return false;
}
@@ -4246,7 +4390,8 @@
mHandler.removeMessages(MSG_ACCESSIBILITY_TV);
}
- private void requestBugreportForTv() {
+ @VisibleForTesting
+ void requestBugreportForTv() {
try {
if (!ActivityManager.getService().launchBugReportHandlerApp()) {
ActivityManager.getService().requestInteractiveBugReport();
@@ -4259,7 +4404,7 @@
// TODO(b/117479243): handle it in InputPolicy
/** {@inheritDoc} */
@Override
- public KeyEvent dispatchUnhandledKey(IBinder focusedToken, KeyEvent event, int policyFlags) {
+ public boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
// Note: This method is only called if the initial down was unhandled.
if (DEBUG_INPUT) {
final KeyInterceptionInfo info =
@@ -4272,75 +4417,26 @@
+ ", keyCode=" + event.getKeyCode()
+ ", scanCode=" + event.getScanCode()
+ ", metaState=" + event.getMetaState()
- + ", repeatCount=" + event.getRepeatCount()
- + ", policyFlags=" + policyFlags);
+ + ", repeatCount=" + event.getRepeatCount());
}
- if (interceptUnhandledKey(event, focusedToken)) {
- return null;
- }
-
- KeyEvent fallbackEvent = null;
- if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
- final KeyCharacterMap kcm = event.getKeyCharacterMap();
- final int keyCode = event.getKeyCode();
- final int metaState = event.getMetaState();
- final boolean initialDown = event.getAction() == KeyEvent.ACTION_DOWN
- && event.getRepeatCount() == 0;
-
- // Check for fallback actions specified by the key character map.
- final FallbackAction fallbackAction;
- if (initialDown) {
- fallbackAction = kcm.getFallbackAction(keyCode, metaState);
- } else {
- fallbackAction = mFallbackActions.get(keyCode);
- }
-
- if (fallbackAction != null) {
- if (DEBUG_INPUT) {
- Slog.d(TAG, "Fallback: keyCode=" + fallbackAction.keyCode
- + " metaState=" + Integer.toHexString(fallbackAction.metaState));
- }
-
- final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK;
- fallbackEvent = KeyEvent.obtain(
- event.getDownTime(), event.getEventTime(),
- event.getAction(), fallbackAction.keyCode,
- event.getRepeatCount(), fallbackAction.metaState,
- event.getDeviceId(), event.getScanCode(),
- flags, event.getSource(), event.getDisplayId(), null);
-
- if (!interceptFallback(focusedToken, fallbackEvent, policyFlags)) {
- fallbackEvent.recycle();
- fallbackEvent = null;
- }
-
- if (initialDown) {
- mFallbackActions.put(keyCode, fallbackAction);
- } else if (event.getAction() == KeyEvent.ACTION_UP) {
- mFallbackActions.remove(keyCode);
- fallbackAction.recycle();
- }
- }
- }
-
- if (DEBUG_INPUT) {
- if (fallbackEvent == null) {
- Slog.d(TAG, "No fallback.");
- } else {
- Slog.d(TAG, "Performing fallback: " + fallbackEvent);
- }
- }
- return fallbackEvent;
- }
-
- private boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
final int keyCode = event.getKeyCode();
final int repeatCount = event.getRepeatCount();
final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
final int metaState = event.getModifiers();
- switch(keyCode) {
+ // TODO(b/358569822): Shift to KeyGestureEvent based handling
+ if (keyCode == KeyEvent.KEYCODE_STEM_PRIMARY) {
+ handleUnhandledSystemKey(event);
+ sendSystemKeyToStatusBarAsync(event);
+ return true;
+ }
+
+ if (useKeyGestureEventHandler()) {
+ return false;
+ }
+
+ switch (keyCode) {
case KeyEvent.KEYCODE_SPACE:
if (down && repeatCount == 0) {
// Handle keyboard layout switching. (CTRL + SPACE)
@@ -4377,10 +4473,6 @@
return true;
}
break;
- case KeyEvent.KEYCODE_STEM_PRIMARY:
- handleUnhandledSystemKey(event);
- sendSystemKeyToStatusBarAsync(event);
- return true;
}
return false;
@@ -4419,19 +4511,6 @@
targetWindowToken);
}
- private boolean interceptFallback(IBinder focusedToken, KeyEvent fallbackEvent,
- int policyFlags) {
- int actions = interceptKeyBeforeQueueing(fallbackEvent, policyFlags);
- if ((actions & ACTION_PASS_TO_USER) != 0) {
- long delayMillis = interceptKeyBeforeDispatching(
- focusedToken, fallbackEvent, policyFlags);
- if (delayMillis == 0 && !interceptUnhandledKey(fallbackEvent, focusedToken)) {
- return true;
- }
- }
- return false;
- }
-
@Override
public void setTopFocusedDisplay(int displayId) {
mTopFocusedDisplayId = displayId;
@@ -4888,6 +4967,7 @@
final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
boolean isWakeKey = (policyFlags & WindowManagerPolicy.FLAG_WAKE) != 0
|| event.isWakeKey();
+ boolean isKeyGestureTriggered = (policyFlags & FLAG_KEY_GESTURE_TRIGGERED) != 0;
// There are key events that perform the operation as the current user,
// and these should be ignored for visible background users.
@@ -5014,8 +5094,13 @@
final boolean isDefaultDisplayOn = Display.isOnState(mDefaultDisplay.getState());
final boolean isDefaultDisplayAwake = mDefaultDisplayPolicy.isAwake();
final boolean interactiveAndAwake = interactive && isDefaultDisplayAwake;
- if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
- handleKeyGesture(event, interactiveAndAwake, isDefaultDisplayOn);
+ if (isKeyGestureTriggered) {
+ // If key gesture is triggered outside policy, reset gesture handlers here
+ mSingleKeyGestureDetector.reset();
+ } else {
+ if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
+ handleKeyGesture(event, interactiveAndAwake, isDefaultDisplayOn);
+ }
}
// Enable haptics if down and virtual key without multiple repetitions. If this is a hard
@@ -5178,7 +5263,7 @@
result &= ~ACTION_PASS_TO_USER;
isWakeKey = false; // wake-up will be handled separately
if (down) {
- interceptPowerKeyDown(event, interactiveAndAwake);
+ interceptPowerKeyDown(event, interactiveAndAwake, isKeyGestureTriggered);
} else {
interceptPowerKeyUp(event, canceled);
}
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 892af6b..ad11657 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -754,11 +754,9 @@
* @param focusedToken Client window token that currently has focus. This is where the key
* event will normally go.
* @param event The key event.
- * @param policyFlags The policy flags associated with the key.
- * @return Returns an alternate key event to redispatch as a fallback, or null to give up.
- * The caller is responsible for recycling the key event.
+ * @return true if the unhandled key is intercepted by the policy.
*/
- KeyEvent dispatchUnhandledKey(IBinder focusedToken, KeyEvent event, int policyFlags);
+ boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken);
/**
* Called when the top focused display is changed.
diff --git a/services/core/java/com/android/server/power/stats/AccumulatedBatteryUsageStatsSection.java b/services/core/java/com/android/server/power/stats/AccumulatedBatteryUsageStatsSection.java
new file mode 100644
index 0000000..dd6d5db
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/AccumulatedBatteryUsageStatsSection.java
@@ -0,0 +1,67 @@
+/*
+ * 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.power.stats;
+
+import android.os.BatteryUsageStats;
+import android.util.IndentingPrintWriter;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+class AccumulatedBatteryUsageStatsSection extends PowerStatsSpan.Section {
+ public static final String TYPE = "accumulated-battery-usage-stats";
+ public static final long ID = Long.MAX_VALUE;
+
+ private final BatteryUsageStats.Builder mBatteryUsageStats;
+
+ AccumulatedBatteryUsageStatsSection(BatteryUsageStats.Builder batteryUsageStats) {
+ super(TYPE);
+ mBatteryUsageStats = batteryUsageStats;
+ }
+
+ public BatteryUsageStats.Builder getBatteryUsageStatsBuilder() {
+ return mBatteryUsageStats;
+ }
+
+ @Override
+ public void write(TypedXmlSerializer serializer) throws IOException {
+ mBatteryUsageStats.build().writeXml(serializer);
+ }
+
+ @Override
+ public void dump(IndentingPrintWriter ipw) {
+ mBatteryUsageStats.build().dump(ipw, "");
+ }
+
+ static class Reader implements PowerStatsSpan.SectionReader {
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+
+ @Override
+ public PowerStatsSpan.Section read(String sectionType, TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ return new AccumulatedBatteryUsageStatsSection(
+ BatteryUsageStats.createBuilderFromXml(parser));
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index cb8e1a0..3f1d9a3 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -509,6 +509,7 @@
}
private boolean mSaveBatteryUsageStatsOnReset;
+ private boolean mAccumulateBatteryUsageStats;
private BatteryUsageStatsProvider mBatteryUsageStatsProvider;
private PowerStatsStore mPowerStatsStore;
@@ -11975,10 +11976,12 @@
*/
public void saveBatteryUsageStatsOnReset(
@NonNull BatteryUsageStatsProvider batteryUsageStatsProvider,
- @NonNull PowerStatsStore powerStatsStore) {
+ @NonNull PowerStatsStore powerStatsStore,
+ boolean accumulateBatteryUsageStats) {
mSaveBatteryUsageStatsOnReset = true;
mBatteryUsageStatsProvider = batteryUsageStatsProvider;
mPowerStatsStore = powerStatsStore;
+ mAccumulateBatteryUsageStats = accumulateBatteryUsageStats;
}
@GuardedBy("this")
@@ -12179,29 +12182,33 @@
return;
}
- final BatteryUsageStats batteryUsageStats;
- synchronized (this) {
- batteryUsageStats = mBatteryUsageStatsProvider.getBatteryUsageStats(this,
- new BatteryUsageStatsQuery.Builder()
- .setMaxStatsAgeMs(0)
- .includePowerModels()
- .includeProcessStateData()
- .build());
- }
-
- // TODO(b/188068523): BatteryUsageStats should use monotonic time for start and end
- // Once that change is made, we will be able to use the BatteryUsageStats' monotonic
- // start time
- long monotonicStartTime =
- mMonotonicClock.monotonicTime() - batteryUsageStats.getStatsDuration();
- mHandler.post(() -> {
- mPowerStatsStore.storeBatteryUsageStats(monotonicStartTime, batteryUsageStats);
- try {
- batteryUsageStats.close();
- } catch (IOException e) {
- Log.e(TAG, "Cannot close BatteryUsageStats", e);
+ if (mAccumulateBatteryUsageStats) {
+ mBatteryUsageStatsProvider.accumulateBatteryUsageStats(this);
+ } else {
+ final BatteryUsageStats batteryUsageStats;
+ synchronized (this) {
+ batteryUsageStats = mBatteryUsageStatsProvider.getBatteryUsageStats(this,
+ new BatteryUsageStatsQuery.Builder()
+ .setMaxStatsAgeMs(0)
+ .includePowerModels()
+ .includeProcessStateData()
+ .build());
}
- });
+
+ // TODO(b/188068523): BatteryUsageStats should use monotonic time for start and end
+ // Once that change is made, we will be able to use the BatteryUsageStats' monotonic
+ // start time
+ long monotonicStartTime =
+ mMonotonicClock.monotonicTime() - batteryUsageStats.getStatsDuration();
+ mHandler.post(() -> {
+ mPowerStatsStore.storeBatteryUsageStats(monotonicStartTime, batteryUsageStats);
+ try {
+ batteryUsageStats.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Cannot close BatteryUsageStats", e);
+ }
+ });
+ }
}
@GuardedBy("this")
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index 87a3e5e..d66e05b 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -33,6 +33,7 @@
import com.android.internal.os.CpuScalingPolicies;
import com.android.internal.os.PowerProfile;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -64,6 +65,7 @@
mClock = clock;
mPowerStatsStore.addSectionReader(new BatteryUsageStatsSection.Reader());
+ mPowerStatsStore.addSectionReader(new AccumulatedBatteryUsageStatsSection.Reader());
}
private List<PowerCalculator> getPowerCalculators() {
@@ -151,6 +153,56 @@
}
/**
+ * Compute BatteryUsageStats for the period since the last accumulated stats were stored,
+ * add them to the accumulated stats and save the result.
+ */
+ public void accumulateBatteryUsageStats(BatteryStatsImpl stats) {
+ BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null;
+
+ PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
+ AccumulatedBatteryUsageStatsSection.ID,
+ AccumulatedBatteryUsageStatsSection.TYPE);
+ if (powerStatsSpan != null) {
+ List<PowerStatsSpan.Section> sections = powerStatsSpan.getSections();
+ for (int i = sections.size() - 1; i >= 0; i--) {
+ PowerStatsSpan.Section section = sections.get(i);
+ if (AccumulatedBatteryUsageStatsSection.TYPE.equals(section.getType())) {
+ accumulatedBatteryUsageStatsBuilder =
+ ((AccumulatedBatteryUsageStatsSection) section)
+ .getBatteryUsageStatsBuilder();
+ break;
+ }
+ }
+ }
+
+ // TODO(b/366493365): add the current batteryusagestats directly into the "accumulated"
+ // builder to avoid allocating a second CursorWindow
+ BatteryUsageStats.Builder currentBatteryUsageStatsBuilder =
+ getCurrentBatteryUsageStatsBuilder(stats,
+ new BatteryUsageStatsQuery.Builder()
+ .setMaxStatsAgeMs(0)
+ .includeProcessStateData()
+ .includePowerStateData()
+ .includeScreenStateData()
+ .build(),
+ mClock.currentTimeMillis());
+
+ if (accumulatedBatteryUsageStatsBuilder == null) {
+ accumulatedBatteryUsageStatsBuilder = currentBatteryUsageStatsBuilder;
+ } else {
+ accumulatedBatteryUsageStatsBuilder.add(currentBatteryUsageStatsBuilder.build());
+ currentBatteryUsageStatsBuilder.discard();
+ }
+
+ powerStatsSpan = new PowerStatsSpan(AccumulatedBatteryUsageStatsSection.ID);
+ powerStatsSpan.addSection(
+ new AccumulatedBatteryUsageStatsSection(accumulatedBatteryUsageStatsBuilder));
+
+ mPowerStatsStore.storePowerStatsSpanAsync(powerStatsSpan,
+ accumulatedBatteryUsageStatsBuilder::discard);
+ }
+
+ /**
* Returns true if the last update was too long ago for the tolerances specified
* by the supplied queries.
*/
@@ -192,15 +244,67 @@
private BatteryUsageStats getBatteryUsageStats(BatteryStatsImpl stats,
BatteryUsageStatsQuery query, long currentTimeMs) {
- if (query.getToTimestamp() == 0) {
+ if ((query.getFlags()
+ & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_ACCUMULATED) != 0) {
+ return getAccumulatedBatteryUsageStats(stats, query);
+ } else if (query.getToTimestamp() == 0) {
return getCurrentBatteryUsageStats(stats, query, currentTimeMs);
} else {
return getAggregatedBatteryUsageStats(stats, query);
}
}
+ private BatteryUsageStats getAccumulatedBatteryUsageStats(BatteryStatsImpl stats,
+ BatteryUsageStatsQuery query) {
+ PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
+ AccumulatedBatteryUsageStatsSection.ID,
+ AccumulatedBatteryUsageStatsSection.TYPE);
+
+ BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null;
+ if (powerStatsSpan != null) {
+ List<PowerStatsSpan.Section> sections = powerStatsSpan.getSections();
+ if (sections.size() == 1) {
+ accumulatedBatteryUsageStatsBuilder =
+ ((AccumulatedBatteryUsageStatsSection) sections.get(0))
+ .getBatteryUsageStatsBuilder();
+ } else {
+ Slog.wtf(TAG, "Unexpected number of sections for type "
+ + AccumulatedBatteryUsageStatsSection.TYPE);
+ }
+ }
+
+ BatteryUsageStats currentBatteryUsageStats = getCurrentBatteryUsageStats(stats, query,
+ mClock.currentTimeMillis());
+
+ BatteryUsageStats result;
+ if (accumulatedBatteryUsageStatsBuilder == null) {
+ result = currentBatteryUsageStats;
+ } else {
+ accumulatedBatteryUsageStatsBuilder.add(currentBatteryUsageStats);
+ try {
+ currentBatteryUsageStats.close();
+ } catch (IOException ex) {
+ Slog.e(TAG, "Closing BatteryUsageStats", ex);
+ }
+ result = accumulatedBatteryUsageStatsBuilder.build();
+ }
+
+ return result;
+ }
+
private BatteryUsageStats getCurrentBatteryUsageStats(BatteryStatsImpl stats,
BatteryUsageStatsQuery query, long currentTimeMs) {
+ BatteryUsageStats.Builder builder = getCurrentBatteryUsageStatsBuilder(stats, query,
+ currentTimeMs);
+ BatteryUsageStats batteryUsageStats = builder.build();
+ if (batteryUsageStats.isProcessStateDataIncluded()) {
+ verify(batteryUsageStats);
+ }
+ return batteryUsageStats;
+ }
+
+ private BatteryUsageStats.Builder getCurrentBatteryUsageStatsBuilder(BatteryStatsImpl stats,
+ BatteryUsageStatsQuery query, long currentTimeMs) {
final long realtimeUs = mClock.elapsedRealtime() * 1000;
final long uptimeUs = mClock.uptimeMillis() * 1000;
@@ -274,11 +378,7 @@
mPowerAttributor.estimatePowerConsumption(batteryUsageStatsBuilder, stats.getHistory(),
monotonicStartTime, monotonicEndTime);
- BatteryUsageStats batteryUsageStats = batteryUsageStatsBuilder.build();
- if (includeProcessStateData) {
- verify(batteryUsageStats);
- }
- return batteryUsageStats;
+ return batteryUsageStatsBuilder;
}
// STOPSHIP(b/229906525): remove verification before shipping
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsStore.java b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
index a875c30..5a6f973 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsStore.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
@@ -133,6 +133,19 @@
}
/**
+ * Schedules saving the specified span on the background thread.
+ */
+ public void storePowerStatsSpanAsync(PowerStatsSpan span, Runnable onComplete) {
+ mHandler.post(() -> {
+ try {
+ storePowerStatsSpan(span);
+ } finally {
+ onComplete.run();
+ }
+ });
+ }
+
+ /**
* Saves the specified span in the store.
*/
public void storePowerStatsSpan(PowerStatsSpan span) {
@@ -172,6 +185,9 @@
lockStoreDirectory();
try {
File file = makePowerStatsSpanFilename(id);
+ if (!file.exists()) {
+ return null;
+ }
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
return PowerStatsSpan.read(inputStream, parser, mSectionReaders, sectionTypes);
} catch (IOException | XmlPullParserException e) {
diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig
index 05d29f5..ce6f57f 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -76,3 +76,13 @@
bug: "364350206"
is_fixed_read_only: true
}
+
+flag {
+ name: "accumulate_battery_usage_stats"
+ namespace: "backstage_power"
+ description: "Add support for monotonically accumulated BatteryUsageStats"
+ bug: "345022340"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 090e679..5b22c10 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -16,6 +16,8 @@
package com.android.server.vibrator;
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.PowerManager;
@@ -117,7 +119,7 @@
* before the release callback.
*/
boolean runVibrationOnVibrationThread(VibrationStepConductor conductor) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "runVibrationOnVibrationThread");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "runVibrationOnVibrationThread");
try {
synchronized (mLock) {
if (mRequestedActiveConductor != null) {
@@ -129,7 +131,7 @@
}
return true;
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -249,17 +251,17 @@
private void clientVibrationCompleteIfNotAlready(@NonNull Vibration.EndInfo vibrationEndInfo) {
if (!mCalledVibrationCompleteCallback) {
mCalledVibrationCompleteCallback = true;
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "notifyVibrationComplete");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "notifyVibrationComplete");
try {
mExecutingConductor.notifyVibrationComplete(vibrationEndInfo);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
}
private void playVibration() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "playVibration");
try {
mExecutingConductor.prepareToStart();
while (!mExecutingConductor.isFinished()) {
@@ -283,7 +285,7 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
}
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index 3c47850..c120fc7 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -16,6 +16,8 @@
package com.android.server.vibrator;
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
import android.annotation.Nullable;
import android.hardware.vibrator.IVibrator;
import android.os.Binder;
@@ -124,7 +126,7 @@
/** Reruns the query to the vibrator to load the {@link VibratorInfo}, if not yet successful. */
public void reloadVibratorInfoIfNeeded() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#reloadVibratorInfoIfNeeded");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#reloadVibratorInfoIfNeeded");
try {
// Early check outside lock, for quick return.
if (mVibratorInfoLoadSuccessful) {
@@ -143,7 +145,7 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -199,13 +201,13 @@
/** Return {@code true} if the underlying vibrator is currently available, false otherwise. */
public boolean isAvailable() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#isAvailable");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#isAvailable");
try {
synchronized (mLock) {
return mNativeWrapper.isAvailable();
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -215,7 +217,9 @@
* <p>This will affect the state of {@link #isUnderExternalControl()}.
*/
public void setExternalControl(boolean externalControl) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "setExternalControl(" + externalControl + ")");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR,
+ externalControl ? "VibratorController#enableExternalControl"
+ : "VibratorController#disableExternalControl");
try {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) {
return;
@@ -225,7 +229,7 @@
mNativeWrapper.setExternalControl(externalControl);
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -234,7 +238,7 @@
* if given {@code effect} is {@code null}.
*/
public void updateAlwaysOn(int id, @Nullable PrebakedSegment prebaked) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#updateAlwaysOn");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#updateAlwaysOn");
try {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) {
return;
@@ -248,13 +252,13 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
/** Set the vibration amplitude. This will NOT affect the state of {@link #isVibrating()}. */
public void setAmplitude(float amplitude) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#setAmplitude");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#setAmplitude");
try {
synchronized (mLock) {
if (mVibratorInfo.hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) {
@@ -265,7 +269,7 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -279,7 +283,7 @@
* do not support the input or a negative number if the operation failed.
*/
public long on(long milliseconds, long vibrationId) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on");
try {
synchronized (mLock) {
long duration = mNativeWrapper.on(milliseconds, vibrationId);
@@ -290,7 +294,7 @@
return duration;
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -304,7 +308,7 @@
* do not support the input or a negative number if the operation failed.
*/
public long on(VibrationEffect.VendorEffect vendorEffect, long vibrationId) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (vendor)");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (vendor)");
synchronized (mLock) {
Parcel vendorData = Parcel.obtain();
try {
@@ -320,7 +324,7 @@
return duration;
} finally {
vendorData.recycle();
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
}
@@ -335,7 +339,7 @@
* do not support the input or a negative number if the operation failed.
*/
public long on(PrebakedSegment prebaked, long vibrationId) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (Prebaked)");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (Prebaked)");
try {
synchronized (mLock) {
long duration = mNativeWrapper.perform(prebaked.getEffectId(),
@@ -347,7 +351,7 @@
return duration;
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -361,7 +365,7 @@
* do not support the input or a negative number if the operation failed.
*/
public long on(PrimitiveSegment[] primitives, long vibrationId) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (Primitive)");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (Primitive)");
try {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) {
return 0;
@@ -375,7 +379,7 @@
return duration;
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -388,7 +392,7 @@
* @return The duration of the effect playing, or 0 if unsupported.
*/
public long on(RampSegment[] primitives, long vibrationId) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE)");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE)");
try {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) {
return 0;
@@ -403,7 +407,7 @@
return duration;
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -413,7 +417,7 @@
* <p>This will affect the state of {@link #isVibrating()}.
*/
public void off() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#off");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#off");
try {
synchronized (mLock) {
mNativeWrapper.off();
@@ -421,7 +425,7 @@
notifyListenerOnVibrating(false);
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index bd17e5d..95c6483 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
import static android.os.VibrationAttributes.USAGE_CLASS_ALARM;
import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
@@ -333,7 +334,7 @@
@VisibleForTesting
void systemReady() {
Slog.v(TAG, "Initializing VibratorManager service...");
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "systemReady");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "systemReady");
try {
// Will retry to load each vibrator's info, if any request have failed.
for (int i = 0; i < mVibrators.size(); i++) {
@@ -352,7 +353,7 @@
mServiceReady = true;
}
Slog.v(TAG, "VibratorManager service initialized");
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -413,7 +414,7 @@
@Override // Binder call
public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId,
@Nullable CombinedVibration effect, @Nullable VibrationAttributes attrs) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "setAlwaysOnEffect");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "setAlwaysOnEffect");
try {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.VIBRATE_ALWAYS_ON,
@@ -449,20 +450,25 @@
}
return true;
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@Override // Binder call
public void vibrate(int uid, int deviceId, String opPkg, @NonNull CombinedVibration effect,
@Nullable VibrationAttributes attrs, String reason, IBinder token) {
- vibrateWithPermissionCheck(uid, deviceId, opPkg, effect, attrs, reason, token);
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "vibrate");
+ try {
+ vibrateWithPermissionCheck(uid, deviceId, opPkg, effect, attrs, reason, token);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
+ }
}
@Override // Binder call
public void performHapticFeedback(int uid, int deviceId, String opPkg, int constant,
String reason, int flags, int privFlags) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedback");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedback");
// Note that the `performHapticFeedback` method does not take a token argument from the
// caller, and instead, uses this service as the token. This is to mitigate performance
// impact that would otherwise be caused due to marshal latency. Haptic feedback effects are
@@ -471,7 +477,7 @@
performHapticFeedbackInternal(uid, deviceId, opPkg, constant, reason, /* token= */
this, flags, privFlags);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -479,13 +485,13 @@
public void performHapticFeedbackForInputDevice(int uid, int deviceId, String opPkg,
int constant, int inputDeviceId, int inputSource, String reason, int flags,
int privFlags) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
try {
performHapticFeedbackForInputDeviceInternal(uid, deviceId, opPkg, constant,
inputDeviceId,
inputSource, reason, /* token= */ this, flags, privFlags);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -563,26 +569,16 @@
HalVibration vibrateWithPermissionCheck(int uid, int deviceId, String opPkg,
@NonNull CombinedVibration effect, @Nullable VibrationAttributes attrs,
String reason, IBinder token) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason = " + reason);
- try {
- attrs = fixupVibrationAttributes(attrs, effect);
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.VIBRATE, "vibrate");
- return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
+ attrs = fixupVibrationAttributes(attrs, effect);
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.VIBRATE, "vibrate");
+ return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
}
HalVibration vibrateWithoutPermissionCheck(int uid, int deviceId, String opPkg,
@NonNull CombinedVibration effect, @NonNull VibrationAttributes attrs,
String reason, IBinder token) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate no perm check, reason = " + reason);
- try {
- return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
+ return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
}
private HalVibration vibrateInternal(int uid, int deviceId, String opPkg,
@@ -671,7 +667,7 @@
@Override // Binder call
public void cancelVibrate(int usageFilter, IBinder token) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelVibrate");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "cancelVibrate");
try {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.VIBRATE,
@@ -708,7 +704,7 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -899,7 +895,7 @@
@GuardedBy("mLock")
@Nullable
private Vibration.EndInfo startVibrationLocked(HalVibration vib) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationLocked");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "startVibrationLocked");
try {
if (mInputDeviceDelegate.isAvailable()) {
return startVibrationOnInputDevicesLocked(vib);
@@ -919,7 +915,7 @@
mNextVibration = conductor;
return null;
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -930,7 +926,7 @@
int mode = startAppOpModeLocked(vib.callerInfo);
switch (mode) {
case AppOpsManager.MODE_ALLOWED:
- Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
+ Trace.asyncTraceBegin(TRACE_TAG_VIBRATOR, "vibration", 0);
// Make sure mCurrentVibration is set while triggering the VibrationThread.
mCurrentVibration = conductor;
if (!mCurrentVibration.linkToDeath()) {
@@ -1581,7 +1577,7 @@
@Override
public boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "prepareSyncedVibration");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "prepareSyncedVibration");
try {
if ((mCapabilities & requiredCapabilities) != requiredCapabilities) {
// This sync step requires capabilities this device doesn't have, skipping
@@ -1590,33 +1586,33 @@
}
return mNativeWrapper.prepareSynced(vibratorIds);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@Override
public boolean triggerSyncedVibration(long vibrationId) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "triggerSyncedVibration");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "triggerSyncedVibration");
try {
return mNativeWrapper.triggerSynced(vibrationId);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@Override
public void cancelSyncedVibration() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelSyncedVibration");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "cancelSyncedVibration");
try {
mNativeWrapper.cancelSynced();
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@Override
public void noteVibratorOn(int uid, long duration) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "noteVibratorOn");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "noteVibratorOn");
try {
if (duration <= 0) {
// Tried to turn vibrator ON and got:
@@ -1635,20 +1631,20 @@
} catch (RemoteException e) {
Slog.e(TAG, "Error logging VibratorStateChanged to ON", e);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@Override
public void noteVibratorOff(int uid) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "noteVibratorOff");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "noteVibratorOff");
try {
mBatteryStatsService.noteVibratorOff(uid);
mFrameworkStatsLogger.writeVibratorStateOffAsync(uid);
} catch (RemoteException e) {
Slog.e(TAG, "Error logging VibratorStateChanged to OFF", e);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -1657,7 +1653,8 @@
if (DEBUG) {
Slog.d(TAG, "VibrationThread released after finished vibration");
}
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onVibrationThreadReleased: " + vibrationId);
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVibrationThreadReleased");
+
try {
synchronized (mLock) {
if (DEBUG) {
@@ -1686,7 +1683,7 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
}
@@ -1994,7 +1991,7 @@
@Override
public ExternalVibrationScale onExternalVibrationStart(ExternalVibration vib) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onExternalVibrationStart");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStart");
try {
// Create Vibration.Stats as close to the received request as possible, for
// tracking.
@@ -2116,13 +2113,13 @@
return externalVibration.getScale();
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@Override
public void onExternalVibrationStop(ExternalVibration vib) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onExternalVibrationStop");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStop");
try {
synchronized (mLock) {
if (mCurrentExternalVibration != null
@@ -2135,7 +2132,7 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -2203,32 +2200,39 @@
@Override
public int onCommand(String cmd) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onCommand " + cmd);
try {
if ("list".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: list");
return runListVibrators();
}
if ("synced".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: synced");
return runMono();
}
if ("combined".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: combined");
return runStereo();
}
if ("sequential".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: sequential");
return runSequential();
}
if ("xml".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: xml");
return runXml();
}
if ("cancel".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: cancel");
return runCancel();
}
if ("feedback".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: feedback");
return runHapticFeedback();
}
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: default");
return handleDefaultCommands(cmd);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 232c3b6..dcf0319 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -188,9 +188,8 @@
* the application did not handle.
*/
@Override
- public KeyEvent dispatchUnhandledKey(
- IBinder focusedToken, KeyEvent event, int policyFlags) {
- return mService.mPolicy.dispatchUnhandledKey(focusedToken, event, policyFlags);
+ public boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
+ return mService.mPolicy.interceptUnhandledKey(event, focusedToken);
}
/** Callback to get pointer layer. */
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 9da848a..bf623b2 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -24,6 +24,8 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -2040,10 +2042,15 @@
final boolean isOtherUndefinedMode = otherWindowingMode == WINDOWING_MODE_UNDEFINED;
// An activity type and windowing mode is compatible if they are the exact same type/mode,
- // or if one of the type/modes is undefined
+ // or if one of the type/modes is undefined. This is with the exception of
+ // freeform/fullscreen where both modes are assumed to be compatible with each other.
final boolean isCompatibleType = activityType == otherActivityType
|| isUndefinedType || isOtherUndefinedType;
final boolean isCompatibleMode = windowingMode == otherWindowingMode
+ || (windowingMode == WINDOWING_MODE_FREEFORM
+ && otherWindowingMode == WINDOWING_MODE_FULLSCREEN)
+ || (windowingMode == WINDOWING_MODE_FULLSCREEN
+ && otherWindowingMode == WINDOWING_MODE_FREEFORM)
|| isUndefinedMode || isOtherUndefinedMode;
return isCompatibleType && isCompatibleMode;
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 0eafb59..a07facf 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -512,8 +512,6 @@
<xs:annotation name="final"/>
</xs:element>
</xs:sequence>
- <!-- valid value of interpolation if specified: linear -->
- <xs:attribute name="interpolation" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="brightnessPoint">
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 355b0ab..5309263 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -91,8 +91,6 @@
public class ComprehensiveBrightnessMap {
ctor public ComprehensiveBrightnessMap();
method @NonNull public final java.util.List<com.android.server.display.config.BrightnessPoint> getBrightnessPoint();
- method public String getInterpolation();
- method public void setInterpolation(String);
}
public class Density {
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java b/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
new file mode 100644
index 0000000..fead05b
--- /dev/null
+++ b/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.supervision;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.Bundle;
+
+/**
+ * Local system service interface for {@link SupervisionService}.
+ *
+ * @hide Only for use within Android OS.
+ */
+public abstract class SupervisionManagerInternal {
+ /**
+ * Returns whether supervision is enabled for the specified user
+ *
+ * @param userId The user to retrieve the supervision state for
+ * @return whether the user is supervised
+ */
+ public abstract boolean isSupervisionEnabledForUser(@UserIdInt int userId);
+
+ /**
+ * Sets whether the supervision lock screen should be shown for the specified user
+ *
+ * @param userId The user set the superivision state for
+ * @param enabled Whether or not the superivision lock screen needs to be shown
+ * @param options Optional configuration parameters for the supervision lock screen
+ */
+ public abstract void setSupervisionLockscreenEnabledForUser(
+ @UserIdInt int userId, boolean enabled, @Nullable Bundle options);
+}
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java
index 7ffd0ec..4c515c1 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionService.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java
@@ -18,14 +18,22 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.supervision.ISupervisionManager;
import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.Bundle;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
+import android.util.SparseArray;
+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.pm.UserManagerInternal;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -38,13 +46,25 @@
private final Context mContext;
+ // TODO(b/362756788): Does this need to be a LockGuard lock?
+ private final Object mLockDoNoUseDirectly = new Object();
+
+ @GuardedBy("getLockObject()")
+ private final SparseArray<SupervisionUserData> mUserData = new SparseArray<>();
+
+ private final UserManagerInternal mUserManagerInternal;
+
public SupervisionService(Context context) {
- mContext = context.createAttributionContext("SupervisionService");
+ mContext = context.createAttributionContext(LOG_TAG);
+ mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+ mUserManagerInternal.addUserLifecycleListener(new UserLifecycleListener());
}
@Override
- public boolean isSupervisionEnabled() {
- return false;
+ public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
+ synchronized (getLockObject()) {
+ return getUserDataLocked(userId).supervisionEnabled;
+ }
}
@Override
@@ -60,11 +80,44 @@
}
@Override
- protected void dump(
- @NonNull FileDescriptor fd, @NonNull PrintWriter fout, @Nullable String[] args) {
- if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, fout)) return;
+ protected void dump(@NonNull FileDescriptor fd,
+ @NonNull PrintWriter printWriter, @Nullable String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, printWriter)) return;
- fout.println("Supervision enabled: " + isSupervisionEnabled());
+ try (var pw = new IndentingPrintWriter(printWriter, " ")) {
+ pw.println("SupervisionService state:");
+ pw.increaseIndent();
+
+ var users = mUserManagerInternal.getUsers(false);
+ synchronized (getLockObject()) {
+ for (var user : users) {
+ getUserDataLocked(user.id).dump(pw);
+ pw.println();
+ }
+ }
+ }
+ }
+
+ private Object getLockObject() {
+ return mLockDoNoUseDirectly;
+ }
+
+ @NonNull
+ @GuardedBy("getLockObject()")
+ SupervisionUserData getUserDataLocked(@UserIdInt int userId) {
+ SupervisionUserData data = mUserData.get(userId);
+ if (data == null) {
+ // TODO(b/362790738): Do not create user data for nonexistent users.
+ data = new SupervisionUserData(userId);
+ mUserData.append(userId, data);
+ }
+ return data;
+ }
+
+ void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) {
+ synchronized (getLockObject()) {
+ getUserDataLocked(userId).supervisionEnabled = enabled;
+ }
}
public static class Lifecycle extends SystemService {
@@ -77,7 +130,35 @@
@Override
public void onStart() {
+ publishLocalService(SupervisionManagerInternal.class, mSupervisionService.mInternal);
publishBinderService(Context.SUPERVISION_SERVICE, mSupervisionService);
}
}
+
+ final SupervisionManagerInternal mInternal = new SupervisionManagerInternal() {
+ public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
+ synchronized (getLockObject()) {
+ return getUserDataLocked(userId).supervisionEnabled;
+ }
+ }
+
+ @Override
+ public void setSupervisionLockscreenEnabledForUser(
+ @UserIdInt int userId, boolean enabled, @Nullable Bundle options) {
+ synchronized (getLockObject()) {
+ SupervisionUserData data = getUserDataLocked(userId);
+ data.supervisionLockScreenEnabled = enabled;
+ data.supervisionLockScreenOptions = options;
+ }
+ }
+ };
+
+ private final class UserLifecycleListener implements UserManagerInternal.UserLifecycleListener {
+ @Override
+ public void onUserRemoved(UserInfo user) {
+ synchronized (getLockObject()) {
+ mUserData.remove(user.id);
+ }
+ }
+ }
}
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
index 3aba24a..2adaae3 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
@@ -17,8 +17,7 @@
package com.android.server.supervision;
import android.os.ShellCommand;
-
-import java.io.PrintWriter;
+import android.os.UserHandle;
public class SupervisionServiceShellCommand extends ShellCommand {
private final SupervisionService mService;
@@ -32,30 +31,29 @@
if (cmd == null) {
return handleDefaultCommands(null);
}
- final PrintWriter pw = getOutPrintWriter();
switch (cmd) {
- case "help": return help(pw);
- case "is-enabled": return isEnabled(pw);
+ case "enable": return setEnabled(true);
+ case "disable": return setEnabled(false);
default: return handleDefaultCommands(cmd);
}
}
- private int help(PrintWriter pw) {
- pw.println("Supervision service commands:");
- pw.println(" help");
- pw.println(" Prints this help text");
- pw.println(" is-enabled");
- pw.println(" Is supervision enabled");
- return 0;
- }
-
- private int isEnabled(PrintWriter pw) {
- pw.println(mService.isSupervisionEnabled());
+ private int setEnabled(boolean enabled) {
+ final var pw = getOutPrintWriter();
+ final var userId = UserHandle.parseUserArg(getNextArgRequired());
+ mService.setSupervisionEnabledForUser(userId, enabled);
return 0;
}
@Override
public void onHelp() {
- help(getOutPrintWriter());
+ final var pw = getOutPrintWriter();
+ pw.println("Supervision service (supervision) commands:");
+ pw.println(" help");
+ pw.println(" Prints this help text");
+ pw.println(" enable <USER_ID>");
+ pw.println(" Enables supervision for the given user.");
+ pw.println(" disable <USER_ID>");
+ pw.println(" Disables supervision for the given user.");
}
}
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
new file mode 100644
index 0000000..5616237
--- /dev/null
+++ b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
@@ -0,0 +1,45 @@
+/*
+ * 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.supervision;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.Bundle;
+import android.util.IndentingPrintWriter;
+
+/** User specific data, used internally by the {@link SupervisionService}. */
+public class SupervisionUserData {
+ public final @UserIdInt int userId;
+ public boolean supervisionEnabled;
+ public boolean supervisionLockScreenEnabled;
+ @Nullable public Bundle supervisionLockScreenOptions;
+
+ public SupervisionUserData(@UserIdInt int userId) {
+ this.userId = userId;
+ }
+
+ void dump(@NonNull IndentingPrintWriter pw) {
+ pw.println();
+ pw.println("User " + userId + ":");
+ pw.increaseIndent();
+ pw.println("supervisionEnabled: " + supervisionEnabled);
+ pw.println("supervisionLockScreenEnabled: " + supervisionLockScreenEnabled);
+ pw.println("supervisionLockScreenOptions: " + supervisionLockScreenOptions);
+ pw.decreaseIndent();
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
index e5d3153..72cbac3 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
@@ -35,6 +35,7 @@
public final class InputMethodManagerServiceTests {
static final int SYSTEM_DECORATION_SUPPORT_DISPLAY_ID = 2;
static final int NO_SYSTEM_DECORATION_SUPPORT_DISPLAY_ID = 3;
+ private static final int TEST_IME_USER_ID = 1;
static InputMethodManagerService.ImeDisplayValidator sChecker =
(displayId) -> {
@@ -102,7 +103,8 @@
null,
null,
null,
- null));
+ null,
+ TEST_IME_USER_ID));
history.dump(new PrintWriter(writer), "" /* prefix */);
diff --git a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
index 5c4716d..7d5532f 100644
--- a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
+++ b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
@@ -57,6 +57,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doReturn
@@ -383,6 +384,10 @@
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)) {
PackageManager.PERMISSION_GRANTED
}
+ whenever(this.checkPermission(
+ eq(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL), anyInt(), anyInt())) {
+ PackageManager.PERMISSION_GRANTED
+ }
}
val mockSharedLibrariesImpl: SharedLibrariesImpl = mock {
whenever(this.snapshot()) { this@mock }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index fd05b26..8e1be9a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -392,7 +392,7 @@
public void testInvalidLuxThrottling() throws Exception {
setupDisplayDeviceConfigFromDisplayConfigFile(
getContent(getInvalidLuxThrottling(), getValidProxSensor(),
- /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
+ /* includeIdleMode= */ true, /* enableEvenDimmer= */ false));
Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData =
mDisplayDeviceConfig.getLuxThrottlingData();
@@ -600,7 +600,7 @@
public void testProximitySensorWithEmptyValuesFromDisplayConfig() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(
getContent(getValidLuxThrottling(), getProxSensorWithEmptyValues(),
- /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
+ /* includeIdleMode= */ true, /* enableEvenDimmer= */ false));
assertNull(mDisplayDeviceConfig.getProximitySensor());
}
@@ -608,7 +608,7 @@
public void testProximitySensorWithRefreshRatesFromDisplayConfig() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(
getContent(getValidLuxThrottling(), getValidProxSensorWithRefreshRateAndVsyncRate(),
- /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
+ /* includeIdleMode= */ true, /* enableEvenDimmer= */ false));
assertEquals("test_proximity_sensor",
mDisplayDeviceConfig.getProximitySensor().type);
assertEquals("Test Proximity Sensor",
@@ -803,7 +803,7 @@
@Test
public void testBrightnessRamps_IdleFallsBackToConfigInteractive() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false));
assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000);
assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000);
@@ -820,14 +820,14 @@
@Test
public void testBrightnessCapForWearBedtimeMode() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false));
assertEquals(0.1f, mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA);
}
@Test
public void testAutoBrightnessBrighteningLevels() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false));
assertArrayEquals(new float[]{0.0f, 80},
mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
@@ -890,7 +890,7 @@
when(mFlags.areAutoBrightnessModesEnabled()).thenReturn(false);
setupDisplayDeviceConfigFromConfigResourceFile();
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false));
assertArrayEquals(new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100),
brightnessIntToFloat(150)},
@@ -929,7 +929,7 @@
when(mFlags.isEvenDimmerEnabled()).thenReturn(true);
when(mResources.getBoolean(R.bool.config_evenDimmerEnabled)).thenReturn(true);
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ true));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ true));
assertTrue(mDisplayDeviceConfig.isEvenDimmerAvailable());
assertEquals(0.01f, mDisplayDeviceConfig.getBacklightFromBrightness(0.002f), ZERO_DELTA);
@@ -1365,7 +1365,7 @@
private String getContent() {
return getContent(getValidLuxThrottling(), getValidProxSensor(),
- /* includeIdleMode= */ true, false);
+ /* includeIdleMode= */ true, /* enableEvenDimmer= */ false);
}
private String getContent(String brightnessCapConfig, String proxSensor,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 120cc84..f5bed99 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -29,8 +29,10 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -45,6 +47,7 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
+import android.util.Spline;
import android.view.Display;
import android.view.DisplayAddress;
import android.view.SurfaceControl;
@@ -59,6 +62,7 @@
import com.android.internal.R;
import com.android.server.LocalServices;
import com.android.server.display.LocalDisplayAdapter.BacklightAdapter;
+import com.android.server.display.color.ColorDisplayService;
import com.android.server.display.feature.DisplayManagerFlags;
import com.android.server.display.mode.DisplayModeDirector;
import com.android.server.display.notifications.DisplayNotificationManager;
@@ -119,6 +123,8 @@
private DisplayManagerFlags mFlags;
@Mock
private DisplayPowerController mMockedDisplayPowerController;
+ @Mock
+ private ColorDisplayService.ColorDisplayServiceInternal mMockedColorDisplayServiceInternal;
private Handler mHandler;
@@ -133,6 +139,11 @@
private Injector mInjector;
@Mock
+ private DisplayDeviceConfig mMockDisplayDeviceConfig;
+ @Mock
+ private BacklightAdapter mMockBacklightAdapter;
+
+ @Mock
private LocalDisplayAdapter.SurfaceControlProxy mSurfaceControlProxy;
private static final float[] DISPLAY_RANGE_NITS = { 2.685f, 478.5f };
private static final int[] BACKLIGHT_RANGE = { 1, 255 };
@@ -150,6 +161,9 @@
doReturn(mMockedResources).when(mMockedContext).getResources();
LocalServices.removeServiceForTest(LightsManager.class);
LocalServices.addService(LightsManager.class, mMockedLightsManager);
+ LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
+ LocalServices.addService(ColorDisplayService.ColorDisplayServiceInternal.class,
+ mMockedColorDisplayServiceInternal);
mInjector = new Injector();
when(mSurfaceControlProxy.getBootDisplayModeSupport()).thenReturn(true);
mAdapter = new LocalDisplayAdapter(mMockedSyncRoot, mMockedContext, mHandler,
@@ -211,7 +225,15 @@
when(mMockedResources.getIntArray(
com.android.internal.R.array.config_autoBrightnessLcdBacklightValues))
.thenReturn(new int[]{});
+
+ when(mMockedColorDisplayServiceInternal.fetchEvenDimmerSpline(3)).thenReturn(
+ new Spline.LinearSpline(
+ new float[]{2f, 3.0f, 500f, 2000f},
+ new float[]{100, 0, 0, 0}));
+ when(mMockDisplayDeviceConfig.isEvenDimmerAvailable()).thenReturn(true);
+
doReturn(true).when(mFlags).isDisplayOffloadEnabled();
+ doReturn(true).when(mFlags).isEvenDimmerEnabled();
initDisplayOffloadSession();
}
@@ -222,6 +244,122 @@
}
}
+ @Test
+ public void testEvenDimmer() throws InterruptedException {
+ // Set up
+ FakeDisplay display = new FakeDisplay(PORT_A);
+ setUpDisplay(display);
+ updateAvailableDisplays();
+ mAdapter.registerLocked();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+ DisplayDevice displayDevice = mListener.addedDisplays.get(0);
+
+ // brightness|backlight| nits | strength
+ // 0.5 | 0.45 | 600 | 0 // initial setup value
+ // 0.4 | 0.35 | 500 | 0 // normal range value
+ // 0.31 | 0.2 | 3 | 0 // transition point
+ // 0.16 | 0.125 | 2.5 | 50 // mid point of even dimmer
+ // 0.1 | 0.05 | 2 | 100 // bottom of even dimmer range
+ // 0.05 | 0.01 | 1 | 100+ // beyond strength=100 range (should still return 100)
+ when(mMockDisplayDeviceConfig.getEvenDimmerTransitionPoint()).thenReturn(0.31f);
+ when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.5f)).thenReturn(0.45f);
+ when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.4f)).thenReturn(0.35f);
+ when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.31f)).thenReturn(0.2f);
+ when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.16f)).thenReturn(0.125f);
+ when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.1f)).thenReturn(0.05f);
+ when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.05f)).thenReturn(0.01f);
+ when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.45f)).thenReturn(600f);
+ when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.35f)).thenReturn(500f);
+ when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.2f)).thenReturn(3f);
+ when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.125f)).thenReturn(2.5f);
+ when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.05f)).thenReturn(2f);
+ when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.01f)).thenReturn(1f);
+
+ // initialise brightness to 0.5
+ Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON,
+ 0.5f, 0.5f, null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ verify(mSurfaceControlProxy).setDisplayPowerMode(any(), anyInt());
+ verify(mMockBacklightAdapter).setBacklight(anyFloat(), anyFloat(), anyFloat(), anyFloat());
+ verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(false), eq(0));
+ verify(mMockedColorDisplayServiceInternal).fetchEvenDimmerSpline(eq(3.0f));
+
+ // set up normal brightness range
+ changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.4f, 0.4f,
+ null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ // verify normal brightness range
+ verify(mMockBacklightAdapter).setBacklight(0.35f, 500f, 0.35f, 500f);
+ verify(mMockedColorDisplayServiceInternal,
+ times(1)) // no more, since the strength is the same
+ .applyEvenDimmerColorChanges(eq(false), eq(0));
+ verify(mMockedColorDisplayServiceInternal, times(2)).fetchEvenDimmerSpline(eq(3.0f));
+
+ // set up even dimmer edge range
+ changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.31f,
+ 0.31f, null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ // verify even dimmer edge range
+ verify(mMockBacklightAdapter).setBacklight(0.2f, 3f, 0.2f, 3f);
+ // verify no more times, since the strength and enabled-ness is the same
+ verify(mMockedColorDisplayServiceInternal, times(1)).applyEvenDimmerColorChanges(eq(false),
+ eq(0));
+ verify(mMockedColorDisplayServiceInternal, times(3)).fetchEvenDimmerSpline(eq(3.0f));
+
+ // set up mid point of even dimmer range
+ changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.16f,
+ 0.16f, null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ // verify within even dimmer range
+ verify(mMockBacklightAdapter).setBacklight(0.125f, 2.5f, 0.125f, 2.5f);
+ verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(true), eq(50));
+ verify(mMockedColorDisplayServiceInternal, times(4)).fetchEvenDimmerSpline(eq(3.0f));
+
+ // set up within even dimmer range
+ changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.1f, 0.1f,
+ null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ // verify within even dimmer range
+ verify(mMockBacklightAdapter).setBacklight(0.05f, 2f, 0.05f, 2f);
+ verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(true), eq(100));
+ verify(mMockedColorDisplayServiceInternal, times(5)).fetchEvenDimmerSpline(eq(3.0f));
+
+ // set up below even dimmer range
+ changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.05f,
+ 0.05f, null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ // verify within even dimmer range
+ verify(mMockBacklightAdapter).setBacklight(0.01f, 1f, 0.01f, 1f);
+ // ensure no greater than 100 strength is returned, therefore not called again.
+ verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(true), eq(100));
+ verify(mMockedColorDisplayServiceInternal, times(6)).fetchEvenDimmerSpline(eq(3.0f));
+
+ // set up return to normal range
+ changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.4f, 0.4f,
+ null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ // verify return to normal range
+ verify(mMockBacklightAdapter, times(2)).setBacklight(0.35f, 500f, 0.35f, 500f);
+ verify(mMockedColorDisplayServiceInternal, times(2)).applyEvenDimmerColorChanges(eq(false),
+ anyInt());
+ verify(mMockedColorDisplayServiceInternal, times(7)).fetchEvenDimmerSpline(eq(3.0f));
+ }
+
/**
* Confirm that display is marked as private when it is listed in
* com.android.internal.R.array.config_localPrivateDisplayPorts.
@@ -1461,15 +1599,16 @@
return mSurfaceControlProxy;
}
- // Instead of using DisplayDeviceConfig.create(context, physicalDisplayId, isFirstDisplay)
- // we should use DisplayDeviceConfig.create(context, isFirstDisplay) for the test to ensure
- // that real device DisplayDeviceConfig is not loaded for FakeDisplay and we are getting
- // consistent behaviour. Please also note that context passed to this method, is
- // mMockContext and values will be loaded from mMockResources.
@Override
public DisplayDeviceConfig createDisplayDeviceConfig(Context context,
long physicalDisplayId, boolean isFirstDisplay, DisplayManagerFlags flags) {
- return DisplayDeviceConfig.create(context, isFirstDisplay, flags);
+ return mMockDisplayDeviceConfig;
+ }
+
+ @Override
+ public BacklightAdapter getBacklightAdapter(IBinder displayToken, boolean isFirstDisplay,
+ LocalDisplayAdapter.SurfaceControlProxy surfaceControlProxy) {
+ return mMockBacklightAdapter;
}
}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
index 1d20538..c037f97 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
@@ -927,7 +927,7 @@
assertThat(mPowerStatsStore.getTableOfContents()).isEmpty();
mBatteryStatsImpl.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider,
- mPowerStatsStore);
+ mPowerStatsStore, /* accumulateBatteryUsageStats */ false);
synchronized (mBatteryStatsImpl) {
mBatteryStatsImpl.noteFlashlightOnLocked(42, mMockClock.realtime, mMockClock.uptime);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index fde84e9..0e60156 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -419,7 +419,8 @@
mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
- batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore);
+ batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore,
+ /* accumulateBatteryUsageStats */ false);
synchronized (batteryStats) {
batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
}
@@ -505,6 +506,102 @@
.of(180.0);
}
+ @Test
+ public void accumulateBatteryUsageStats() {
+ BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
+
+ setTime(5 * MINUTE_IN_MS);
+
+ // Capture the session start timestamp
+ synchronized (batteryStats) {
+ batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+ }
+
+ PowerStatsStore powerStatsStore = new PowerStatsStore(
+ new File(mStatsRule.getHistoryDir(), getClass().getSimpleName()),
+ mStatsRule.getHandler());
+ powerStatsStore.reset();
+
+ BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
+ mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
+ mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
+
+ batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore,
+ /* accumulateBatteryUsageStats */ true);
+
+ synchronized (batteryStats) {
+ batteryStats.noteFlashlightOnLocked(APP_UID,
+ 10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
+ }
+ synchronized (batteryStats) {
+ batteryStats.noteFlashlightOffLocked(APP_UID,
+ 20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
+ }
+
+ synchronized (batteryStats) {
+ batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+ }
+
+ synchronized (batteryStats) {
+ batteryStats.noteFlashlightOnLocked(APP_UID,
+ 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS);
+ }
+ synchronized (batteryStats) {
+ batteryStats.noteFlashlightOffLocked(APP_UID,
+ 50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
+ }
+ setTime(55 * MINUTE_IN_MS);
+ synchronized (batteryStats) {
+ batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+ }
+
+ // This section has not been saved yet, but should be added to the accumulated totals
+ synchronized (batteryStats) {
+ batteryStats.noteFlashlightOnLocked(APP_UID,
+ 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
+ }
+ synchronized (batteryStats) {
+ batteryStats.noteFlashlightOffLocked(APP_UID,
+ 110 * MINUTE_IN_MS, 110 * MINUTE_IN_MS);
+ }
+ setTime(115 * MINUTE_IN_MS);
+
+ // Await completion
+ ConditionVariable done = new ConditionVariable();
+ mStatsRule.getHandler().post(done::open);
+ done.block();
+
+ BatteryUsageStats stats = provider.getBatteryUsageStats(batteryStats,
+ new BatteryUsageStatsQuery.Builder().accumulated().build());
+
+ assertThat(stats.getStatsStartTimestamp()).isEqualTo(5 * MINUTE_IN_MS);
+ assertThat(stats.getStatsEndTimestamp()).isEqualTo(115 * MINUTE_IN_MS);
+
+ // Section 1 (saved): 20 - 10 = 10
+ // Section 2 (saved): 50 - 30 = 20
+ // Section 3 (fresh): 110 - 80 = 30
+ // Total: 10 + 20 + 30 = 60
+ assertThat(stats.getAggregateBatteryConsumer(
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+ .getConsumedPower(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+ .isWithin(0.0001)
+ .of(360.0); // 360 mA * 1.0 hour
+ assertThat(stats.getAggregateBatteryConsumer(
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+ .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+ .isEqualTo(60 * MINUTE_IN_MS);
+
+ final UidBatteryConsumer uidBatteryConsumer = stats.getUidBatteryConsumers().stream()
+ .filter(uid -> uid.getUid() == APP_UID).findFirst().get();
+ assertThat(uidBatteryConsumer
+ .getConsumedPower(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+ .isWithin(0.1)
+ .of(360.0);
+ assertThat(uidBatteryConsumer
+ .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+ .isEqualTo(60 * MINUTE_IN_MS);
+ }
+
private void setTime(long timeMs) {
mMockClock.currentTime = timeMs;
mMockClock.realtime = timeMs;
@@ -550,7 +647,8 @@
return null;
}).when(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
- mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore);
+ mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore,
+ /* accumulateBatteryUsageStats */ false);
// Make an incompatible change of supported energy components. This will trigger
// a BatteryStats reset, which will generate a snapshot of battery stats.
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index bbe0755..ac1b7c6 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -54,6 +54,7 @@
"services.flags",
"services.net",
"services.people",
+ "services.supervision",
"services.usage",
"service-permission.stubs.system_server",
"guava",
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
index 4645156..3e2949d6 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
@@ -16,7 +16,9 @@
package com.android.server.audio;
+import static com.android.media.audio.Flags.FLAG_ABS_VOLUME_INDEX_FIX;
import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME;
+import static com.android.media.audio.Flags.absVolumeIndexFix;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
@@ -116,7 +118,7 @@
}
@Test
- @RequiresFlagsDisabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME)
+ @RequiresFlagsDisabled({FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME, FLAG_ABS_VOLUME_INDEX_FIX})
public void configurablePreScaleAbsoluteVolume_checkIndex() throws Exception {
AudioManager am = mContext.getSystemService(AudioManager.class);
final int minIndex = am.getStreamMinVolume(AudioManager.STREAM_MUSIC);
@@ -177,6 +179,7 @@
final AudioDeviceAttributes bleDevice = new AudioDeviceAttributes(
/*native type*/ AudioSystem.DEVICE_OUT_BLE_HEADSET, /*address*/ "bla");
final int maxPreScaleIndex = 3;
+ int passedIndex = maxIndex;
for (int i = 0; i < maxPreScaleIndex; i++) {
final VolumeInfo volCur = new VolumeInfo.Builder(volMedia)
@@ -185,9 +188,12 @@
mAudioService.setDeviceVolume(volCur, bleDevice, mPackageName);
mTestLooper.dispatchAll();
+ if (absVolumeIndexFix()) {
+ passedIndex = i + 1;
+ }
// Stream volume changes
verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
- AudioManager.STREAM_MUSIC, maxIndex,
+ AudioManager.STREAM_MUSIC, passedIndex,
AudioSystem.DEVICE_OUT_BLE_HEADSET);
}
@@ -197,8 +203,11 @@
mAudioService.setDeviceVolume(volIndex4, bleDevice, mPackageName);
mTestLooper.dispatchAll();
+ if (absVolumeIndexFix()) {
+ passedIndex = 4;
+ }
verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
- AudioManager.STREAM_MUSIC, maxIndex,
+ AudioManager.STREAM_MUSIC, passedIndex,
AudioSystem.DEVICE_OUT_BLE_HEADSET);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
index beed0a3d..c305fd9 100644
--- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
@@ -39,8 +39,9 @@
import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
-import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME;
import static com.android.media.audio.Flags.FLAG_ABS_VOLUME_INDEX_FIX;
+import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME;
+import static com.android.media.audio.Flags.absVolumeIndexFix;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -627,7 +628,6 @@
@Test
@RequiresFlagsEnabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME)
- @RequiresFlagsDisabled(FLAG_ABS_VOLUME_INDEX_FIX)
public void disablePreScaleAbsoluteVolume_checkIndex() throws Exception {
final int minIndex = mAm.getStreamMinVolume(STREAM_MUSIC);
final int maxIndex = mAm.getStreamMaxVolume(STREAM_MUSIC);
@@ -638,6 +638,7 @@
final AudioDeviceAttributes bleDevice = new AudioDeviceAttributes(
/*native type*/ AudioSystem.DEVICE_OUT_BLE_HEADSET, /*address*/ "bla");
final int maxPreScaleIndex = 3;
+ int passedIndex = maxIndex;
for (int i = 0; i < maxPreScaleIndex; i++) {
final VolumeInfo volCur = new VolumeInfo.Builder(volMedia)
@@ -646,9 +647,12 @@
mAudioService.setDeviceVolume(volCur, bleDevice, mContext.getOpPackageName());
mTestLooper.dispatchAll();
+ if (absVolumeIndexFix()) {
+ passedIndex = i + 1;
+ }
// Stream volume changes
verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
- STREAM_MUSIC, maxIndex,
+ STREAM_MUSIC, passedIndex,
AudioSystem.DEVICE_OUT_BLE_HEADSET);
}
@@ -658,8 +662,11 @@
mAudioService.setDeviceVolume(volIndex4, bleDevice, mContext.getOpPackageName());
mTestLooper.dispatchAll();
+ if (absVolumeIndexFix()) {
+ passedIndex = 4;
+ }
verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
- STREAM_MUSIC, maxIndex,
+ STREAM_MUSIC, passedIndex,
AudioSystem.DEVICE_OUT_BLE_HEADSET);
}
diff --git a/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java b/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java
index f91f77a..cdfc521 100644
--- a/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java
@@ -86,7 +86,6 @@
private File mDataBlockFile;
private File mFrpSecretFile;
private File mFrpSecretTmpFile;
- private String mOemUnlockPropertyValue;
private boolean mIsUpgradingFromPreV = false;
@Mock private UserManager mUserManager;
@@ -105,13 +104,6 @@
}
@Override
- void setProperty(String key, String value) {
- // Override to capture the value instead of actually setting the property.
- assertThat(key).isEqualTo("sys.oem_unlock_allowed");
- mOemUnlockPropertyValue = value;
- }
-
- @Override
boolean isUpgradingFromPreVRelease() {
return mIsUpgradingFromPreV;
}
@@ -598,7 +590,6 @@
mInterface.setOemUnlockEnabled(true);
assertThat(mInterface.getOemUnlockEnabled()).isTrue();
- assertThat(mOemUnlockPropertyValue).isEqualTo("1");
}
@Test
@@ -635,7 +626,6 @@
// The current implementation does not check digest before set or get the oem unlock bit.
tamperWithDigest();
mInterface.setOemUnlockEnabled(true);
- assertThat(mOemUnlockPropertyValue).isEqualTo("1");
tamperWithDigest();
assertThat(mInterface.getOemUnlockEnabled()).isTrue();
}
@@ -676,7 +666,6 @@
mInternalInterface.forceOemUnlockEnabled(true);
- assertThat(mOemUnlockPropertyValue).isEqualTo("1");
assertThat(readBackingFile(mPdbService.getOemUnlockDataOffset(), 1).array())
.isEqualTo(new byte[] { 1 });
}
diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
new file mode 100644
index 0000000..6bd4279
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.supervision
+
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.server.LocalServices
+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
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+/**
+ * Unit tests for {@link SupervisionService}.
+ * <p/>
+ * Run with <code>atest SupervisionServiceTest</code>.
+ */
+@RunWith(AndroidJUnit4::class)
+class SupervisionServiceTest {
+ companion object {
+ const val USER_ID = 100
+ }
+
+ 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
+
+ LocalServices.removeServiceForTest(UserManagerInternal::class.java)
+ LocalServices.addService(UserManagerInternal::class.java, mockUserManagerInternal)
+
+ service = SupervisionService(context)
+ }
+
+ @Test
+ fun testSetSupervisionEnabledForUser() {
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+
+ service.setSupervisionEnabledForUser(USER_ID, true)
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+
+ service.setSupervisionEnabledForUser(USER_ID, false)
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+ }
+
+ @Test
+ fun testSetSupervisionLockscreenEnabledForUser() {
+ var userData = service.getUserDataLocked(USER_ID)
+ assertThat(userData.supervisionLockScreenEnabled).isFalse()
+ assertThat(userData.supervisionLockScreenOptions).isNull()
+
+ service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, true, Bundle())
+ userData = service.getUserDataLocked(USER_ID)
+ assertThat(userData.supervisionLockScreenEnabled).isTrue()
+ assertThat(userData.supervisionLockScreenOptions).isNotNull()
+
+ service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, false, null)
+ userData = service.getUserDataLocked(USER_ID)
+ assertThat(userData.supervisionLockScreenEnabled).isFalse()
+ assertThat(userData.supervisionLockScreenOptions).isNull()
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index d69d678..6c9015d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -16611,7 +16611,7 @@
mService.addNotification(r);
mService.addEnqueuedNotification(r1);
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
waitForIdle();
@@ -16651,9 +16651,9 @@
mService.addNotification(r);
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
waitForIdle();
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
waitForIdle();
ArgumentCaptor<NotificationRecord> captor =
@@ -16668,7 +16668,7 @@
mContext.getTestablePermissions().setPermission(
android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
// start from true state
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
// qualifying posted notification
Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
@@ -16709,7 +16709,7 @@
mService.addNotification(r);
mService.addEnqueuedNotification(r1);
- mBinderService.setCanBePromoted(mPkg, mUid, false);
+ mBinderService.setCanBePromoted(mPkg, mUid, false, true);
waitForIdle();
@@ -16733,7 +16733,7 @@
mContext.getTestablePermissions().setPermission(
android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
// start from true state
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
// qualifying posted notification
Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
@@ -16751,9 +16751,9 @@
mService.addNotification(r);
- mBinderService.setCanBePromoted(mPkg, mUid, false);
+ mBinderService.setCanBePromoted(mPkg, mUid, false, true);
waitForIdle();
- mBinderService.setCanBePromoted(mPkg, mUid, false);
+ mBinderService.setCanBePromoted(mPkg, mUid, false, true);
waitForIdle();
ArgumentCaptor<NotificationRecord> captor =
@@ -16765,7 +16765,7 @@
@Test
@EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testPostPromotableNotification() throws Exception {
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
assertThat(mBinderService.appCanBePromoted(mPkg, mUid)).isTrue();
mContext.getTestablePermissions().setPermission(
android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
@@ -16823,7 +16823,7 @@
@Test
@EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testPostPromotableNotification_unimportantNotification() throws Exception {
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
mContext.getTestablePermissions().setPermission(
android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
Notification n = new Notification.Builder(mContext, mMinChannel.getId())
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 7d63062..a0c0df8 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -62,6 +62,7 @@
import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA;
import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER;
import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE;
+import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_PROMOTABLE;
import static com.android.server.notification.PreferencesHelper.NOTIFICATION_CHANNEL_COUNT_LIMIT;
import static com.android.server.notification.PreferencesHelper.NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT;
import static com.android.server.notification.PreferencesHelper.UNKNOWN_UID;
@@ -643,7 +644,7 @@
mHelper.setShowBadge(PKG_N_MR1, UID_N_MR1, true);
if (android.app.Flags.uiRichOngoing()) {
- mHelper.setCanBePromoted(PKG_N_MR1, UID_N_MR1, true);
+ mHelper.setCanBePromoted(PKG_N_MR1, UID_N_MR1, true, true);
}
ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, false,
@@ -657,6 +658,8 @@
assertTrue(mXmlHelper.canShowBadge(PKG_N_MR1, UID_N_MR1));
if (android.app.Flags.uiRichOngoing()) {
assertThat(mXmlHelper.canBePromoted(PKG_N_MR1, UID_N_MR1)).isTrue();
+ assertThat(mXmlHelper.getAppLockedFields(PKG_N_MR1, UID_N_MR1) & USER_LOCKED_PROMOTABLE)
+ .isNotEqualTo(0);
}
assertEquals(channel1,
mXmlHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId(), false));
@@ -6311,20 +6314,30 @@
}
@Test
- @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testNoAppHasPermissionToPromoteByDefault() {
mHelper.setShowBadge(PKG_P, UID_P, true);
assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isFalse();
}
@Test
- @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testSetCanBePromoted() {
- mHelper.setCanBePromoted(PKG_P, UID_P, true);
+ mHelper.setCanBePromoted(PKG_P, UID_P, true, true);
assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
- mHelper.setCanBePromoted(PKG_P, UID_P, false);
+ mHelper.setCanBePromoted(PKG_P, UID_P, false, true);
assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isFalse();
verify(mHandler, never()).requestSort();
}
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
+ public void testSetCanBePromoted_allowlistNotOverrideUser() {
+ mHelper.setCanBePromoted(PKG_P, UID_P, true, true);
+ assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
+
+ mHelper.setCanBePromoted(PKG_P, UID_P, false, false);
+ assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
index 1c33116..6f9c890 100644
--- a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
@@ -22,6 +22,7 @@
import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS;
import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE;
+import android.platform.test.annotations.DisableFlags;
import android.view.ViewConfiguration;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -39,6 +40,7 @@
*/
@MediumTest
@RunWith(AndroidJUnit4.class)
+@DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_PRESS_GESTURES)
public class CombinationKeyTests extends ShortcutKeyTestBase {
private static final long A11Y_KEY_HOLD_MILLIS = 3500;
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
index 487390d..8b5f68a 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
@@ -71,12 +71,12 @@
new KeyCombinationManager.TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN,
KEYCODE_POWER) {
@Override
- void execute() {
+ public void execute() {
mAction1Triggered.countDown();
}
@Override
- void cancel() {
+ public void cancel() {
}
});
@@ -85,21 +85,21 @@
new KeyCombinationManager.TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN,
KEYCODE_VOLUME_UP) {
@Override
- boolean preCondition() {
+ public boolean preCondition() {
return mPreCondition;
}
@Override
- void execute() {
+ public void execute() {
mAction2Triggered.countDown();
}
@Override
- void cancel() {
+ public void cancel() {
}
@Override
- long getKeyInterceptDelayMs() {
+ public long getKeyInterceptDelayMs() {
return 0;
}
});
@@ -115,12 +115,12 @@
};
@Override
- void execute() {
+ public void execute() {
mHandler.postDelayed(mAction, SCHEDULE_TIME);
}
@Override
- void cancel() {
+ public void cancel() {
mHandler.removeCallbacks(mAction);
}
});
@@ -235,12 +235,12 @@
new KeyCombinationManager.TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN,
KEYCODE_POWER) {
@Override
- void execute() {
+ public void execute() {
mAction1Triggered.countDown();
}
@Override
- void cancel() {
+ public void cancel() {
}
};
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
index 3d978e4..cdb4542 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
@@ -22,10 +22,13 @@
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_ALL_APPS;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_ASSIST;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_NOTIFICATION_PANEL;
+import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS;
+import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE;
import static com.android.server.policy.PhoneWindowManager.SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL;
import android.hardware.input.KeyGestureEvent;
import android.os.RemoteException;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.KeyEvent;
@@ -56,88 +59,15 @@
private static final int CTRL_ON = MODIFIER.get(KeyEvent.KEYCODE_CTRL_LEFT);
@Keep
- private static Object[][] shortcutTestArguments() {
+ private static Object[][] shortcutTestArgumentsNotMigratedToKeyGestureController() {
// testName, testKeys, expectedKeyGestureType, expectedKey, expectedModifierState
return new Object[][]{
- {"Meta + H -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_H},
- KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyEvent.KEYCODE_H, META_ON},
- {"Meta + Enter -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
- KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyEvent.KEYCODE_ENTER,
- META_ON},
{"HOME key -> Open Home", new int[]{KeyEvent.KEYCODE_HOME},
KeyGestureEvent.KEY_GESTURE_TYPE_HOME,
KeyEvent.KEYCODE_HOME, 0},
- {"RECENT_APPS key -> Open Overview", new int[]{KeyEvent.KEYCODE_RECENT_APPS},
- KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS,
- KeyEvent.KEYCODE_RECENT_APPS, 0},
- {"Meta + Tab -> Open Overview", new int[]{META_KEY, KeyEvent.KEYCODE_TAB},
- KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyEvent.KEYCODE_TAB,
- META_ON},
- {"Alt + Tab -> Open Overview", new int[]{ALT_KEY, KeyEvent.KEYCODE_TAB},
- KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyEvent.KEYCODE_TAB,
- ALT_ON},
{"BACK key -> Go back", new int[]{KeyEvent.KEYCODE_BACK},
KeyGestureEvent.KEY_GESTURE_TYPE_BACK,
KeyEvent.KEYCODE_BACK, 0},
- {"Meta + Escape -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_ESCAPE},
- KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_ESCAPE,
- META_ON},
- {"Meta + Left arrow -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
- KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DPAD_LEFT,
- META_ON},
- {"Meta + Del -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DEL},
- KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DEL, META_ON},
- {"APP_SWITCH key -> Open App switcher", new int[]{KeyEvent.KEYCODE_APP_SWITCH},
- KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
- KeyEvent.KEYCODE_APP_SWITCH, 0},
- {"ASSIST key -> Launch assistant", new int[]{KeyEvent.KEYCODE_ASSIST},
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
- KeyEvent.KEYCODE_ASSIST, 0},
- {"Meta + A -> Launch assistant", new int[]{META_KEY, KeyEvent.KEYCODE_A},
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, KeyEvent.KEYCODE_A,
- META_ON},
- {"VOICE_ASSIST key -> Launch Voice Assistant",
- new int[]{KeyEvent.KEYCODE_VOICE_ASSIST},
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT,
- KeyEvent.KEYCODE_VOICE_ASSIST, 0},
- {"Meta + I -> Launch System Settings", new int[]{META_KEY, KeyEvent.KEYCODE_I},
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS,
- KeyEvent.KEYCODE_I, META_ON},
- {"Meta + N -> Toggle Notification panel", new int[]{META_KEY, KeyEvent.KEYCODE_N},
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
- KeyEvent.KEYCODE_N, META_ON},
- {"NOTIFICATION key -> Toggle Notification Panel",
- new int[]{KeyEvent.KEYCODE_NOTIFICATION},
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
- KeyEvent.KEYCODE_NOTIFICATION,
- 0},
- {"Meta + Ctrl + S -> Take Screenshot",
- new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S},
- KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, KeyEvent.KEYCODE_S,
- META_ON | CTRL_ON},
- {"Meta + / -> Open Shortcut Helper", new int[]{META_KEY, KeyEvent.KEYCODE_SLASH},
- KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
- KeyEvent.KEYCODE_SLASH, META_ON},
- {"BRIGHTNESS_UP key -> Increase Brightness",
- new int[]{KeyEvent.KEYCODE_BRIGHTNESS_UP},
- KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP,
- KeyEvent.KEYCODE_BRIGHTNESS_UP, 0},
- {"BRIGHTNESS_DOWN key -> Decrease Brightness",
- new int[]{KeyEvent.KEYCODE_BRIGHTNESS_DOWN},
- KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN,
- KeyEvent.KEYCODE_BRIGHTNESS_DOWN, 0},
- {"KEYBOARD_BACKLIGHT_UP key -> Increase Keyboard Backlight",
- new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP},
- KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP,
- KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP, 0},
- {"KEYBOARD_BACKLIGHT_DOWN key -> Decrease Keyboard Backlight",
- new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN},
- KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN,
- KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN, 0},
- {"KEYBOARD_BACKLIGHT_TOGGLE key -> Toggle Keyboard Backlight",
- new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE},
- KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE,
- KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE, 0},
{"VOLUME_UP key -> Increase Volume", new int[]{KeyEvent.KEYCODE_VOLUME_UP},
KeyGestureEvent.KEY_GESTURE_TYPE_VOLUME_UP,
KeyEvent.KEYCODE_VOLUME_UP, 0},
@@ -147,53 +77,9 @@
{"VOLUME_MUTE key -> Mute Volume", new int[]{KeyEvent.KEYCODE_VOLUME_MUTE},
KeyGestureEvent.KEY_GESTURE_TYPE_VOLUME_MUTE,
KeyEvent.KEYCODE_VOLUME_MUTE, 0},
- {"ALL_APPS key -> Open App Drawer",
- new int[]{KeyEvent.KEYCODE_ALL_APPS},
- KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
- KeyEvent.KEYCODE_ALL_APPS, 0},
- {"SEARCH key -> Launch Search Activity", new int[]{KeyEvent.KEYCODE_SEARCH},
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
- KeyEvent.KEYCODE_SEARCH, 0},
- {"LANGUAGE_SWITCH key -> Switch Keyboard Language",
- new int[]{KeyEvent.KEYCODE_LANGUAGE_SWITCH},
- KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
- KeyEvent.KEYCODE_LANGUAGE_SWITCH, 0},
- {"META key -> Open App Drawer", new int[]{META_KEY},
- KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, META_KEY,
- META_ON},
- {"Meta + Alt -> Toggle CapsLock", new int[]{META_KEY, ALT_KEY},
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, ALT_KEY,
- META_ON | ALT_ON},
- {"Alt + Meta -> Toggle CapsLock", new int[]{ALT_KEY, META_KEY},
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, META_KEY,
- META_ON | ALT_ON},
- {"CAPS_LOCK key -> Toggle CapsLock", new int[]{KeyEvent.KEYCODE_CAPS_LOCK},
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
- KeyEvent.KEYCODE_CAPS_LOCK, 0},
{"MUTE key -> Mute System Microphone", new int[]{KeyEvent.KEYCODE_MUTE},
KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_MUTE, KeyEvent.KEYCODE_MUTE,
0},
- {"Meta + Ctrl + DPAD_UP -> Split screen navigation",
- new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_UP},
- KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
- KeyEvent.KEYCODE_DPAD_UP,
- META_ON | CTRL_ON},
- {"Meta + Ctrl + DPAD_LEFT -> Split screen navigation",
- new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
- KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT,
- KeyEvent.KEYCODE_DPAD_LEFT,
- META_ON | CTRL_ON},
- {"Meta + Ctrl + DPAD_RIGHT -> Split screen navigation",
- new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_RIGHT},
- KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT,
- KeyEvent.KEYCODE_DPAD_RIGHT,
- META_ON | CTRL_ON},
- {"Meta + L -> Lock Homescreen", new int[]{META_KEY, KeyEvent.KEYCODE_L},
- KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, KeyEvent.KEYCODE_L,
- META_ON},
- {"Meta + Ctrl + N -> Open Notes", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_N},
- KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES, KeyEvent.KEYCODE_N,
- META_ON | CTRL_ON},
{"POWER key -> Toggle Power", new int[]{KeyEvent.KEYCODE_POWER},
KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_POWER, KeyEvent.KEYCODE_POWER,
0},
@@ -279,7 +165,130 @@
{"Meta + S -> Launch Default Messaging App",
new int[]{META_KEY, KeyEvent.KEYCODE_S},
KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING,
- KeyEvent.KEYCODE_S, META_ON},
+ KeyEvent.KEYCODE_S, META_ON}};
+ }
+
+ @Keep
+ private static Object[][] shortcutTestArgumentsMigratedToKeyGestureController() {
+ // testName, testKeys, expectedKeyGestureType, expectedKey, expectedModifierState
+ return new Object[][]{
+ {"Meta + H -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_H},
+ KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyEvent.KEYCODE_H, META_ON},
+ {"Meta + Enter -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
+ KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyEvent.KEYCODE_ENTER,
+ META_ON},
+ {"RECENT_APPS key -> Open Overview", new int[]{KeyEvent.KEYCODE_RECENT_APPS},
+ KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS,
+ KeyEvent.KEYCODE_RECENT_APPS, 0},
+ {"Meta + Tab -> Open Overview", new int[]{META_KEY, KeyEvent.KEYCODE_TAB},
+ KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyEvent.KEYCODE_TAB,
+ META_ON},
+ {"Alt + Tab -> Open Overview", new int[]{ALT_KEY, KeyEvent.KEYCODE_TAB},
+ KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyEvent.KEYCODE_TAB,
+ ALT_ON},
+ {"Meta + Escape -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_ESCAPE},
+ KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_ESCAPE,
+ META_ON},
+ {"Meta + Left arrow -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
+ KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DPAD_LEFT,
+ META_ON},
+ {"Meta + Del -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DEL},
+ KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DEL, META_ON},
+ {"APP_SWITCH key -> Open App switcher", new int[]{KeyEvent.KEYCODE_APP_SWITCH},
+ KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
+ KeyEvent.KEYCODE_APP_SWITCH, 0},
+ {"ASSIST key -> Launch assistant", new int[]{KeyEvent.KEYCODE_ASSIST},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
+ KeyEvent.KEYCODE_ASSIST, 0},
+ {"Meta + A -> Launch assistant", new int[]{META_KEY, KeyEvent.KEYCODE_A},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, KeyEvent.KEYCODE_A,
+ META_ON},
+ {"VOICE_ASSIST key -> Launch Voice Assistant",
+ new int[]{KeyEvent.KEYCODE_VOICE_ASSIST},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT,
+ KeyEvent.KEYCODE_VOICE_ASSIST, 0},
+ {"Meta + I -> Launch System Settings", new int[]{META_KEY, KeyEvent.KEYCODE_I},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS,
+ KeyEvent.KEYCODE_I, META_ON},
+ {"Meta + N -> Toggle Notification panel", new int[]{META_KEY, KeyEvent.KEYCODE_N},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+ KeyEvent.KEYCODE_N, META_ON},
+ {"NOTIFICATION key -> Toggle Notification Panel",
+ new int[]{KeyEvent.KEYCODE_NOTIFICATION},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+ KeyEvent.KEYCODE_NOTIFICATION,
+ 0},
+ {"Meta + Ctrl + S -> Take Screenshot",
+ new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, KeyEvent.KEYCODE_S,
+ META_ON | CTRL_ON},
+ {"Meta + / -> Open Shortcut Helper", new int[]{META_KEY, KeyEvent.KEYCODE_SLASH},
+ KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
+ KeyEvent.KEYCODE_SLASH, META_ON},
+ {"BRIGHTNESS_UP key -> Increase Brightness",
+ new int[]{KeyEvent.KEYCODE_BRIGHTNESS_UP},
+ KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP,
+ KeyEvent.KEYCODE_BRIGHTNESS_UP, 0},
+ {"BRIGHTNESS_DOWN key -> Decrease Brightness",
+ new int[]{KeyEvent.KEYCODE_BRIGHTNESS_DOWN},
+ KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN,
+ KeyEvent.KEYCODE_BRIGHTNESS_DOWN, 0},
+ {"KEYBOARD_BACKLIGHT_UP key -> Increase Keyboard Backlight",
+ new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP},
+ KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP, 0},
+ {"KEYBOARD_BACKLIGHT_DOWN key -> Decrease Keyboard Backlight",
+ new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN},
+ KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN, 0},
+ {"KEYBOARD_BACKLIGHT_TOGGLE key -> Toggle Keyboard Backlight",
+ new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE},
+ KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE, 0},
+ {"ALL_APPS key -> Open App Drawer",
+ new int[]{KeyEvent.KEYCODE_ALL_APPS},
+ KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
+ KeyEvent.KEYCODE_ALL_APPS, 0},
+ {"SEARCH key -> Launch Search Activity", new int[]{KeyEvent.KEYCODE_SEARCH},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
+ KeyEvent.KEYCODE_SEARCH, 0},
+ {"LANGUAGE_SWITCH key -> Switch Keyboard Language",
+ new int[]{KeyEvent.KEYCODE_LANGUAGE_SWITCH},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+ KeyEvent.KEYCODE_LANGUAGE_SWITCH, 0},
+ {"META key -> Open App Drawer", new int[]{META_KEY},
+ KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, META_KEY,
+ META_ON},
+ {"Meta + Alt -> Toggle CapsLock", new int[]{META_KEY, ALT_KEY},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, ALT_KEY,
+ META_ON | ALT_ON},
+ {"Alt + Meta -> Toggle CapsLock", new int[]{ALT_KEY, META_KEY},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, META_KEY,
+ META_ON | ALT_ON},
+ {"CAPS_LOCK key -> Toggle CapsLock", new int[]{KeyEvent.KEYCODE_CAPS_LOCK},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
+ KeyEvent.KEYCODE_CAPS_LOCK, 0},
+ {"Meta + Ctrl + DPAD_UP -> Split screen navigation",
+ new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_UP},
+ KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
+ KeyEvent.KEYCODE_DPAD_UP,
+ META_ON | CTRL_ON},
+ {"Meta + Ctrl + DPAD_LEFT -> Split screen navigation",
+ new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
+ KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT,
+ KeyEvent.KEYCODE_DPAD_LEFT,
+ META_ON | CTRL_ON},
+ {"Meta + Ctrl + DPAD_RIGHT -> Split screen navigation",
+ new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_RIGHT},
+ KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT,
+ KeyEvent.KEYCODE_DPAD_RIGHT,
+ META_ON | CTRL_ON},
+ {"Meta + L -> Lock Homescreen", new int[]{META_KEY, KeyEvent.KEYCODE_L},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, KeyEvent.KEYCODE_L,
+ META_ON},
+ {"Meta + Ctrl + N -> Open Notes", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_N},
+ KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES, KeyEvent.KEYCODE_N,
+ META_ON | CTRL_ON},
{"Meta + Ctrl + DPAD_DOWN -> Enter desktop mode",
new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_DOWN},
KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE,
@@ -296,72 +305,14 @@
new int[]{KeyEvent.KEYCODE_HOME}, LONG_PRESS_HOME_NOTIFICATION_PANEL,
KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
KeyEvent.KEYCODE_HOME, 0},
- {"Long press META + ENTER -> Toggle Notification panel",
- new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
- LONG_PRESS_HOME_NOTIFICATION_PANEL,
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
- KeyEvent.KEYCODE_ENTER,
- META_ON},
- {"Long press META + H -> Toggle Notification panel",
- new int[]{META_KEY, KeyEvent.KEYCODE_H}, LONG_PRESS_HOME_NOTIFICATION_PANEL,
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
- KeyEvent.KEYCODE_H, META_ON},
{"Long press HOME key -> Launch assistant",
new int[]{KeyEvent.KEYCODE_HOME}, LONG_PRESS_HOME_ASSIST,
KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
KeyEvent.KEYCODE_HOME, 0},
- {"Long press META + ENTER -> Launch assistant",
- new int[]{META_KEY, KeyEvent.KEYCODE_ENTER}, LONG_PRESS_HOME_ASSIST,
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
- KeyEvent.KEYCODE_ENTER, META_ON},
- {"Long press META + H -> Launch assistant",
- new int[]{META_KEY, KeyEvent.KEYCODE_H}, LONG_PRESS_HOME_ASSIST,
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, KeyEvent.KEYCODE_H,
- META_ON},
{"Long press HOME key -> Open App Drawer",
new int[]{KeyEvent.KEYCODE_HOME}, LONG_PRESS_HOME_ALL_APPS,
KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
- KeyEvent.KEYCODE_HOME, 0},
- {"Long press META + ENTER -> Open App Drawer",
- new int[]{META_KEY, KeyEvent.KEYCODE_ENTER}, LONG_PRESS_HOME_ALL_APPS,
- KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
- KeyEvent.KEYCODE_ENTER, META_ON},
- {"Long press META + H -> Open App Drawer",
- new int[]{META_KEY, KeyEvent.KEYCODE_H},
- LONG_PRESS_HOME_ALL_APPS,
- KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
- KeyEvent.KEYCODE_H, META_ON}};
- }
-
- @Keep
- private static Object[][] doubleTapOnHomeTestArguments() {
- // testName, testKeys, doubleTapOnHomeBehavior, expectedKeyGestureType, expectedKey,
- // expectedModifierState
- return new Object[][]{
- {"Double tap HOME -> Open App switcher",
- new int[]{KeyEvent.KEYCODE_HOME}, DOUBLE_TAP_HOME_RECENT_SYSTEM_UI,
- KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH, KeyEvent.KEYCODE_HOME,
- 0},
- {"Double tap META + ENTER -> Open App switcher",
- new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
- DOUBLE_TAP_HOME_RECENT_SYSTEM_UI,
- KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
- KeyEvent.KEYCODE_ENTER, META_ON},
- {"Double tap META + H -> Open App switcher",
- new int[]{META_KEY, KeyEvent.KEYCODE_H}, DOUBLE_TAP_HOME_RECENT_SYSTEM_UI,
- KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH, KeyEvent.KEYCODE_H,
- META_ON}};
- }
-
- @Keep
- private static Object[][] settingsKeyTestArguments() {
- // testName, testKeys, settingsKeyBehavior, expectedKeyGestureType, expectedKey,
- // expectedModifierState
- return new Object[][]{
- {"SETTINGS key -> Toggle Notification panel", new int[]{KeyEvent.KEYCODE_SETTINGS},
- SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL,
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
- KeyEvent.KEYCODE_SETTINGS, 0}};
+ KeyEvent.KEYCODE_HOME, 0}};
}
@Before
@@ -381,8 +332,18 @@
}
@Test
- @Parameters(method = "shortcutTestArguments")
- public void testShortcut(String testName, int[] testKeys,
+ @Parameters(method = "shortcutTestArgumentsNotMigratedToKeyGestureController")
+ public void testShortcuts_notMigratedToKeyGestureController(String testName,
+ int[] testKeys, @KeyGestureEvent.KeyGestureType int expectedKeyGestureType,
+ int expectedKey, int expectedModifierState) {
+ testShortcutInternal(testName, testKeys, expectedKeyGestureType, expectedKey,
+ expectedModifierState);
+ }
+
+ @Test
+ @Parameters(method = "shortcutTestArgumentsMigratedToKeyGestureController")
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+ public void testShortcuts_migratedToKeyGestureController(String testName, int[] testKeys,
@KeyGestureEvent.KeyGestureType int expectedKeyGestureType, int expectedKey,
int expectedModifierState) {
testShortcutInternal(testName, testKeys, expectedKeyGestureType, expectedKey,
@@ -402,31 +363,29 @@
}
@Test
- @Parameters(method = "doubleTapOnHomeTestArguments")
- public void testDoubleTapOnHomeBehavior(String testName, int[] testKeys,
- int doubleTapOnHomeBehavior,
- @KeyGestureEvent.KeyGestureType int expectedKeyGestureType, int expectedKey,
- int expectedModifierState) {
- mPhoneWindowManager.overriderDoubleTapOnHomeBehavior(doubleTapOnHomeBehavior);
- sendKeyCombination(testKeys, 0 /* duration */);
- sendKeyCombination(testKeys, 0 /* duration */);
+ public void testDoubleTapOnHomeBehavior_AppSwitchBehavior() {
+ mPhoneWindowManager.overriderDoubleTapOnHomeBehavior(DOUBLE_TAP_HOME_RECENT_SYSTEM_UI);
+ sendKeyCombination(new int[]{KeyEvent.KEYCODE_HOME}, 0 /* duration */);
+ sendKeyCombination(new int[]{KeyEvent.KEYCODE_HOME}, 0 /* duration */);
mPhoneWindowManager.assertKeyGestureCompleted(
- new int[]{expectedKey}, expectedModifierState, expectedKeyGestureType,
- "Failed while executing " + testName);
+ new int[]{KeyEvent.KEYCODE_HOME}, /* modifierState = */0,
+ KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
+ "Failed while executing Double tap HOME -> Open App switcher");
}
@Test
- @Parameters(method = "settingsKeyTestArguments")
- public void testSettingsKey(String testName, int[] testKeys, int settingsKeyBehavior,
- @KeyGestureEvent.KeyGestureType int expectedKeyGestureType, int expectedKey,
- int expectedModifierState) {
- mPhoneWindowManager.overrideSettingsKeyBehavior(settingsKeyBehavior);
- testShortcutInternal(testName, testKeys, expectedKeyGestureType, expectedKey,
- expectedModifierState);
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+ public void testSettingsKey_ToggleNotificationBehavior() {
+ mPhoneWindowManager.overrideSettingsKeyBehavior(SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL);
+ testShortcutInternal("SETTINGS key -> Toggle Notification panel",
+ new int[]{KeyEvent.KEYCODE_SETTINGS},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+ KeyEvent.KEYCODE_SETTINGS, 0);
}
@Test
@EnableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testBugreportShortcutPress() {
testShortcutInternal("Meta + Ctrl + Del -> Trigger bug report",
new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DEL},
@@ -599,4 +558,145 @@
sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH));
mPhoneWindowManager.assertLaunchSearch();
}
+
+ @Test
+ public void testKeyGestureScreenshotChord() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+ mPhoneWindowManager.moveTimeForward(500);
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+ mPhoneWindowManager.assertTakeScreenshotCalled();
+ }
+
+ @Test
+ public void testKeyGestureScreenshotChordCancelled() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+ mPhoneWindowManager.assertTakeScreenshotNotCalled();
+ }
+
+ @Test
+ public void testKeyGestureAccessibilityShortcutChord() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
+ mPhoneWindowManager.moveTimeForward(5000);
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
+ mPhoneWindowManager.assertAccessibilityKeychordCalled();
+ }
+
+ @Test
+ public void testKeyGestureAccessibilityShortcutChordCancelled() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
+ mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
+ }
+
+ @Test
+ public void testKeyGestureRingerToggleChord() {
+ mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+ mPhoneWindowManager.moveTimeForward(500);
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+ mPhoneWindowManager.assertVolumeMute();
+ }
+
+ @Test
+ public void testKeyGestureRingerToggleChordCancelled() {
+ mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+ mPhoneWindowManager.assertVolumeNotMuted();
+ }
+
+ @Test
+ public void testKeyGestureGlobalAction() {
+ mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS);
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+ mPhoneWindowManager.moveTimeForward(500);
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+ mPhoneWindowManager.assertShowGlobalActionsCalled();
+ }
+
+ @Test
+ public void testKeyGestureGlobalActionCancelled() {
+ mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS);
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+ mPhoneWindowManager.assertShowGlobalActionsNotCalled();
+ }
+
+ @Test
+ public void testKeyGestureAccessibilityTvShortcutChord() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
+ mPhoneWindowManager.moveTimeForward(5000);
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
+ mPhoneWindowManager.assertAccessibilityKeychordCalled();
+ }
+
+ @Test
+ public void testKeyGestureAccessibilityTvShortcutChordCancelled() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
+ mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
+ }
+
+ @Test
+ public void testKeyGestureTvTriggerBugReport() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+ mPhoneWindowManager.moveTimeForward(1000);
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+ mPhoneWindowManager.assertBugReportTakenForTv();
+ }
+
+ @Test
+ public void testKeyGestureTvTriggerBugReportCancelled() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+ mPhoneWindowManager.assertBugReportNotTakenForTv();
+ }
+
+ @Test
+ public void testKeyGestureAccessibilityShortcut() {
+ Assert.assertTrue(
+ sendKeyGestureEventComplete(
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT));
+ mPhoneWindowManager.assertAccessibilityKeychordCalled();
+ }
+
+ @Test
+ public void testKeyGestureCloseAllDialogs() {
+ Assert.assertTrue(
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS));
+ mPhoneWindowManager.assertCloseAllDialogs();
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
index 43171f8..c186a03 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
@@ -119,6 +119,7 @@
* ALT + TAB to show recent apps.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testAltTab() {
mPhoneWindowManager.overrideStatusBarManagerInternal();
sendKeyCombination(new int[]{KEYCODE_ALT_LEFT, KEYCODE_TAB}, 0);
@@ -129,6 +130,7 @@
* CTRL + SPACE to switch keyboard layout.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testCtrlSpace() {
sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_SPACE}, /* duration= */ 0,
ANY_DISPLAY_ID);
@@ -139,6 +141,7 @@
* CTRL + SHIFT + SPACE to switch keyboard layout backwards.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testCtrlShiftSpace() {
sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_SHIFT_LEFT, KEYCODE_SPACE},
/* duration= */ 0, ANY_DISPLAY_ID);
@@ -149,6 +152,7 @@
* CTRL + ALT + Z to enable accessibility service.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testCtrlAltZ() {
sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_ALT_LEFT, KEYCODE_Z}, 0);
mPhoneWindowManager.assertAccessibilityKeychordCalled();
@@ -158,6 +162,7 @@
* META + CTRL+ S to take screenshot.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testMetaCtrlS() {
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_S}, 0);
mPhoneWindowManager.assertTakeScreenshotCalled();
@@ -167,6 +172,7 @@
* META + N to expand notification panel.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testMetaN() throws RemoteException {
mPhoneWindowManager.overrideTogglePanel();
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_N}, 0);
@@ -177,6 +183,7 @@
* META + SLASH to toggle shortcuts menu.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testMetaSlash() {
mPhoneWindowManager.overrideStatusBarManagerInternal();
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_SLASH}, 0);
@@ -187,6 +194,7 @@
* META + ALT to toggle Cap Lock.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testMetaAlt() {
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_ALT_LEFT}, 0);
mPhoneWindowManager.assertToggleCapsLock();
@@ -196,6 +204,7 @@
* META + H to go to homescreen
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testMetaH() {
mPhoneWindowManager.overrideLaunchHome();
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_H}, 0);
@@ -206,6 +215,7 @@
* META + ENTER to go to homescreen
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testMetaEnter() {
mPhoneWindowManager.overrideLaunchHome();
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_ENTER}, 0);
@@ -216,6 +226,7 @@
* Sends a KEYCODE_BRIGHTNESS_DOWN event and validates the brightness is decreased as expected;
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testKeyCodeBrightnessDown() {
float[] currentBrightness = new float[]{0.1f, 0.05f, 0.0f};
float[] newBrightness = new float[]{0.065738f, 0.0275134f, 0.0f};
@@ -231,9 +242,9 @@
* Sends a KEYCODE_SCREENSHOT and validates screenshot is taken if flag is enabled
*/
@Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testTakeScreenshot_flagEnabled() {
- mSetFlagsRule.enableFlags(com.android.hardware.input.Flags
- .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
mPhoneWindowManager.assertTakeScreenshotCalled();
}
@@ -242,9 +253,9 @@
* Sends a KEYCODE_SCREENSHOT and validates screenshot is not taken if flag is disabled
*/
@Test
+ @DisableFlags({com.android.hardware.input.Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE,
+ com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER})
public void testTakeScreenshot_flagDisabled() {
- mSetFlagsRule.disableFlags(com.android.hardware.input.Flags
- .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
mPhoneWindowManager.assertTakeScreenshotNotCalled();
}
@@ -254,6 +265,7 @@
*/
@Test
@EnableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testTakeBugReport_flagEnabled() throws RemoteException {
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_DEL}, 0);
mPhoneWindowManager.assertTakeBugreport(true);
@@ -263,7 +275,8 @@
* META+CTRL+BACKSPACE for taking a bugreport does nothing when the flag is disabledd.
*/
@Test
- @DisableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+ @DisableFlags({com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT,
+ com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER})
public void testTakeBugReport_flagDisabled() throws RemoteException {
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_DEL}, 0);
mPhoneWindowManager.assertTakeBugreport(false);
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 50b7db4..9e47a00 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -241,6 +241,13 @@
KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
}
+ boolean sendKeyGestureEventCancel(int gestureType) {
+ return mPhoneWindowManager.sendKeyGestureEvent(
+ new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction(
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE).setFlags(
+ KeyGestureEvent.FLAG_CANCELLED).build());
+ }
+
boolean sendKeyGestureEventComplete(int gestureType, int modifierState) {
return mPhoneWindowManager.sendKeyGestureEvent(
new KeyGestureEvent.Builder().setModifierState(modifierState).setKeyGestureType(
@@ -276,7 +283,7 @@
if ((actions & ACTION_PASS_TO_USER) != 0) {
if (0 == mPhoneWindowManager.interceptKeyBeforeDispatching(keyEvent)) {
if (!mDispatchedKeyHandler.onKeyDispatched(keyEvent)) {
- mPhoneWindowManager.dispatchUnhandledKey(keyEvent);
+ mPhoneWindowManager.interceptUnhandledKey(keyEvent);
}
}
}
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 98401b3..1aa9087 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -418,8 +418,8 @@
mKeyEventPolicyFlags);
}
- void dispatchUnhandledKey(KeyEvent event) {
- mPhoneWindowManager.dispatchUnhandledKey(mInputToken, event, FLAG_INTERACTIVE);
+ void interceptUnhandledKey(KeyEvent event) {
+ mPhoneWindowManager.interceptUnhandledKey(event, mInputToken);
}
boolean sendKeyGestureEvent(KeyGestureEvent event) {
@@ -657,16 +657,36 @@
verify(mPowerManager).userActivity(anyLong(), anyBoolean());
}
+ void assertShowGlobalActionsNotCalled() {
+ mTestLooper.dispatchAll();
+ verify(mGlobalActions, never()).showDialog(anyBoolean(), anyBoolean());
+ verify(mPowerManager, never()).userActivity(anyLong(), anyBoolean());
+ }
+
void assertVolumeMute() {
mTestLooper.dispatchAll();
verify(mAudioManagerInternal).silenceRingerModeInternal(eq("volume_hush"));
}
+ void assertVolumeNotMuted() {
+ mTestLooper.dispatchAll();
+ verify(mAudioManagerInternal, never()).silenceRingerModeInternal(any());
+ }
+
void assertAccessibilityKeychordCalled() {
mTestLooper.dispatchAll();
verify(mAccessibilityShortcutController).performAccessibilityShortcut();
}
+ void assertAccessibilityKeychordNotCalled() {
+ mTestLooper.dispatchAll();
+ verify(mAccessibilityShortcutController, never()).performAccessibilityShortcut();
+ }
+
+ void assertCloseAllDialogs() {
+ verify(mContext).closeSystemDialogs();
+ }
+
void assertDreamRequest() {
mTestLooper.dispatchAll();
verify(mDreamManagerInternal).requestDream();
@@ -809,6 +829,16 @@
}
+ void assertBugReportTakenForTv() {
+ mTestLooper.dispatchAll();
+ verify(mPhoneWindowManager).requestBugreportForTv();
+ }
+
+ void assertBugReportNotTakenForTv() {
+ mTestLooper.dispatchAll();
+ verify(mPhoneWindowManager, never()).requestBugreportForTv();
+ }
+
void assertTogglePanel() throws RemoteException {
mTestLooper.dispatchAll();
verify(mPhoneWindowManager.mStatusBarService).togglePanel();
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 8227ed9..63983437 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -230,6 +230,10 @@
mDisplayContent.setIgnoreOrientationRequest(enabled);
}
+ void setTopOrganizedTaskAsTopTask() {
+ doReturn(mTaskStack.top()).when(mActivityStack.top()).getOrganizedTask();
+ }
+
void setTopTaskInMultiWindowMode(boolean inMultiWindowMode) {
doReturn(inMultiWindowMode).when(mTaskStack.top()).inMultiWindowMode();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
index 21fac9b..3f34b81 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
@@ -16,10 +16,14 @@
package com.android.server.wm;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE;
+
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static org.mockito.Mockito.when;
+import android.app.CameraCompatTaskInfo.FreeformCameraCompatMode;
+import android.app.TaskInfo;
import android.platform.test.annotations.Presubmit;
import androidx.annotation.NonNull;
@@ -114,6 +118,72 @@
});
}
+ @Test
+ public void testTopActivityEligibleForUserAspectRatioButton_eligible() {
+ runTestScenario((robot) -> {
+ robot.applyOnActivity((a) -> {
+ a.createActivityWithComponentInNewTask();
+ a.setIgnoreOrientationRequest(true);
+ });
+ robot.conf().enableUserAppAspectRatioSettings(true);
+
+ robot.checkTaskInfoEligibleForUserAspectRatioButton(true);
+ });
+ }
+
+ @Test
+ public void testTopActivityEligibleForUserAspectRatioButton_disabled_notEligible() {
+ runTestScenario((robot) -> {
+ robot.applyOnActivity((a) -> {
+ a.createActivityWithComponentInNewTask();
+ a.setIgnoreOrientationRequest(true);
+ });
+ robot.conf().enableUserAppAspectRatioSettings(false);
+
+ robot.checkTaskInfoEligibleForUserAspectRatioButton(false);
+ });
+ }
+
+ @Test
+ public void testTopActivityEligibleForUserAspectRatioButton_inSizeCompatMode_notEligible() {
+ runTestScenario((robot) -> {
+ robot.applyOnActivity((a) -> {
+ a.createActivityWithComponentInNewTask();
+ a.setIgnoreOrientationRequest(true);
+ a.setTopOrganizedTaskAsTopTask();
+ a.setTopActivityInSizeCompatMode(true);
+ a.setTopActivityVisible(true);
+ });
+ robot.conf().enableUserAppAspectRatioSettings(true);
+
+ robot.checkTaskInfoEligibleForUserAspectRatioButton(false);
+ });
+ }
+
+ @Test
+ public void testTopActivityEligibleForUserAspectRatioButton_transparentTop_notEligible() {
+ runTestScenario((robot) -> {
+ robot.transparentActivity((ta) -> {
+ ta.launchTransparentActivityInTask();
+ ta.activity().setIgnoreOrientationRequest(true);
+ });
+ robot.conf().enableUserAppAspectRatioSettings(true);
+
+ robot.checkTaskInfoEligibleForUserAspectRatioButton(false);
+ });
+ }
+
+ @Test
+ public void getTaskInfoPropagatesCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.applyOnActivity(AppCompatActivityRobot::createActivityWithComponentInNewTask);
+
+ robot.setFreeformCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
+ robot.checkTaskInfoFreeformCameraCompatMode(
+ CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
+ });
+ }
+
/**
* Runs a test scenario providing a Robot.
*/
@@ -125,11 +195,14 @@
private static class AppCompatUtilsRobotTest extends AppCompatRobotBase {
private final WindowState mWindowState;
+ @NonNull
+ private final AppCompatTransparentActivityRobot mTransparentActivityRobot;
AppCompatUtilsRobotTest(@NonNull WindowManagerService wm,
@NonNull ActivityTaskManagerService atm,
@NonNull ActivityTaskSupervisor supervisor) {
super(wm, atm, supervisor);
+ mTransparentActivityRobot = new AppCompatTransparentActivityRobot(activity());
mWindowState = Mockito.mock(WindowState.class);
}
@@ -139,6 +212,12 @@
spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy());
}
+ void transparentActivity(@NonNull Consumer<AppCompatTransparentActivityRobot> consumer) {
+ // We always create at least an opaque activity in a Task.
+ activity().createNewTaskWithBaseActivity();
+ consumer.accept(mTransparentActivityRobot);
+ }
+
void setIsLetterboxedForFixedOrientationAndAspectRatio(
boolean forFixedOrientationAndAspectRatio) {
when(activity().top().mAppCompatController.getAppCompatAspectRatioPolicy()
@@ -155,11 +234,30 @@
when(mWindowState.isLetterboxedForDisplayCutout()).thenReturn(displayCutout);
}
+ void setFreeformCameraCompatMode(@FreeformCameraCompatMode int mode) {
+ activity().top().mAppCompatController.getAppCompatCameraOverrides()
+ .setFreeformCameraCompatMode(mode);
+ }
+
void checkTopActivityLetterboxReason(@NonNull String expected) {
Assert.assertEquals(expected,
AppCompatUtils.getLetterboxReasonString(activity().top(), mWindowState));
}
+ @NonNull
+ TaskInfo getTopTaskInfo() {
+ return activity().top().getTask().getTaskInfo();
+ }
+
+ void checkTaskInfoEligibleForUserAspectRatioButton(boolean eligible) {
+ Assert.assertEquals(eligible, getTopTaskInfo().appCompatTaskInfo
+ .eligibleForUserAspectRatioButton());
+ }
+
+ void checkTaskInfoFreeformCameraCompatMode(@FreeformCameraCompatMode int mode) {
+ Assert.assertEquals(mode, getTopTaskInfo().appCompatTaskInfo
+ .cameraCompatTaskInfo.freeformCameraCompatMode);
+ }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index e0344d7..df17cd1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -432,6 +432,29 @@
}
@Test
+ public void testAddTaskCompatibleWindowingMode_withFreeformAndFullscreen_expectRemove() {
+ Task task1 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK)
+ .build();
+ doReturn(WINDOWING_MODE_FREEFORM).when(task1).getWindowingMode();
+ mRecentTasks.add(task1);
+ mCallbacksRecorder.clear();
+
+ Task task2 = createTaskBuilder(".Task1")
+ .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+ .setFlags(FLAG_ACTIVITY_NEW_TASK)
+ .build();
+ assertEquals(WINDOWING_MODE_FULLSCREEN, task2.getWindowingMode());
+ mRecentTasks.add(task2);
+
+ assertThat(mCallbacksRecorder.mAdded).hasSize(1);
+ assertThat(mCallbacksRecorder.mAdded).contains(task2);
+ assertThat(mCallbacksRecorder.mTrimmed).isEmpty();
+ assertThat(mCallbacksRecorder.mRemoved).hasSize(1);
+ assertThat(mCallbacksRecorder.mRemoved).contains(task1);
+ }
+
+ @Test
public void testAddTaskIncompatibleWindowingMode_expectNoRemove() {
Task task1 = createTaskBuilder(".Task1")
.setWindowingMode(WINDOWING_MODE_FULLSCREEN)
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 4b03483..e4512c3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -18,7 +18,6 @@
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
@@ -705,50 +704,6 @@
}
@Test
- public void testTopActivityEligibleForUserAspectRatioButton() {
- DisplayContent display = mAtm.mRootWindowContainer.getDefaultDisplay();
- final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
- .setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build();
- final Task task = rootTask.getBottomMostTask();
- final ActivityRecord root = task.getTopNonFinishingActivity();
- spyOn(mWm.mAppCompatConfiguration);
- spyOn(root);
- spyOn(root.mAppCompatController.getAppCompatAspectRatioOverrides());
-
- doReturn(true).when(root).fillsParent();
- doReturn(true).when(
- root.mAppCompatController.getAppCompatAspectRatioOverrides())
- .shouldEnableUserAspectRatioSettings();
- doReturn(false).when(root).inSizeCompatMode();
- doReturn(task).when(root).getOrganizedTask();
-
- // The button should be eligible to be displayed
- assertTrue(task.getTaskInfo()
- .appCompatTaskInfo.eligibleForUserAspectRatioButton());
-
- // When shouldApplyUserMinAspectRatioOverride is disable the button is not enabled
- doReturn(false).when(
- root.mAppCompatController.getAppCompatAspectRatioOverrides())
- .shouldEnableUserAspectRatioSettings();
- assertFalse(task.getTaskInfo()
- .appCompatTaskInfo.eligibleForUserAspectRatioButton());
- doReturn(true).when(root.mAppCompatController
- .getAppCompatAspectRatioOverrides()).shouldEnableUserAspectRatioSettings();
-
- // When in size compat mode the button is not enabled
- doReturn(true).when(root).inSizeCompatMode();
- assertFalse(task.getTaskInfo()
- .appCompatTaskInfo.eligibleForUserAspectRatioButton());
- doReturn(false).when(root).inSizeCompatMode();
-
- // When the top activity is transparent, the button is not enabled
- doReturn(false).when(root).fillsParent();
- assertFalse(task.getTaskInfo()
- .appCompatTaskInfo.eligibleForUserAspectRatioButton());
- doReturn(true).when(root).fillsParent();
- }
-
- @Test
public void testIsTopActivityTranslucent() {
DisplayContent display = mAtm.mRootWindowContainer.getDefaultDisplay();
final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
@@ -2112,17 +2067,6 @@
}
@Test
- public void getTaskInfoPropagatesCameraCompatMode() {
- final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
- final ActivityRecord activity = task.getTopMostActivity();
- activity.mAppCompatController.getAppCompatCameraOverrides().setFreeformCameraCompatMode(
- CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
-
- assertEquals(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE,
- task.getTaskInfo().appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode);
- }
-
- @Test
public void testUpdateTaskDescriptionOnReparent() {
final Task rootTask1 = createTask(mDisplayContent);
final Task rootTask2 = createTask(mDisplayContent);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index eebb487..9e9874b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -103,8 +103,8 @@
}
@Override
- public KeyEvent dispatchUnhandledKey(IBinder focusedToken, KeyEvent event, int policyFlags) {
- return null;
+ public boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
+ return false;
}
@Override
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
index dc5f6e9..fb031bd 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
@@ -842,7 +842,7 @@
return;
}
- model.setRequested(config.allowMultipleTriggers);
+ model.setRequested(config.isAllowMultipleTriggers());
// TODO: Remove this block if the lower layer supports multiple triggers.
if (model.isRequested()) {
updateRecognitionLocked(model, true);
@@ -964,7 +964,7 @@
RecognitionConfig config = modelData.getRecognitionConfig();
if (config != null) {
// Whether we should continue by starting this again.
- modelData.setRequested(config.allowMultipleTriggers);
+ modelData.setRequested(config.isAllowMultipleTriggers());
}
// TODO: Remove this block if the lower layer supports multiple triggers.
if (modelData.isRequested()) {
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 862aff9..2bb86bc 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -1439,7 +1439,7 @@
runOrAddOperation(new Operation(
// always execute:
() -> {
- if (!mRecognitionConfig.allowMultipleTriggers) {
+ if (!mRecognitionConfig.isAllowMultipleTriggers()) {
// Unregister this remoteService once op is done
synchronized (mCallbacksLock) {
mCallbacks.remove(mPuuid.getUuid());
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index ba36007..4ae06a4 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -18,6 +18,8 @@
import android.content.Context
import android.content.ContextWrapper
+import android.content.pm.PackageManager
+import android.content.res.Resources
import android.hardware.input.IInputManager
import android.hardware.input.AidlKeyGestureEvent
import android.hardware.input.IKeyGestureEventListener
@@ -25,15 +27,20 @@
import android.hardware.input.InputManager
import android.hardware.input.InputManagerGlobal
import android.hardware.input.KeyGestureEvent
-import android.hardware.input.KeyGestureEvent.KeyGestureType
import android.os.IBinder
import android.os.Process
+import android.os.SystemClock
+import android.os.SystemProperties
import android.os.test.TestLooper
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.Presubmit
+import android.platform.test.flag.junit.SetFlagsRule
import android.view.InputDevice
-import android.view.KeyCharacterMap
import android.view.KeyEvent
+import android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE
import androidx.test.core.app.ApplicationProvider
+import com.android.internal.R
import com.android.internal.annotations.Keep
import com.android.internal.util.FrameworkStatsLog
import com.android.modules.utils.testing.ExtendedMockitoRule
@@ -78,32 +85,60 @@
KeyEvent.KEYCODE_META_LEFT to (KeyEvent.META_META_LEFT_ON or KeyEvent.META_META_ON),
KeyEvent.KEYCODE_META_RIGHT to (KeyEvent.META_META_RIGHT_ON or KeyEvent.META_META_ON),
)
+ const val SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH = 0
+ const val SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY = 1
+ const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0
+ const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1
+ const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2
}
@JvmField
@Rule
val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
- .mockStatic(FrameworkStatsLog::class.java).build()!!
+ .mockStatic(FrameworkStatsLog::class.java)
+ .mockStatic(SystemProperties::class.java).build()!!
+
+ @JvmField
+ @Rule
+ val rule = SetFlagsRule()
@Mock
private lateinit var iInputManager: IInputManager
+ @Mock
+ private lateinit var resources: Resources
+
+ @Mock
+ private lateinit var packageManager: PackageManager
+
private var currentPid = 0
- private lateinit var keyGestureController: KeyGestureController
private lateinit var context: Context
private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession
private lateinit var testLooper: TestLooper
private var events = mutableListOf<KeyGestureEvent>()
- private var handleEvents = mutableListOf<KeyGestureEvent>()
@Before
fun setup() {
context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+ Mockito.`when`(context.resources).thenReturn(resources)
inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager)
setupInputDevices()
+ setupBehaviors()
testLooper = TestLooper()
currentPid = Process.myPid()
- keyGestureController = KeyGestureController(context, testLooper.looper)
+ }
+
+ private fun setupBehaviors() {
+ Mockito.`when`(
+ resources.getBoolean(
+ com.android.internal.R.bool.config_enableScreenshotChord
+ )
+ ).thenReturn(true)
+ Mockito.`when`(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH))
+ .thenReturn(true)
+ Mockito.`when`(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK))
+ .thenReturn(true)
+ Mockito.`when`(context.packageManager).thenReturn(packageManager)
}
private fun setupInputDevices() {
@@ -116,19 +151,22 @@
Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice)
}
- private fun notifyHomeGestureCompleted() {
- keyGestureController.notifyKeyGestureCompleted(DEVICE_ID, intArrayOf(KeyEvent.KEYCODE_H),
+ private fun notifyHomeGestureCompleted(keyGestureController: KeyGestureController) {
+ keyGestureController.notifyKeyGestureCompleted(
+ DEVICE_ID, intArrayOf(KeyEvent.KEYCODE_H),
KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON,
- KeyGestureEvent.KEY_GESTURE_TYPE_HOME)
+ KeyGestureEvent.KEY_GESTURE_TYPE_HOME
+ )
}
@Test
fun testKeyGestureEvent_registerUnregisterListener() {
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
val listener = KeyGestureEventListener()
// Register key gesture event listener
keyGestureController.registerKeyGestureEventListener(listener, 0)
- notifyHomeGestureCompleted()
+ notifyHomeGestureCompleted(keyGestureController)
testLooper.dispatchAll()
assertEquals(
"Listener should get callbacks on key gesture event completed",
@@ -144,7 +182,7 @@
// Unregister listener
events.clear()
keyGestureController.unregisterKeyGestureEventListener(listener, 0)
- notifyHomeGestureCompleted()
+ notifyHomeGestureCompleted(keyGestureController)
testLooper.dispatchAll()
assertEquals(
"Listener should not get callback after being unregistered",
@@ -155,20 +193,22 @@
@Test
fun testKeyGestureEvent_multipleGestureHandlers() {
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+
// Set up two callbacks.
var callbackCount1 = 0
var callbackCount2 = 0
var selfCallback = 0
val externalHandler1 = KeyGestureHandler { _, _ ->
- callbackCount1++;
+ callbackCount1++
true
}
val externalHandler2 = KeyGestureHandler { _, _ ->
- callbackCount2++;
+ callbackCount2++
true
}
val selfHandler = KeyGestureHandler { _, _ ->
- selfCallback++;
+ selfCallback++
false
}
@@ -406,6 +446,14 @@
intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
),
TestData(
+ "META + / -> Open Shortcut Helper",
+ intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_SLASH),
+ KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
+ intArrayOf(KeyEvent.KEYCODE_SLASH),
+ KeyEvent.META_META_ON,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ ),
+ TestData(
"BRIGHTNESS_UP -> Brightness Up",
intArrayOf(KeyEvent.KEYCODE_BRIGHTNESS_UP),
KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP,
@@ -528,12 +576,314 @@
KeyGestureEvent.ACTION_GESTURE_COMPLETE
)
),
+ TestData(
+ "CTRL + SPACE -> Switch Language Forward",
+ intArrayOf(KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_SPACE),
+ KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+ intArrayOf(KeyEvent.KEYCODE_SPACE),
+ KeyEvent.META_CTRL_ON,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ ),
+ TestData(
+ "CTRL + SHIFT + SPACE -> Switch Language Backward",
+ intArrayOf(
+ KeyEvent.KEYCODE_CTRL_LEFT,
+ KeyEvent.KEYCODE_SHIFT_LEFT,
+ KeyEvent.KEYCODE_SPACE
+ ),
+ KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+ intArrayOf(KeyEvent.KEYCODE_SPACE),
+ KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ ),
+ TestData(
+ "CTRL + ALT + Z -> Accessibility Shortcut",
+ intArrayOf(
+ KeyEvent.KEYCODE_CTRL_LEFT,
+ KeyEvent.KEYCODE_ALT_LEFT,
+ KeyEvent.KEYCODE_Z
+ ),
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
+ intArrayOf(KeyEvent.KEYCODE_Z),
+ KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ ),
+ TestData(
+ "SYSRQ -> Take screenshot",
+ intArrayOf(KeyEvent.KEYCODE_SYSRQ),
+ KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
+ intArrayOf(KeyEvent.KEYCODE_SYSRQ),
+ 0,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ ),
+ TestData(
+ "ESC -> Close All Dialogs",
+ intArrayOf(KeyEvent.KEYCODE_ESCAPE),
+ KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
+ intArrayOf(KeyEvent.KEYCODE_ESCAPE),
+ 0,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ ),
)
}
@Test
@Parameters(method = "keyGestureEventHandlerTestArguments")
fun testKeyGestures(test: TestData) {
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(keyGestureController, test)
+ }
+
+ @Test
+ fun testKeycodesFullyConsumed_irrespectiveOfHandlers() {
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ val testKeys = intArrayOf(
+ KeyEvent.KEYCODE_RECENT_APPS,
+ KeyEvent.KEYCODE_APP_SWITCH,
+ KeyEvent.KEYCODE_BRIGHTNESS_UP,
+ KeyEvent.KEYCODE_BRIGHTNESS_DOWN,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE,
+ KeyEvent.KEYCODE_ALL_APPS,
+ KeyEvent.KEYCODE_NOTIFICATION,
+ KeyEvent.KEYCODE_SETTINGS,
+ KeyEvent.KEYCODE_LANGUAGE_SWITCH,
+ KeyEvent.KEYCODE_SCREENSHOT,
+ KeyEvent.KEYCODE_META_LEFT,
+ KeyEvent.KEYCODE_META_RIGHT,
+ KeyEvent.KEYCODE_ASSIST,
+ KeyEvent.KEYCODE_VOICE_ASSIST,
+ KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY,
+ KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY,
+ KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY,
+ KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL,
+ )
+
+ val handler = KeyGestureHandler { _, _ -> false }
+ keyGestureController.registerKeyGestureHandler(handler, 0)
+
+ for (key in testKeys) {
+ sendKeys(keyGestureController, intArrayOf(key), assertNotSentToApps = true)
+ }
+ }
+
+ @Test
+ fun testSearchKeyGestures_defaultSearch() {
+ Mockito.`when`(resources.getInteger(R.integer.config_searchKeyBehavior))
+ .thenReturn(SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH)
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureNotProduced(
+ keyGestureController,
+ "SEARCH -> Default Search",
+ intArrayOf(KeyEvent.KEYCODE_SEARCH),
+ )
+ }
+
+ @Test
+ fun testSearchKeyGestures_searchActivity() {
+ Mockito.`when`(resources.getInteger(R.integer.config_searchKeyBehavior))
+ .thenReturn(SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY)
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(
+ keyGestureController,
+ TestData(
+ "SEARCH -> Launch Search Activity",
+ intArrayOf(KeyEvent.KEYCODE_SEARCH),
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
+ intArrayOf(KeyEvent.KEYCODE_SEARCH),
+ 0,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ )
+ )
+ }
+
+ @Test
+ fun testSettingKeyGestures_doNothing() {
+ Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior))
+ .thenReturn(SETTINGS_KEY_BEHAVIOR_NOTHING)
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureNotProduced(
+ keyGestureController,
+ "SETTINGS -> Do Nothing",
+ intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+ )
+ }
+
+ @Test
+ fun testSettingKeyGestures_settingsActivity() {
+ Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior))
+ .thenReturn(SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY)
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(
+ keyGestureController,
+ TestData(
+ "SETTINGS -> Launch Settings Activity",
+ intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS,
+ intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+ 0,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ )
+ )
+ }
+
+ @Test
+ fun testSettingKeyGestures_notificationPanel() {
+ Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior))
+ .thenReturn(SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL)
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(
+ keyGestureController,
+ TestData(
+ "SETTINGS -> Toggle Notification Panel",
+ intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+ intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+ 0,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ )
+ )
+ }
+
+ @Test
+ @EnableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+ fun testTriggerBugReport() {
+ Mockito.`when`(SystemProperties.get("ro.debuggable")).thenReturn("1")
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(
+ keyGestureController,
+ TestData(
+ "META + CTRL + DEL -> Trigger Bug Report",
+ intArrayOf(
+ KeyEvent.KEYCODE_META_LEFT,
+ KeyEvent.KEYCODE_CTRL_LEFT,
+ KeyEvent.KEYCODE_DEL
+ ),
+ KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT,
+ intArrayOf(KeyEvent.KEYCODE_DEL),
+ KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ )
+ )
+ }
+
+ @Test
+ @DisableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+ fun testTriggerBugReport_flagDisabled() {
+ Mockito.`when`(SystemProperties.get("ro.debuggable")).thenReturn("1")
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(
+ keyGestureController,
+ TestData(
+ "META + CTRL + DEL -> Not Trigger Bug Report (Fallback to BACK)",
+ intArrayOf(
+ KeyEvent.KEYCODE_META_LEFT,
+ KeyEvent.KEYCODE_CTRL_LEFT,
+ KeyEvent.KEYCODE_DEL
+ ),
+ KeyGestureEvent.KEY_GESTURE_TYPE_BACK,
+ intArrayOf(KeyEvent.KEYCODE_DEL),
+ KeyEvent.META_META_ON,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ )
+ )
+ }
+
+ @Test
+ fun testCapsLockPressNotified() {
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ val listener = KeyGestureEventListener()
+
+ keyGestureController.registerKeyGestureEventListener(listener, 0)
+ sendKeys(keyGestureController, intArrayOf(KeyEvent.KEYCODE_CAPS_LOCK))
+ testLooper.dispatchAll()
+ assertEquals(
+ "Listener should get callbacks on key gesture event completed",
+ 1,
+ events.size
+ )
+ assertEquals(
+ "Listener should get callback for Toggle Caps Lock key gesture complete event",
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
+ events[0].keyGestureType
+ )
+ }
+
+ @Keep
+ private fun keyGestureEventHandlerTestArguments_forKeyCombinations(): Array<TestData> {
+ return arrayOf(
+ TestData(
+ "VOLUME_DOWN + POWER -> Screenshot Chord",
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER),
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER),
+ 0,
+ intArrayOf(
+ KeyGestureEvent.ACTION_GESTURE_START,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE
+ )
+ ),
+ TestData(
+ "POWER + STEM_PRIMARY -> Screenshot Chord",
+ intArrayOf(KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_STEM_PRIMARY),
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ intArrayOf(KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_STEM_PRIMARY),
+ 0,
+ intArrayOf(
+ KeyGestureEvent.ACTION_GESTURE_START,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE
+ )
+ ),
+ TestData(
+ "VOLUME_DOWN + VOLUME_UP -> Accessibility Chord",
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP),
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP),
+ 0,
+ intArrayOf(
+ KeyGestureEvent.ACTION_GESTURE_START,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE
+ )
+ ),
+ TestData(
+ "BACK + DPAD_DOWN -> TV Accessibility Chord",
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+ 0,
+ intArrayOf(
+ KeyGestureEvent.ACTION_GESTURE_START,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE
+ )
+ ),
+ TestData(
+ "BACK + DPAD_CENTER -> TV Trigger Bug Report",
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER),
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER),
+ 0,
+ intArrayOf(
+ KeyGestureEvent.ACTION_GESTURE_START,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE
+ )
+ ),
+ )
+ }
+
+ @Test
+ @Parameters(method = "keyGestureEventHandlerTestArguments_forKeyCombinations")
+ @EnableFlags(
+ com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER,
+ com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_PRESS_GESTURES
+ )
+ fun testKeyCombinationGestures(test: TestData) {
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(keyGestureController, test)
+ }
+
+ private fun testKeyGestureInternal(keyGestureController: KeyGestureController, test: TestData) {
+ var handleEvents = mutableListOf<KeyGestureEvent>()
val handler = KeyGestureHandler { event, _ ->
handleEvents.add(KeyGestureEvent(event))
true
@@ -541,7 +891,7 @@
keyGestureController.registerKeyGestureHandler(handler, 0)
handleEvents.clear()
- sendKeys(test.keys, /* assertAllConsumed = */ false)
+ sendKeys(keyGestureController, test.keys)
assertEquals(
"Test: $test doesn't produce correct number of key gesture events",
@@ -575,55 +925,37 @@
keyGestureController.unregisterKeyGestureHandler(handler, 0)
}
- @Test
- fun testKeycodesFullyConsumed_irrespectiveOfHandlers() {
- val testKeys = intArrayOf(
- KeyEvent.KEYCODE_RECENT_APPS,
- KeyEvent.KEYCODE_APP_SWITCH,
- KeyEvent.KEYCODE_BRIGHTNESS_UP,
- KeyEvent.KEYCODE_BRIGHTNESS_DOWN,
- KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN,
- KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP,
- KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE,
- KeyEvent.KEYCODE_ALL_APPS,
- KeyEvent.KEYCODE_NOTIFICATION,
- KeyEvent.KEYCODE_SETTINGS,
- KeyEvent.KEYCODE_LANGUAGE_SWITCH,
- KeyEvent.KEYCODE_SCREENSHOT,
- KeyEvent.KEYCODE_META_LEFT,
- KeyEvent.KEYCODE_META_RIGHT,
- KeyEvent.KEYCODE_ASSIST,
- KeyEvent.KEYCODE_VOICE_ASSIST,
- KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY,
- KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY,
- KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY,
- KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL,
- )
-
- val handler = KeyGestureHandler { _, _ -> false }
- keyGestureController.registerKeyGestureHandler(handler, 0)
-
- for (key in testKeys) {
- sendKeys(intArrayOf(key), /* assertAllConsumed = */ true)
+ private fun testKeyGestureNotProduced(
+ keyGestureController: KeyGestureController,
+ testName: String,
+ testKeys: IntArray
+ ) {
+ var handleEvents = mutableListOf<KeyGestureEvent>()
+ val handler = KeyGestureHandler { event, _ ->
+ handleEvents.add(KeyGestureEvent(event))
+ true
}
+ keyGestureController.registerKeyGestureHandler(handler, 0)
+ handleEvents.clear()
+
+ sendKeys(keyGestureController, testKeys)
+ assertEquals("Test: $testName should not produce Key gesture", 0, handleEvents.size)
}
- private fun sendKeys(testKeys: IntArray, assertAllConsumed: Boolean) {
+ private fun sendKeys(
+ keyGestureController: KeyGestureController,
+ testKeys: IntArray,
+ assertNotSentToApps: Boolean = false
+ ) {
var metaState = 0
+ val now = SystemClock.uptimeMillis()
for (key in testKeys) {
val downEvent = KeyEvent(
- /* downTime = */0, /* eventTime = */ 0, KeyEvent.ACTION_DOWN, key,
- 0 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/,
- 0 /*flags*/, InputDevice.SOURCE_KEYBOARD
+ now, now, KeyEvent.ACTION_DOWN, key, 0 /*repeat*/, metaState,
+ DEVICE_ID, 0 /*scancode*/, 0 /*flags*/,
+ InputDevice.SOURCE_KEYBOARD
)
- val consumed =
- keyGestureController.interceptKeyBeforeDispatching(null, downEvent, 0) == -1L
- if (assertAllConsumed) {
- assertTrue(
- "interceptKeyBeforeDispatching should consume all events $downEvent",
- consumed
- )
- }
+ interceptKey(keyGestureController, downEvent, assertNotSentToApps)
metaState = metaState or MODIFIER.getOrDefault(key, 0)
downEvent.recycle()
@@ -632,24 +964,39 @@
for (key in testKeys.reversed()) {
val upEvent = KeyEvent(
- /* downTime = */0, /* eventTime = */ 0, KeyEvent.ACTION_UP, key,
- 0 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/,
- 0 /*flags*/, InputDevice.SOURCE_KEYBOARD
+ now, now, KeyEvent.ACTION_UP, key, 0 /*repeat*/, metaState,
+ DEVICE_ID, 0 /*scancode*/, 0 /*flags*/,
+ InputDevice.SOURCE_KEYBOARD
)
- val consumed =
- keyGestureController.interceptKeyBeforeDispatching(null, upEvent, 0) == -1L
- if (assertAllConsumed) {
- assertTrue(
- "interceptKeyBeforeDispatching should consume all events $upEvent",
- consumed
- )
- }
+ interceptKey(keyGestureController, upEvent, assertNotSentToApps)
+ metaState = metaState and MODIFIER.getOrDefault(key, 0).inv()
upEvent.recycle()
testLooper.dispatchAll()
}
}
+ private fun interceptKey(
+ keyGestureController: KeyGestureController,
+ event: KeyEvent,
+ assertNotSentToApps: Boolean
+ ) {
+ keyGestureController.interceptKeyBeforeQueueing(event, FLAG_INTERACTIVE)
+ testLooper.dispatchAll()
+
+ val consumed =
+ keyGestureController.interceptKeyBeforeDispatching(null, event, 0) == -1L
+ if (assertNotSentToApps) {
+ assertTrue(
+ "interceptKeyBeforeDispatching should consume all events $event",
+ consumed
+ )
+ }
+ if (!consumed) {
+ keyGestureController.interceptUnhandledKey(event, null)
+ }
+ }
+
inner class KeyGestureEventListener : IKeyGestureEventListener.Stub() {
override fun onKeyGestureEvent(event: AidlKeyGestureEvent) {
events.add(KeyGestureEvent(event))
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index b5258df..60fa52f 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -402,4 +402,73 @@
// Verify that no updateViewLayout is called (as expected for a two-finger drag gesture).
verify(mWindowManager, times(0)).updateViewLayout(any(), any());
}
-}
\ No newline at end of file
+
+ @Test
+ public void testPinchDrag() {
+ float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10;
+
+ MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_MOUSE)
+ .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(40f)
+ )
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build();
+ mTouchpadDebugView.dispatchTouchEvent(actionDown);
+
+ MotionEvent pointerDown = new MotionEventBuilder(MotionEvent.ACTION_POINTER_DOWN,
+ SOURCE_MOUSE)
+ .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(40f)
+ )
+ .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(45f)
+ )
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build();
+ mTouchpadDebugView.dispatchTouchEvent(pointerDown);
+
+ // Simulate ACTION_MOVE event (both fingers moving apart).
+ MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_MOUSE)
+ .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(40f - offsetY)
+ )
+ .rawXCursorPosition(mWindowLayoutParams.x + 10f)
+ .rawYCursorPosition(mWindowLayoutParams.y + 10f)
+ .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(45f + offsetY)
+ )
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build();
+ mTouchpadDebugView.dispatchTouchEvent(actionMove);
+
+ MotionEvent pointerUp = new MotionEventBuilder(MotionEvent.ACTION_POINTER_UP, SOURCE_MOUSE)
+ .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(40f - offsetY)
+ )
+ .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(45f + offsetY)
+ )
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build();
+ mTouchpadDebugView.dispatchTouchEvent(pointerUp);
+
+ MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_MOUSE)
+ .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(40f - offsetY)
+ )
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build();
+ mTouchpadDebugView.dispatchTouchEvent(actionUp);
+
+ // Verify that no updateViewLayout is called (as expected for a two-finger drag gesture).
+ verify(mWindowManager, times(0)).updateViewLayout(any(), any());
+ }
+}
diff --git a/tests/Tracing/TEST_MAPPING b/tests/Tracing/TEST_MAPPING
index 7f58fce..f6e5221 100644
--- a/tests/Tracing/TEST_MAPPING
+++ b/tests/Tracing/TEST_MAPPING
@@ -1,5 +1,5 @@
{
- "postsubmit": [
+ "presubmit": [
{
"name": "TracingTests"
}