Merge "Resolve requested deferral policy based on caller." into udc-dev
diff --git a/Android.bp b/Android.bp
index bc6cda3..cff863b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -343,6 +343,7 @@
"hardware/interfaces/biometrics/fingerprint/aidl",
"hardware/interfaces/graphics/common/aidl",
"hardware/interfaces/keymaster/aidl",
+ "system/hardware/interfaces/media/aidl",
],
},
dxflags: [
@@ -632,6 +633,7 @@
"hardware/interfaces/biometrics/fingerprint/aidl",
"hardware/interfaces/graphics/common/aidl",
"hardware/interfaces/keymaster/aidl",
+ "system/hardware/interfaces/media/aidl",
],
},
// These are libs from framework-internal-utils that are required (i.e. being referenced)
diff --git a/ApiDocs.bp b/ApiDocs.bp
index 90b6603..a46ecce 100644
--- a/ApiDocs.bp
+++ b/ApiDocs.bp
@@ -422,27 +422,26 @@
"$(location merge_zips) $(out) $(location :ds-docs-java{.docs.zip}) $(genDir)/ds-docs-kt-moved.zip",
}
-// Disable doc generation until Doclava is migrated to JDK 17 (b/240421555).
-// java_genrule {
-// name: "ds-docs-switched",
-// tools: [
-// "switcher4",
-// "soong_zip",
-// ],
-// srcs: [
-// ":ds-docs-java{.docs.zip}",
-// ":ds-docs-kt{.docs.zip}",
-// ],
-// out: ["ds-docs-switched.zip"],
-// dist: {
-// targets: ["docs"],
-// },
-// cmd: "unzip -q $(location :ds-docs-java{.docs.zip}) -d $(genDir) && " +
-// "unzip -q $(location :ds-docs-kt{.docs.zip}) -d $(genDir)/en/reference/kotlin && " +
-// "SWITCHER=$$(cd $$(dirname $(location switcher4)) && pwd)/$$(basename $(location switcher4)) && " +
-// "(cd $(genDir)/en/reference && $$SWITCHER --work platform) > /dev/null && " +
-// "$(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)",
-// }
+java_genrule {
+ name: "ds-docs-switched",
+ tools: [
+ "switcher4",
+ "soong_zip",
+ ],
+ srcs: [
+ ":ds-docs-java{.docs.zip}",
+ ":ds-docs-kt{.docs.zip}",
+ ],
+ out: ["ds-docs-switched.zip"],
+ dist: {
+ targets: ["docs"],
+ },
+ cmd: "unzip -q $(location :ds-docs-java{.docs.zip}) -d $(genDir) && " +
+ "unzip -q $(location :ds-docs-kt{.docs.zip}) -d $(genDir)/en/reference/kotlin && " +
+ "SWITCHER=$$(cd $$(dirname $(location switcher4)) && pwd)/$$(basename $(location switcher4)) && " +
+ "(cd $(genDir)/en/reference && $$SWITCHER --work platform) > /dev/null && " +
+ "$(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)",
+}
droiddoc {
name: "ds-static-docs",
diff --git a/core/api/current.txt b/core/api/current.txt
index 443954b..02de1cd 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -224,6 +224,7 @@
field @Deprecated public static final String PERSISTENT_ACTIVITY = "android.permission.PERSISTENT_ACTIVITY";
field public static final String POST_NOTIFICATIONS = "android.permission.POST_NOTIFICATIONS";
field @Deprecated public static final String PROCESS_OUTGOING_CALLS = "android.permission.PROCESS_OUTGOING_CALLS";
+ field public static final String PROVIDE_REMOTE_CREDENTIALS = "android.permission.PROVIDE_REMOTE_CREDENTIALS";
field public static final String QUERY_ALL_PACKAGES = "android.permission.QUERY_ALL_PACKAGES";
field public static final String READ_ASSISTANT_APP_SEARCH_DATA = "android.permission.READ_ASSISTANT_APP_SEARCH_DATA";
field public static final String READ_BASIC_PHONE_STATE = "android.permission.READ_BASIC_PHONE_STATE";
@@ -9678,6 +9679,7 @@
method @Nullable public String getAttributionTag();
method @Nullable public android.content.AttributionSource getNext();
method @Nullable public String getPackageName();
+ method public int getPid();
method public int getUid();
method public boolean isTrusted(@NonNull android.content.Context);
method @NonNull public static android.content.AttributionSource myAttributionSource();
@@ -9692,6 +9694,7 @@
method @NonNull public android.content.AttributionSource.Builder setAttributionTag(@Nullable String);
method @NonNull public android.content.AttributionSource.Builder setNext(@Nullable android.content.AttributionSource);
method @NonNull public android.content.AttributionSource.Builder setPackageName(@Nullable String);
+ method @NonNull public android.content.AttributionSource.Builder setPid(int);
}
public abstract class BroadcastReceiver {
@@ -11401,7 +11404,7 @@
public class RestrictionsManager {
method public static android.os.Bundle convertRestrictionsToBundle(java.util.List<android.content.RestrictionEntry>);
method public android.content.Intent createLocalApprovalIntent();
- method @Deprecated public android.os.Bundle getApplicationRestrictions();
+ method public android.os.Bundle getApplicationRestrictions();
method @NonNull @WorkerThread public java.util.List<android.os.Bundle> getApplicationRestrictionsPerAdmin();
method public java.util.List<android.content.RestrictionEntry> getManifestRestrictions(String);
method public boolean hasRestrictionsProvider();
@@ -27343,7 +27346,9 @@
field public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 2; // 0x2
field public static final int TIME_SHIFT_STATUS_UNKNOWN = 0; // 0x0
field public static final int TIME_SHIFT_STATUS_UNSUPPORTED = 1; // 0x1
+ field public static final String TV_MESSAGE_KEY_RAW_DATA = "android.media.tv.TvInputManager.raw_data";
field public static final String TV_MESSAGE_KEY_STREAM_ID = "android.media.tv.TvInputManager.stream_id";
+ field public static final String TV_MESSAGE_KEY_SUBTYPE = "android.media.tv.TvInputManager.subtype";
field public static final int TV_MESSAGE_TYPE_CLOSED_CAPTION = 2; // 0x2
field public static final int TV_MESSAGE_TYPE_OTHER = 1000; // 0x3e8
field public static final int TV_MESSAGE_TYPE_WATERMARK = 1; // 0x1
@@ -33610,6 +33615,7 @@
method @Deprecated public static final boolean supportsProcesses();
field public static final int BLUETOOTH_UID = 1002; // 0x3ea
field public static final int FIRST_APPLICATION_UID = 10000; // 0x2710
+ field public static final int INVALID_PID = -1; // 0xffffffff
field public static final int INVALID_UID = -1; // 0xffffffff
field public static final int LAST_APPLICATION_UID = 19999; // 0x4e1f
field public static final int PHONE_UID = 1001; // 0x3e9
@@ -33861,7 +33867,7 @@
public class UserManager {
method public static android.content.Intent createUserCreationIntent(@Nullable String, @Nullable String, @Nullable String, @Nullable android.os.PersistableBundle);
- method @Deprecated @WorkerThread public android.os.Bundle getApplicationRestrictions(String);
+ method @WorkerThread public android.os.Bundle getApplicationRestrictions(String);
method public long getSerialNumberForUser(android.os.UserHandle);
method @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.CREATE_USERS"}) public int getUserCount();
method public long getUserCreationTime(android.os.UserHandle);
@@ -40588,7 +40594,7 @@
method @NonNull public android.service.credentials.BeginCreateCredentialResponse.Builder addCreateEntry(@NonNull android.service.credentials.CreateEntry);
method @NonNull public android.service.credentials.BeginCreateCredentialResponse build();
method @NonNull public android.service.credentials.BeginCreateCredentialResponse.Builder setCreateEntries(@NonNull java.util.List<android.service.credentials.CreateEntry>);
- method @NonNull @RequiresPermission("android.permission.PROVIDE_REMOTE_CREDENTIALS") public android.service.credentials.BeginCreateCredentialResponse.Builder setRemoteCreateEntry(@Nullable android.service.credentials.RemoteEntry);
+ method @NonNull @RequiresPermission(android.Manifest.permission.PROVIDE_REMOTE_CREDENTIALS) public android.service.credentials.BeginCreateCredentialResponse.Builder setRemoteCreateEntry(@Nullable android.service.credentials.RemoteEntry);
}
public class BeginGetCredentialOption implements android.os.Parcelable {
@@ -40637,7 +40643,7 @@
method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setActions(@NonNull java.util.List<android.service.credentials.Action>);
method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setAuthenticationActions(@NonNull java.util.List<android.service.credentials.Action>);
method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setCredentialEntries(@NonNull java.util.List<android.service.credentials.CredentialEntry>);
- method @NonNull @RequiresPermission("android.permission.PROVIDE_REMOTE_CREDENTIALS") public android.service.credentials.BeginGetCredentialResponse.Builder setRemoteCredentialEntry(@Nullable android.service.credentials.RemoteEntry);
+ method @NonNull @RequiresPermission(android.Manifest.permission.PROVIDE_REMOTE_CREDENTIALS) public android.service.credentials.BeginGetCredentialResponse.Builder setRemoteCredentialEntry(@Nullable android.service.credentials.RemoteEntry);
}
public final class CallingAppInfo implements android.os.Parcelable {
@@ -41555,7 +41561,6 @@
public abstract class RecognitionService extends android.app.Service {
ctor public RecognitionService();
- method public void clearModelDownloadListener(@NonNull android.content.Intent, @NonNull android.content.AttributionSource);
method public int getMaxConcurrentSessionsCount();
method public final android.os.IBinder onBind(android.content.Intent);
method protected abstract void onCancel(android.speech.RecognitionService.Callback);
@@ -41565,7 +41570,7 @@
method protected abstract void onStopListening(android.speech.RecognitionService.Callback);
method public void onTriggerModelDownload(@NonNull android.content.Intent);
method public void onTriggerModelDownload(@NonNull android.content.Intent, @NonNull android.content.AttributionSource);
- method public void setModelDownloadListener(@NonNull android.content.Intent, @NonNull android.content.AttributionSource, @NonNull android.speech.ModelDownloadListener);
+ method public void onTriggerModelDownload(@NonNull android.content.Intent, @NonNull android.content.AttributionSource, @NonNull android.speech.ModelDownloadListener);
field public static final String SERVICE_INTERFACE = "android.speech.RecognitionService";
field public static final String SERVICE_META_DATA = "android.speech";
}
@@ -41690,18 +41695,17 @@
public class SpeechRecognizer {
method @MainThread public void cancel();
method public void checkRecognitionSupport(@NonNull android.content.Intent, @NonNull java.util.concurrent.Executor, @NonNull android.speech.RecognitionSupportCallback);
- method public void clearModelDownloadListener(@NonNull android.content.Intent);
method @MainThread @NonNull public static android.speech.SpeechRecognizer createOnDeviceSpeechRecognizer(@NonNull android.content.Context);
method @MainThread public static android.speech.SpeechRecognizer createSpeechRecognizer(android.content.Context);
method @MainThread public static android.speech.SpeechRecognizer createSpeechRecognizer(android.content.Context, android.content.ComponentName);
method public void destroy();
method public static boolean isOnDeviceRecognitionAvailable(@NonNull android.content.Context);
method public static boolean isRecognitionAvailable(@NonNull android.content.Context);
- method public void setModelDownloadListener(@NonNull android.content.Intent, @NonNull java.util.concurrent.Executor, @NonNull android.speech.ModelDownloadListener);
method @MainThread public void setRecognitionListener(android.speech.RecognitionListener);
method @MainThread public void startListening(android.content.Intent);
method @MainThread public void stopListening();
method public void triggerModelDownload(@NonNull android.content.Intent);
+ method public void triggerModelDownload(@NonNull android.content.Intent, @NonNull java.util.concurrent.Executor, @NonNull android.speech.ModelDownloadListener);
field public static final String CONFIDENCE_SCORES = "confidence_scores";
field public static final String DETECTED_LANGUAGE = "detected_language";
field public static final int ERROR_AUDIO = 3; // 0x3
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index d573e33..fb7ce5b 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -255,7 +255,6 @@
field public static final String PERFORM_SIM_ACTIVATION = "android.permission.PERFORM_SIM_ACTIVATION";
field public static final String POWER_SAVER = "android.permission.POWER_SAVER";
field public static final String PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE = "android.permission.PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE";
- field public static final String PROVIDE_REMOTE_CREDENTIALS = "android.permission.PROVIDE_REMOTE_CREDENTIALS";
field public static final String PROVIDE_RESOLVER_RANKER_SERVICE = "android.permission.PROVIDE_RESOLVER_RANKER_SERVICE";
field public static final String PROVIDE_TRUST_AGENT = "android.permission.PROVIDE_TRUST_AGENT";
field public static final String PROVISION_DEMO_DEVICE = "android.permission.PROVISION_DEMO_DEVICE";
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 574ed6f..445b957 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2027,7 +2027,9 @@
method public static boolean is64BitAbi(String);
method public static boolean isDebuggable();
field @Nullable public static final String BRAND_FOR_ATTESTATION;
+ field @Nullable public static final String DEVICE_FOR_ATTESTATION;
field public static final boolean IS_EMULATOR;
+ field @Nullable public static final String MANUFACTURER_FOR_ATTESTATION;
field @Nullable public static final String MODEL_FOR_ATTESTATION;
field @Nullable public static final String PRODUCT_FOR_ATTESTATION;
}
@@ -3986,6 +3988,7 @@
public static class WindowInfosListenerForTest.WindowInfo {
field @NonNull public final android.graphics.Rect bounds;
+ field public final boolean isTrustedOverlay;
field @NonNull public final String name;
field @NonNull public final android.os.IBinder windowToken;
}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index a3ada76..2751b54 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -849,8 +849,10 @@
@UnsupportedAppUsage
Intent intent;
boolean rebind;
+ long bindSeq;
public String toString() {
- return "BindServiceData{token=" + token + " intent=" + intent + "}";
+ return "BindServiceData{token=" + token + " intent=" + intent
+ + " bindSeq=" + bindSeq + "}";
}
}
@@ -1107,12 +1109,13 @@
}
public final void scheduleBindService(IBinder token, Intent intent,
- boolean rebind, int processState) {
+ boolean rebind, int processState, long bindSeq) {
updateProcessState(processState, false);
BindServiceData s = new BindServiceData();
s.token = token;
s.intent = intent;
s.rebind = rebind;
+ s.bindSeq = bindSeq;
if (DEBUG_SERVICE)
Slog.v(TAG, "scheduleBindService token=" + token + " intent=" + intent + " uid="
@@ -1124,6 +1127,7 @@
BindServiceData s = new BindServiceData();
s.token = token;
s.intent = intent;
+ s.bindSeq = -1;
sendMessage(H.UNBIND_SERVICE, s);
}
@@ -3557,8 +3561,13 @@
if (mLastProcessState == processState) {
return;
}
+ // Do not issue a transitional GC if we are transitioning between 2 cached states.
+ // Only update if the state flips between cached and uncached or vice versa
+ if (ActivityManager.isProcStateCached(mLastProcessState)
+ != ActivityManager.isProcStateCached(processState)) {
+ updateVmProcessState(processState);
+ }
mLastProcessState = processState;
- updateVmProcessState(processState);
if (localLOGV) {
Slog.i(TAG, "******************* PROCESS STATE CHANGED TO: " + processState
+ (fromIpc ? " (from ipc" : ""));
@@ -3567,12 +3576,16 @@
}
/** Update VM state based on ActivityManager.PROCESS_STATE_* constants. */
+ // Currently ART VM only uses state updates for Transitional GC, and thus
+ // this function initiates a Transitional GC for transitions into Cached apps states.
private void updateVmProcessState(int processState) {
- // TODO: Tune this since things like gmail sync are important background but not jank
- // perceptible.
- final int state = processState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
- ? VM_PROCESS_STATE_JANK_PERCEPTIBLE
- : VM_PROCESS_STATE_JANK_IMPERCEPTIBLE;
+ // Only a transition into Cached state should result in a Transitional GC request
+ // to the ART runtime. Update VM state to JANK_IMPERCEPTIBLE in that case.
+ // Note that there are 4 possible cached states currently, all of which are
+ // JANK_IMPERCEPTIBLE from GC point of view.
+ final int state = ActivityManager.isProcStateCached(processState)
+ ? VM_PROCESS_STATE_JANK_IMPERCEPTIBLE
+ : VM_PROCESS_STATE_JANK_PERCEPTIBLE;
VMRuntime.getRuntime().updateProcessState(state);
}
@@ -6644,12 +6657,13 @@
// Setup a location to store generated/compiled graphics code.
final Context deviceContext = context.createDeviceProtectedStorageContext();
final File codeCacheDir = deviceContext.getCodeCacheDir();
- if (codeCacheDir != null) {
+ final File deviceCacheDir = deviceContext.getCacheDir();
+ if (codeCacheDir != null && deviceCacheDir != null) {
try {
int uid = Process.myUid();
String[] packages = getPackageManager().getPackagesForUid(uid);
if (packages != null) {
- HardwareRenderer.setupDiskCache(codeCacheDir);
+ HardwareRenderer.setupDiskCache(deviceCacheDir);
RenderScriptCacheDir.setupDiskCache(codeCacheDir);
}
} catch (RemoteException e) {
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 181bd35..265b564 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -8408,9 +8408,9 @@
public int noteProxyOp(int op, @Nullable String proxiedPackageName, int proxiedUid,
@Nullable String proxiedAttributionTag, @Nullable String message) {
return noteProxyOp(op, new AttributionSource(mContext.getAttributionSource(),
- new AttributionSource(proxiedUid, proxiedPackageName, proxiedAttributionTag,
- mContext.getAttributionSource().getToken())), message,
- /*skipProxyOperation*/ false);
+ new AttributionSource(proxiedUid, Process.INVALID_PID, proxiedPackageName,
+ proxiedAttributionTag, mContext.getAttributionSource().getToken())),
+ message, /*skipProxyOperation*/ false);
}
/**
@@ -8495,8 +8495,9 @@
int proxiedUid, @Nullable String proxiedAttributionTag, @Nullable String message) {
return noteProxyOpNoThrow(strOpToOp(op), new AttributionSource(
mContext.getAttributionSource(), new AttributionSource(proxiedUid,
- proxiedPackageName, proxiedAttributionTag, mContext.getAttributionSource()
- .getToken())), message,/*skipProxyOperation*/ false);
+ Process.INVALID_PID, proxiedPackageName, proxiedAttributionTag,
+ mContext.getAttributionSource().getToken())), message,
+ /*skipProxyOperation*/ false);
}
/**
@@ -8906,9 +8907,9 @@
public int startProxyOp(@NonNull String op, int proxiedUid, @NonNull String proxiedPackageName,
@Nullable String proxiedAttributionTag, @Nullable String message) {
return startProxyOp(op, new AttributionSource(mContext.getAttributionSource(),
- new AttributionSource(proxiedUid, proxiedPackageName, proxiedAttributionTag,
- mContext.getAttributionSource().getToken())), message,
- /*skipProxyOperation*/ false);
+ new AttributionSource(proxiedUid, Process.INVALID_PID, proxiedPackageName,
+ proxiedAttributionTag, mContext.getAttributionSource().getToken())),
+ message, /*skipProxyOperation*/ false);
}
/**
@@ -8954,7 +8955,7 @@
@Nullable String message) {
return startProxyOpNoThrow(AppOpsManager.strOpToOp(op), new AttributionSource(
mContext.getAttributionSource(), new AttributionSource(proxiedUid,
- proxiedPackageName, proxiedAttributionTag,
+ Process.INVALID_PID, proxiedPackageName, proxiedAttributionTag,
mContext.getAttributionSource().getToken())), message,
/*skipProxyOperation*/ false);
}
@@ -9103,8 +9104,8 @@
@NonNull String proxiedPackageName, @Nullable String proxiedAttributionTag) {
IBinder token = mContext.getAttributionSource().getToken();
finishProxyOp(token, op, new AttributionSource(mContext.getAttributionSource(),
- new AttributionSource(proxiedUid, proxiedPackageName, proxiedAttributionTag,
- token)), /*skipProxyOperation*/ false);
+ new AttributionSource(proxiedUid, Process.INVALID_PID, proxiedPackageName,
+ proxiedAttributionTag, token)), /*skipProxyOperation*/ false);
}
/**
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 762fb04..0cd42a3 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -3461,7 +3461,9 @@
@Nullable AttributionSource nextAttributionSource,
@Nullable Set<String> renouncedPermissions) {
AttributionSource attributionSource = new AttributionSource(Process.myUid(),
- mOpPackageName, attributionTag, renouncedPermissions, nextAttributionSource);
+ Process.myPid(), mOpPackageName, attributionTag,
+ (renouncedPermissions != null) ? renouncedPermissions.toArray(new String[0]) : null,
+ nextAttributionSource);
// If we want to access protected data on behalf of another app we need to
// tell the OS that we opt in to participate in the attribution chain.
if (nextAttributionSource != null) {
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index 4f77203..6b5f6b0 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -95,7 +95,7 @@
void processInBackground();
@UnsupportedAppUsage
void scheduleBindService(IBinder token,
- in Intent intent, boolean rebind, int processState);
+ in Intent intent, boolean rebind, int processState, long bindSeq);
@UnsupportedAppUsage
void scheduleUnbindService(IBinder token,
in Intent intent);
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
index 0ef8e92..09450f5 100644
--- a/core/java/android/app/KeyguardManager.java
+++ b/core/java/android/app/KeyguardManager.java
@@ -155,6 +155,14 @@
"android.app.extra.REMOTE_LOCKSCREEN_VALIDATION_SESSION";
/**
+ * A boolean indicating that credential confirmation activity should be a task overlay.
+ * {@link #ACTION_CONFIRM_DEVICE_CREDENTIAL_WITH_USER}.
+ * @hide
+ */
+ public static final String EXTRA_FORCE_TASK_OVERLAY =
+ "android.app.KeyguardManager.FORCE_TASK_OVERLAY";
+
+ /**
* Result code returned by the activity started by
* {@link #createConfirmFactoryResetCredentialIntent} or
* {@link #createConfirmDeviceCredentialForRemoteValidationIntent}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 440ee20..e78fb17 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -8657,13 +8657,13 @@
* where the platform doesn't support the MIME type, the original text provided in the
* constructor will be used.
* @param dataMimeType The MIME type of the content. See
- * <a href="{@docRoot}notifications/messaging.html"> for the list of supported MIME
- * types on Android and Android Wear.
+ * {@link android.graphics.ImageDecoder#isMimeTypeSupported(String)} for a list of
+ * supported image MIME types.
* @param dataUri The uri containing the content whose type is given by the MIME type.
* <p class="note">
+ * Notification Listeners including the System UI need permission to access the
+ * data the Uri points to. The recommended ways to do this are:
* <ol>
- * <li>Notification Listeners including the System UI need permission to access the
- * data the Uri points to. The recommended ways to do this are:</li>
* <li>Store the data in your own ContentProvider, making sure that other apps have
* the correct permission to access your provider. The preferred mechanism for
* providing access is to use per-URI permissions which are temporary and only
diff --git a/core/java/android/app/WindowConfiguration.java b/core/java/android/app/WindowConfiguration.java
index 39d77c4..5b95503 100644
--- a/core/java/android/app/WindowConfiguration.java
+++ b/core/java/android/app/WindowConfiguration.java
@@ -875,15 +875,6 @@
}
/**
- * Returns true if any visible windows belonging to apps with this window configuration should
- * be kept on screen when the app is killed due to something like the low memory killer.
- * @hide
- */
- public boolean keepVisibleDeadAppWindowOnScreen() {
- return mWindowingMode != WINDOWING_MODE_PINNED;
- }
-
- /**
* Returns true if the backdrop on the client side should match the frame of the window.
* Returns false, if the backdrop should be fullscreen.
* @hide
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index 2b400c1f..cd45f4d 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -101,22 +101,28 @@
@TestApi
public AttributionSource(int uid, @Nullable String packageName,
@Nullable String attributionTag) {
- this(uid, packageName, attributionTag, sDefaultToken);
+ this(uid, Process.INVALID_PID, packageName, attributionTag, sDefaultToken);
+ }
+
+ /** @hide */
+ public AttributionSource(int uid, int pid, @Nullable String packageName,
+ @Nullable String attributionTag) {
+ this(uid, pid, packageName, attributionTag, sDefaultToken);
}
/** @hide */
@TestApi
public AttributionSource(int uid, @Nullable String packageName,
@Nullable String attributionTag, @NonNull IBinder token) {
- this(uid, packageName, attributionTag, token, /*renouncedPermissions*/ null,
- /*next*/ null);
+ this(uid, Process.INVALID_PID, packageName, attributionTag, token,
+ /*renouncedPermissions*/ null, /*next*/ null);
}
/** @hide */
- public AttributionSource(int uid, @Nullable String packageName,
- @Nullable String attributionTag, @NonNull IBinder token,
- @Nullable AttributionSource next) {
- this(uid, packageName, attributionTag, token, /*renouncedPermissions*/ null, next);
+ public AttributionSource(int uid, int pid, @Nullable String packageName,
+ @Nullable String attributionTag, @NonNull IBinder token) {
+ this(uid, pid, packageName, attributionTag, token, /*renouncedPermissions*/ null,
+ /*next*/ null);
}
/** @hide */
@@ -124,26 +130,33 @@
public AttributionSource(int uid, @Nullable String packageName,
@Nullable String attributionTag, @Nullable Set<String> renouncedPermissions,
@Nullable AttributionSource next) {
- this(uid, packageName, attributionTag, (renouncedPermissions != null)
- ? renouncedPermissions.toArray(new String[0]) : null, next);
+ this(uid, Process.INVALID_PID, packageName, attributionTag, sDefaultToken,
+ (renouncedPermissions != null)
+ ? renouncedPermissions.toArray(new String[0]) : null, /*next*/ next);
}
/** @hide */
public AttributionSource(@NonNull AttributionSource current, @Nullable AttributionSource next) {
- this(current.getUid(), current.getPackageName(), current.getAttributionTag(),
- current.getToken(), current.mAttributionSourceState.renouncedPermissions, next);
+ this(current.getUid(), current.getPid(), current.getPackageName(),
+ current.getAttributionTag(), current.getToken(),
+ current.mAttributionSourceState.renouncedPermissions, next);
}
- AttributionSource(int uid, @Nullable String packageName, @Nullable String attributionTag,
- @Nullable String[] renouncedPermissions, @Nullable AttributionSource next) {
- this(uid, packageName, attributionTag, sDefaultToken, renouncedPermissions, next);
+ /** @hide */
+ public AttributionSource(int uid, int pid, @Nullable String packageName,
+ @Nullable String attributionTag, @Nullable String[] renouncedPermissions,
+ @Nullable AttributionSource next) {
+ this(uid, pid, packageName, attributionTag, sDefaultToken, renouncedPermissions, next);
}
- AttributionSource(int uid, @Nullable String packageName, @Nullable String attributionTag,
- @NonNull IBinder token, @Nullable String[] renouncedPermissions,
+ /** @hide */
+ public AttributionSource(int uid, int pid, @Nullable String packageName,
+ @Nullable String attributionTag, @NonNull IBinder token,
+ @Nullable String[] renouncedPermissions,
@Nullable AttributionSource next) {
mAttributionSourceState = new AttributionSourceState();
mAttributionSourceState.uid = uid;
+ mAttributionSourceState.pid = pid;
mAttributionSourceState.token = token;
mAttributionSourceState.packageName = packageName;
mAttributionSourceState.attributionTag = attributionTag;
@@ -162,7 +175,17 @@
// Since we just unpacked this object as part of it transiting a Binder
// call, this is the perfect time to enforce that its UID and PID can be trusted
- enforceCallingUidAndPid();
+ enforceCallingUid();
+
+ // If this object is being constructed as part of a oneway Binder call, getCallingPid will
+ // return 0 instead of the true PID. In that case, invalidate the PID by setting it to
+ // INVALID_PID (-1).
+ final int callingPid = Binder.getCallingPid();
+ if (callingPid == 0) {
+ mAttributionSourceState.pid = Process.INVALID_PID;
+ }
+
+ enforceCallingPid();
}
/** @hide */
@@ -172,23 +195,29 @@
/** @hide */
public AttributionSource withNextAttributionSource(@Nullable AttributionSource next) {
- return new AttributionSource(getUid(), getPackageName(), getAttributionTag(),
- mAttributionSourceState.renouncedPermissions, next);
+ return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(),
+ getToken(), mAttributionSourceState.renouncedPermissions, next);
}
/** @hide */
public AttributionSource withPackageName(@Nullable String packageName) {
- return new AttributionSource(getUid(), packageName, getAttributionTag(),
- mAttributionSourceState.renouncedPermissions, getNext());
+ return new AttributionSource(getUid(), getPid(), packageName, getAttributionTag(),
+ getToken(), mAttributionSourceState.renouncedPermissions, getNext());
}
/** @hide */
public AttributionSource withToken(@NonNull Binder token) {
- return new AttributionSource(getUid(), getPackageName(), getAttributionTag(),
+ return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(),
token, mAttributionSourceState.renouncedPermissions, getNext());
}
/** @hide */
+ public AttributionSource withPid(int pid) {
+ return new AttributionSource(getUid(), pid, getPackageName(), getAttributionTag(),
+ getToken(), mAttributionSourceState.renouncedPermissions, getNext());
+ }
+
+ /** @hide */
public @NonNull AttributionSourceState asState() {
return mAttributionSourceState;
}
@@ -228,6 +257,7 @@
}
try {
return new AttributionSource.Builder(uid)
+ .setPid(Process.myPid())
.setPackageName(AppGlobals.getPackageManager().getPackagesForUid(uid)[0])
.build();
} catch (Exception ignored) {
@@ -265,18 +295,6 @@
}
/**
- * If you are handling an IPC and you don't trust the caller you need to validate whether the
- * attribution source is one for the calling app to prevent the caller to pass you a source from
- * another app without including themselves in the attribution chain.
- *
- * @throws SecurityException if the attribution source cannot be trusted to be from the caller.
- */
- private void enforceCallingUidAndPid() {
- enforceCallingUid();
- enforceCallingPid();
- }
-
- /**
* If you are handling an IPC and you don't trust the caller you need to validate
* whether the attribution source is one for the calling app to prevent the caller
* to pass you a source from another app without including themselves in the
@@ -312,7 +330,10 @@
}
/**
- * Validate that the pid being claimed for the calling app is not spoofed
+ * Validate that the pid being claimed for the calling app is not spoofed.
+ *
+ * Note that the PID may be unavailable, for example if we're in a oneway Binder call. In this
+ * case, calling enforceCallingPid is guaranteed to fail. The caller should anticipate this.
*
* @throws SecurityException if the attribution source cannot be trusted to be from the caller.
* @hide
@@ -320,8 +341,12 @@
@TestApi
public void enforceCallingPid() {
if (!checkCallingPid()) {
- throw new SecurityException("Calling pid: " + Binder.getCallingPid()
- + " doesn't match source pid: " + mAttributionSourceState.pid);
+ if (Binder.getCallingPid() == 0) {
+ throw new SecurityException("Calling pid unavailable due to oneway Binder call.");
+ } else {
+ throw new SecurityException("Calling pid: " + Binder.getCallingPid()
+ + " doesn't match source pid: " + mAttributionSourceState.pid);
+ }
}
}
@@ -332,7 +357,8 @@
*/
private boolean checkCallingPid() {
final int callingPid = Binder.getCallingPid();
- if (mAttributionSourceState.pid != -1 && callingPid != mAttributionSourceState.pid) {
+ if (mAttributionSourceState.pid != Process.INVALID_PID
+ && callingPid != mAttributionSourceState.pid) {
return false;
}
return true;
@@ -449,6 +475,13 @@
}
/**
+ * The PID that is accessing the permission protected data.
+ */
+ public int getPid() {
+ return mAttributionSourceState.pid;
+ }
+
+ /**
* The package that is accessing the permission protected data.
*/
public @Nullable String getPackageName() {
@@ -551,6 +584,7 @@
throw new IllegalArgumentException("current AttributionSource can not be null");
}
mAttributionSourceState.uid = current.getUid();
+ mAttributionSourceState.pid = current.getPid();
mAttributionSourceState.packageName = current.getPackageName();
mAttributionSourceState.attributionTag = current.getAttributionTag();
mAttributionSourceState.token = current.getToken();
@@ -559,11 +593,25 @@
}
/**
+ * The PID of the process that is accessing the permission protected data.
+ *
+ * If not called, pid will default to {@link Process@INVALID_PID} (-1). This indicates that
+ * the PID data is missing. Supplying a PID is not required, but recommended when
+ * accessible.
+ */
+ public @NonNull Builder setPid(int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2;
+ mAttributionSourceState.pid = value;
+ return this;
+ }
+
+ /**
* The package that is accessing the permission protected data.
*/
public @NonNull Builder setPackageName(@Nullable String value) {
checkNotUsed();
- mBuilderFieldsSet |= 0x2;
+ mBuilderFieldsSet |= 0x4;
mAttributionSourceState.packageName = value;
return this;
}
@@ -573,7 +621,7 @@
*/
public @NonNull Builder setAttributionTag(@Nullable String value) {
checkNotUsed();
- mBuilderFieldsSet |= 0x4;
+ mBuilderFieldsSet |= 0x8;
mAttributionSourceState.attributionTag = value;
return this;
}
@@ -606,7 +654,7 @@
@RequiresPermission(android.Manifest.permission.RENOUNCE_PERMISSIONS)
public @NonNull Builder setRenouncedPermissions(@Nullable Set<String> value) {
checkNotUsed();
- mBuilderFieldsSet |= 0x8;
+ mBuilderFieldsSet |= 0x10;
mAttributionSourceState.renouncedPermissions = (value != null)
? value.toArray(new String[0]) : null;
return this;
@@ -617,7 +665,7 @@
*/
public @NonNull Builder setNext(@Nullable AttributionSource value) {
checkNotUsed();
- mBuilderFieldsSet |= 0x10;
+ mBuilderFieldsSet |= 0x20;
mAttributionSourceState.next = (value != null) ? new AttributionSourceState[]
{value.mAttributionSourceState} : mAttributionSourceState.next;
return this;
@@ -629,15 +677,18 @@
mBuilderFieldsSet |= 0x40; // Mark builder used
if ((mBuilderFieldsSet & 0x2) == 0) {
- mAttributionSourceState.packageName = null;
+ mAttributionSourceState.pid = Process.INVALID_PID;
}
if ((mBuilderFieldsSet & 0x4) == 0) {
- mAttributionSourceState.attributionTag = null;
+ mAttributionSourceState.packageName = null;
}
if ((mBuilderFieldsSet & 0x8) == 0) {
- mAttributionSourceState.renouncedPermissions = null;
+ mAttributionSourceState.attributionTag = null;
}
if ((mBuilderFieldsSet & 0x10) == 0) {
+ mAttributionSourceState.renouncedPermissions = null;
+ }
+ if ((mBuilderFieldsSet & 0x20) == 0) {
mAttributionSourceState.next = null;
}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 85daf15..667ec7e 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5920,9 +5920,8 @@
/**
* Optional argument to be used with {@link #ACTION_CHOOSER}.
- * A {@link android.app.PendingIntent} to be sent when the user wants to modify the content that
- * they're sharing. This can be used to allow the user to return to the source app to, for
- * example, select different media.
+ * A {@link ChooserAction} to allow the user to modify what is being shared in some way. This
+ * may be integrated into the content preview on sharesheets that have a preview UI.
*/
public static final String EXTRA_CHOOSER_MODIFY_SHARE_ACTION =
"android.intent.extra.CHOOSER_MODIFY_SHARE_ACTION";
diff --git a/core/java/android/content/PermissionChecker.java b/core/java/android/content/PermissionChecker.java
index 8d3452e..0e3217d 100644
--- a/core/java/android/content/PermissionChecker.java
+++ b/core/java/android/content/PermissionChecker.java
@@ -152,7 +152,7 @@
@NonNull String permission, int pid, int uid, @Nullable String packageName,
@Nullable String attributionTag, @Nullable String message, boolean startDataDelivery) {
return checkPermissionForDataDelivery(context, permission, pid, new AttributionSource(uid,
- packageName, attributionTag), message, startDataDelivery);
+ pid, packageName, attributionTag), message, startDataDelivery);
}
/**
diff --git a/core/java/android/content/RestrictionsManager.java b/core/java/android/content/RestrictionsManager.java
index 8115292..44a84e4 100644
--- a/core/java/android/content/RestrictionsManager.java
+++ b/core/java/android/content/RestrictionsManager.java
@@ -427,11 +427,12 @@
* @return the application restrictions as a Bundle. Returns null if there
* are no restrictions.
*
- * @deprecated Use {@link #getApplicationRestrictionsPerAdmin} instead.
- * Starting from Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, it is
- * possible for there to be multiple managing agents on the device with the ability to set
- * restrictions. This API will only to return the restrictions set by device policy controllers
- * (DPCs)
+ * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * it is possible for there to be multiple managing apps on the device with the ability to set
+ * restrictions, e.g. a Device Policy Controller (DPC) and a Supervision admin.
+ * This API will only return the restrictions set by the DPCs. To retrieve restrictions
+ * set by all managing apps, use
+ * {@link android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin} instead.
*
* @see DevicePolicyManager
*/
@@ -453,8 +454,8 @@
* stable between multiple calls.
*
* <p>Starting from Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
- * it is possible for there to be multiple managing agents on the device with the ability to set
- * restrictions, e.g. an Enterprise DPC and a Supervision admin.
+ * it is possible for there to be multiple managing apps on the device with the ability to set
+ * restrictions, e.g. an Enterprise Device Policy Controller (DPC) and a Supervision admin.
*
* <p>Each {@link Bundle} consists of key-value pairs, as defined by the application,
* where the types of values may be:
@@ -471,6 +472,7 @@
* package. Returns an empty {@link List} if there are no saved restrictions.
*
* @see UserManager#KEY_RESTRICTIONS_PENDING
+ * @see DevicePolicyManager
*/
@WorkerThread
@UserHandleAware
diff --git a/core/java/android/credentials/CredentialDescription.java b/core/java/android/credentials/CredentialDescription.java
index bf34c1c..a23d7e4 100644
--- a/core/java/android/credentials/CredentialDescription.java
+++ b/core/java/android/credentials/CredentialDescription.java
@@ -42,7 +42,7 @@
private final String mType;
/**
- * The flattened JSON string that will be matched with requests.
+ * Flattened semicolon separated keys of JSON values to match with requests.
*/
@NonNull
private final String mFlattenedRequestString;
@@ -57,7 +57,8 @@
* Constructs a {@link CredentialDescription}.
*
* @param type the type of the credential returned.
- * @param flattenedRequestString flattened JSON string that will be matched with requests.
+ * @param flattenedRequestString flattened semicolon separated keys of JSON values
+ * to match with requests.
* @param credentialEntries a list of {@link CredentialEntry}s that are to be shown on the
* account selector if a credential matches with this description.
* Each entry contains information to be displayed within an
@@ -151,4 +152,29 @@
public List<CredentialEntry> getCredentialEntries() {
return mCredentialEntries;
}
+
+ /**
+ * {@link CredentialDescription#mType} and
+ * {@link CredentialDescription#mFlattenedRequestString} are enough for hashing. Constructor
+ * enforces {@link CredentialEntry} to have the same type and
+ * {@link android.app.slice.Slice} contained by the entry can not be hashed.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mType, mFlattenedRequestString);
+ }
+
+ /**
+ * {@link CredentialDescription#mType} and
+ * {@link CredentialDescription#mFlattenedRequestString} are enough for equality check.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof CredentialDescription)) {
+ return false;
+ }
+ CredentialDescription other = (CredentialDescription) obj;
+ return mType.equals(other.mType)
+ && mFlattenedRequestString.equals(other.mFlattenedRequestString);
+ }
}
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java
index 0806f1d..493a4ff 100644
--- a/core/java/android/credentials/CredentialManager.java
+++ b/core/java/android/credentials/CredentialManager.java
@@ -167,6 +167,48 @@
}
/**
+ * Gets a {@link GetPendingCredentialResponse} that can launch the credential retrieval UI flow
+ * to request a user credential for your app.
+ *
+ * @param request the request specifying type(s) of credentials to get from the user
+ * @param cancellationSignal an optional signal that allows for cancelling this call
+ * @param executor the callback will take place on this {@link Executor}
+ * @param callback the callback invoked when the request succeeds or fails
+ *
+ * @hide
+ */
+ public void getPendingCredential(
+ @NonNull GetCredentialRequest request,
+ @Nullable CancellationSignal cancellationSignal,
+ @CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<
+ GetPendingCredentialResponse, GetCredentialException> callback) {
+ requireNonNull(request, "request must not be null");
+ requireNonNull(executor, "executor must not be null");
+ requireNonNull(callback, "callback must not be null");
+
+ if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+ Log.w(TAG, "getPendingCredential already canceled");
+ return;
+ }
+
+ ICancellationSignal cancelRemote = null;
+ try {
+ cancelRemote =
+ mService.executeGetPendingCredential(
+ request,
+ new GetPendingCredentialTransport(executor, callback),
+ mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+
+ if (cancellationSignal != null && cancelRemote != null) {
+ cancellationSignal.setRemote(cancelRemote);
+ }
+ }
+
+ /**
* Launches the necessary flows to register an app credential for the user.
*
* <p>The execution can potentially launch UI flows to collect user consent to creating or
@@ -442,6 +484,32 @@
}
}
+ private static class GetPendingCredentialTransport extends IGetPendingCredentialCallback.Stub {
+ // TODO: listen for cancellation to release callback.
+
+ private final Executor mExecutor;
+ private final OutcomeReceiver<
+ GetPendingCredentialResponse, GetCredentialException> mCallback;
+
+ private GetPendingCredentialTransport(
+ Executor executor,
+ OutcomeReceiver<GetPendingCredentialResponse, GetCredentialException> callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onResponse(GetPendingCredentialResponse response) {
+ mExecutor.execute(() -> mCallback.onResult(response));
+ }
+
+ @Override
+ public void onError(String errorType, String message) {
+ mExecutor.execute(
+ () -> mCallback.onError(new GetCredentialException(errorType, message)));
+ }
+ }
+
private static class GetCredentialTransport extends IGetCredentialCallback.Stub {
// TODO: listen for cancellation to release callback.
diff --git a/core/java/android/credentials/GetPendingCredentialResponse.aidl b/core/java/android/credentials/GetPendingCredentialResponse.aidl
new file mode 100644
index 0000000..1cdd637
--- /dev/null
+++ b/core/java/android/credentials/GetPendingCredentialResponse.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2022 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.credentials;
+
+parcelable GetPendingCredentialResponse;
\ No newline at end of file
diff --git a/core/java/android/credentials/GetPendingCredentialResponse.java b/core/java/android/credentials/GetPendingCredentialResponse.java
new file mode 100644
index 0000000..9005d9d
--- /dev/null
+++ b/core/java/android/credentials/GetPendingCredentialResponse.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2022 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.credentials;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.os.CancellationSignal;
+import android.os.OutcomeReceiver;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.concurrent.Executor;
+
+
+/**
+ * A response object that prefetches user app credentials and provides metadata about them. It can
+ * then be used to issue the full credential retrieval flow via the
+ * {@link #show(Activity, CancellationSignal, Executor, OutcomeReceiver)} method to perform the
+ * necessary flows such as consent collection and officially retrieve a credential.
+ *
+ * @hide
+ */
+public final class GetPendingCredentialResponse implements Parcelable {
+ private final boolean mHasCredentialResults;
+ private final boolean mHasAuthenticationResults;
+ private final boolean mHasRemoteResults;
+
+ /** Returns true if the user has any candidate credentials, and false otherwise. */
+ public boolean hasCredentialResults() {
+ return mHasCredentialResults;
+ }
+
+ /**
+ * Returns true if the user has any candidate authentication actions (locked credential
+ * supplier), and false otherwise.
+ */
+ public boolean hasAuthenticationResults() {
+ return mHasAuthenticationResults;
+ }
+
+ /**
+ * Returns true if the user has any candidate remote credential results, and false otherwise.
+ */
+ public boolean hasRemoteResults() {
+ return mHasRemoteResults;
+ }
+
+ /**
+ * Launches the necessary flows such as consent collection and credential selection to
+ * officially retrieve a credential among the pending credential candidates.
+ *
+ * @param activity the activity used to launch any UI needed
+ * @param cancellationSignal an optional signal that allows for cancelling this call
+ * @param executor the callback will take place on this {@link Executor}
+ * @param callback the callback invoked when the request succeeds or fails
+ */
+ public void show(@NonNull Activity activity, @Nullable CancellationSignal cancellationSignal,
+ @CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
+ // TODO(b/273308895): implement
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mHasCredentialResults);
+ dest.writeBoolean(mHasAuthenticationResults);
+ dest.writeBoolean(mHasRemoteResults);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "GetCredentialResponse {" + "credential=" + mHasCredentialResults + "}";
+ }
+
+ /**
+ * Constructs a {@link GetPendingCredentialResponse}.
+ *
+ * @param hasCredentialResults whether the user has any candidate credentials
+ * @param hasAuthenticationResults whether the user has any candidate authentication actions
+ * @param hasRemoteResults whether the user has any candidate remote options
+ */
+ public GetPendingCredentialResponse(boolean hasCredentialResults,
+ boolean hasAuthenticationResults, boolean hasRemoteResults) {
+ mHasCredentialResults = hasCredentialResults;
+ mHasAuthenticationResults = hasAuthenticationResults;
+ mHasRemoteResults = hasRemoteResults;
+ }
+
+ private GetPendingCredentialResponse(@NonNull Parcel in) {
+ mHasCredentialResults = in.readBoolean();
+ mHasAuthenticationResults = in.readBoolean();
+ mHasRemoteResults = in.readBoolean();
+ }
+
+ public static final @NonNull Creator<GetPendingCredentialResponse> CREATOR = new Creator<>() {
+ @Override
+ public GetPendingCredentialResponse[] newArray(int size) {
+ return new GetPendingCredentialResponse[size];
+ }
+
+ @Override
+ public GetPendingCredentialResponse createFromParcel(@NonNull Parcel in) {
+ return new GetPendingCredentialResponse(in);
+ }
+ };
+}
diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl
index 8c2cb5a..af8e7b4 100644
--- a/core/java/android/credentials/ICredentialManager.aidl
+++ b/core/java/android/credentials/ICredentialManager.aidl
@@ -27,6 +27,7 @@
import android.credentials.IClearCredentialStateCallback;
import android.credentials.ICreateCredentialCallback;
import android.credentials.IGetCredentialCallback;
+import android.credentials.IGetPendingCredentialCallback;
import android.credentials.ISetEnabledProvidersCallback;
import android.content.ComponentName;
import android.os.ICancellationSignal;
@@ -40,6 +41,8 @@
@nullable ICancellationSignal executeGetCredential(in GetCredentialRequest request, in IGetCredentialCallback callback, String callingPackage);
+ @nullable ICancellationSignal executeGetPendingCredential(in GetCredentialRequest request, in IGetPendingCredentialCallback callback, String callingPackage);
+
@nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback, String callingPackage);
@nullable ICancellationSignal clearCredentialState(in ClearCredentialStateRequest request, in IClearCredentialStateCallback callback, String callingPackage);
diff --git a/core/java/android/credentials/IGetPendingCredentialCallback.aidl b/core/java/android/credentials/IGetPendingCredentialCallback.aidl
new file mode 100644
index 0000000..4ab0f99
--- /dev/null
+++ b/core/java/android/credentials/IGetPendingCredentialCallback.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 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.credentials;
+
+import android.app.PendingIntent;
+import android.credentials.GetPendingCredentialResponse;
+
+/**
+ * Listener for a executeGetPendingCredential request.
+ *
+ * @hide
+ */
+interface IGetPendingCredentialCallback {
+ oneway void onResponse(in GetPendingCredentialResponse response);
+ oneway void onError(String errorType, String message);
+}
\ No newline at end of file
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index b9b310f..c95d081 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -4123,7 +4123,8 @@
* counterparts.
* This key will only be present for devices which advertise the
* {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability.</p>
+ * capability or devices where {@link CameraCharacteristics#getAvailableCaptureRequestKeys }
+ * lists {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}}</p>
* <p><b>Units</b>: Pixel coordinates on the image sensor</p>
* <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
*
@@ -4148,7 +4149,8 @@
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }.
* This key will only be present for devices which advertise the
* {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability.</p>
+ * capability or devices where {@link CameraCharacteristics#getAvailableCaptureRequestKeys }
+ * lists {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}}</p>
* <p><b>Units</b>: Pixels</p>
* <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
*
@@ -4172,7 +4174,8 @@
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }.
* This key will only be present for devices which advertise the
* {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability.</p>
+ * capability or devices where {@link CameraCharacteristics#getAvailableCaptureRequestKeys }
+ * lists {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}}</p>
* <p><b>Units</b>: Pixel coordinates on the image sensor</p>
* <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
*
@@ -4192,14 +4195,29 @@
* to improve various aspects of imaging such as noise reduction, low light
* performance etc. These groups can be of various sizes such as 2X2 (quad bayer),
* 3X3 (nona-bayer). This key specifies the length and width of the pixels grouped under
- * the same color filter.</p>
- * <p>This key will not be present if REMOSAIC_REPROCESSING is not supported, since RAW images
- * will have a regular bayer pattern.</p>
- * <p>This key will not be present for sensors which don't have the
- * {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability.</p>
+ * the same color filter.
+ * In case the device has the
+ * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
+ * capability :</p>
+ * <ul>
+ * <li>This key will not be present if REMOSAIC_REPROCESSING is not supported, since RAW
+ * images will have a regular bayer pattern.</li>
+ * </ul>
+ * <p>In case the device does not have the
+ * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
+ * capability :</p>
+ * <ul>
+ * <li>This key will be present if
+ * {@link CameraCharacteristics#getAvailableCaptureRequestKeys }
+ * lists {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}}, since RAW
+ * images may not necessarily have a regular bayer pattern when
+ * {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}} is set to
+ * {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }.</li>
+ * </ul>
* <p><b>Units</b>: Pixels</p>
* <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+ *
+ * @see CaptureRequest#SENSOR_PIXEL_MODE
*/
@PublicKey
@NonNull
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 722dd08..e6b3069 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -125,6 +125,23 @@
"camera.enable_landscape_to_portrait";
/**
+ * Enable physical camera availability callbacks when the logical camera is unavailable
+ *
+ * <p>Previously once a logical camera becomes unavailable, no {@link
+ * #onPhysicalCameraAvailable} or {@link #onPhysicalCameraUnavailable} will be called until
+ * the logical camera becomes available again. The results in the app opening the logical
+ * camera not able to receive physical camera availability change.</p>
+ *
+ * <p>With this change, the {@link #onPhysicalCameraAvailable} and {@link
+ * #onPhysicalCameraUnavailable} can still be called while the logical camera is unavailable.
+ * </p>
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private static final long ENABLE_PHYSICAL_CAMERA_CALLBACK_FOR_UNAVAILABLE_LOGICAL_CAMERA =
+ 244358506L;
+
+ /**
* @hide
*/
public CameraManager(Context context) {
@@ -1194,6 +1211,14 @@
}
/**
+ * @hide
+ */
+ public static boolean physicalCallbacksAreEnabledForUnavailableCamera() {
+ return CompatChanges.isChangeEnabled(
+ ENABLE_PHYSICAL_CAMERA_CALLBACK_FOR_UNAVAILABLE_LOGICAL_CAMERA);
+ }
+
+ /**
* A callback for camera devices becoming available or unavailable to open.
*
* <p>Cameras become available when they are no longer in use, or when a new
@@ -1270,9 +1295,10 @@
* to begin with, {@link #onPhysicalCameraUnavailable} may be invoked after
* {@link #onCameraAvailable}.</p>
*
- * <p>Limitation: Opening a logical camera disables the {@link #onPhysicalCameraAvailable}
- * and {@link #onPhysicalCameraUnavailable} callbacks for its physical cameras. For example,
- * if app A opens the camera device:</p>
+ * <p>If {@link android.content.pm.ApplicationInfo#targetSdkVersion targetSdkVersion}
+ * < {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, opening a logical camera
+ * disables the {@link #onPhysicalCameraAvailable} and {@link #onPhysicalCameraUnavailable}
+ * callbacks for its physical cameras. For example, if app A opens the camera device:</p>
*
* <ul>
*
@@ -1284,6 +1310,33 @@
*
* </ul>
*
+ * <p>If {@link android.content.pm.ApplicationInfo#targetSdkVersion targetSdkVersion}
+ * ≥ {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}:</p>
+ *
+ * <ul>
+ *
+ * <li>A physical camera status change will trigger {@link #onPhysicalCameraAvailable}
+ * or {@link #onPhysicalCameraUnavailable} even after the logical camera becomes
+ * unavailable. A {@link #onCameraUnavailable} call for a logical camera doesn't reset the
+ * physical cameras' availability status. This makes it possible for an application opening
+ * the logical camera device to know which physical camera becomes unavailable or available
+ * to use.</li>
+ *
+ * <li>Similar to {@link android.os.Build.VERSION_CODES#TIRAMISU Android 13} and earlier,
+ * the logical camera's {@link #onCameraAvailable} callback implies all of its physical
+ * cameras' status become available. {@link #onPhysicalCameraUnavailable} will be called
+ * for any unavailable physical cameras upon the logical camera becoming available.</li>
+ *
+ * </ul>
+ *
+ * <p>Given the pipeline nature of the camera capture through {@link
+ * android.hardware.camera2.CaptureRequest}, there may be frame drops if the application
+ * requests images from a physical camera of a logical multi-camera and that physical camera
+ * becomes unavailable. The application should stop requesting directly from an unavailable
+ * physical camera as soon as {@link #onPhysicalCameraUnavailable} is received, and also be
+ * ready to robustly handle frame drop errors for requests targeting physical cameras,
+ * since those errors may arrive before the unavailability callback.</p>
+ *
* <p>The default implementation of this method does nothing.</p>
*
* @param cameraId The unique identifier of the logical multi-camera.
@@ -1306,9 +1359,10 @@
* cameras of its parent logical multi-camera, when {@link #onCameraUnavailable} for
* the logical multi-camera is invoked.</p>
*
- * <p>Limitation: Opening a logical camera disables the {@link #onPhysicalCameraAvailable}
- * and {@link #onPhysicalCameraUnavailable} callbacks for its physical cameras. For example,
- * if app A opens the camera device:</p>
+ * <p>If {@link android.content.pm.ApplicationInfo#targetSdkVersion targetSdkVersion}
+ * < {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, opening a logical camera
+ * disables the {@link #onPhysicalCameraAvailable} and {@link #onPhysicalCameraUnavailable}
+ * callbacks for its physical cameras. For example, if app A opens the camera device:</p>
*
* <ul>
*
@@ -1320,6 +1374,33 @@
*
* </ul>
*
+ * <p>If {@link android.content.pm.ApplicationInfo#targetSdkVersion targetSdkVersion}
+ * ≥ {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}:</p>
+ *
+ * <ul>
+ *
+ * <li>A physical camera status change will trigger {@link #onPhysicalCameraAvailable}
+ * or {@link #onPhysicalCameraUnavailable} even after the logical camera becomes
+ * unavailable. A {@link #onCameraUnavailable} call for a logical camera doesn't reset the
+ * physical cameras' availability status. This makes it possible for an application opening
+ * the logical camera device to know which physical camera becomes unavailable or available
+ * to use.</li>
+ *
+ * <li>Similar to {@link android.os.Build.VERSION_CODES#TIRAMISU Android 13} and earlier,
+ * the logical camera's {@link #onCameraAvailable} callback implies all of its physical
+ * cameras' status become available. {@link #onPhysicalCameraUnavailable} will be called
+ * for any unavailable physical cameras upon the logical camera becoming available.</li>
+ *
+ * </ul>
+ *
+ * <p>Given the pipeline nature of the camera capture through {@link
+ * android.hardware.camera2.CaptureRequest}, there may be frame drops if the application
+ * requests images from a physical camera of a logical multi-camera and that physical camera
+ * becomes unavailable. The application should stop requesting directly from an unavailable
+ * physical camera as soon as {@link #onPhysicalCameraUnavailable} is received, and also be
+ * ready to robustly handle frame drop errors for requests targeting physical cameras,
+ * since those errors may arrive before the unavailability callback.</p>
+ *
* <p>The default implementation of this method does nothing.</p>
*
* @param cameraId The unique identifier of the logical multi-camera.
@@ -2283,7 +2364,8 @@
postSingleUpdate(callback, executor, id, null /*physicalId*/, status);
// Send the NOT_PRESENT state for unavailable physical cameras
- if (isAvailable(status) && mUnavailablePhysicalDevices.containsKey(id)) {
+ if ((isAvailable(status) || physicalCallbacksAreEnabledForUnavailableCamera())
+ && mUnavailablePhysicalDevices.containsKey(id)) {
ArrayList<String> unavailableIds = mUnavailablePhysicalDevices.get(id);
for (String unavailableId : unavailableIds) {
postSingleUpdate(callback, executor, id, unavailableId,
@@ -2416,7 +2498,8 @@
return;
}
- if (!isAvailable(mDeviceStatus.get(id))) {
+ if (!physicalCallbacksAreEnabledForUnavailableCamera()
+ && !isAvailable(mDeviceStatus.get(id))) {
Log.i(TAG, String.format("Camera %s is not available. Ignore physical camera "
+ "status change callback(s)", id));
return;
diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index 705afc5..ed2a198 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -3645,17 +3645,13 @@
//
/**
- * <p>This is the default sensor pixel mode. This is the only sensor pixel mode
- * supported unless a camera device advertises
- * {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }.</p>
+ * <p>This is the default sensor pixel mode.</p>
* @see CaptureRequest#SENSOR_PIXEL_MODE
*/
public static final int SENSOR_PIXEL_MODE_DEFAULT = 0;
/**
- * <p>This sensor pixel mode is offered by devices with capability
- * {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }.
- * In this mode, sensors typically do not bin pixels, as a result can offer larger
+ * <p>In this mode, sensors typically do not bin pixels, as a result can offer larger
* image sizes.</p>
* @see CaptureRequest#SENSOR_PIXEL_MODE
*/
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 381c87d..929868b 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -1430,7 +1430,9 @@
* mode.</p>
* <p>For camera devices with the
* {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability,
+ * capability or devices where
+ * {@link CameraCharacteristics#getAvailableCaptureRequestKeys }
+ * lists {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}}
* {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
* {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.preCorrectionActiveArraySizeMaximumResolution} must be used as the
* coordinate system for requests where {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode} is set to
@@ -1660,7 +1662,10 @@
* mode.</p>
* <p>For camera devices with the
* {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability, {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
+ * capability or devices where
+ * {@link CameraCharacteristics#getAvailableCaptureRequestKeys }
+ * lists {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}},
+ * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
* {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.preCorrectionActiveArraySizeMaximumResolution} must be used as the
* coordinate system for requests where {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode} is set to
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }.</p>
@@ -1882,7 +1887,10 @@
* mode.</p>
* <p>For camera devices with the
* {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability, {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
+ * capability or devices where
+ * {@link CameraCharacteristics#getAvailableCaptureRequestKeys }
+ * lists {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}},
+ * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
* {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.preCorrectionActiveArraySizeMaximumResolution} must be used as the
* coordinate system for requests where {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode} is set to
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }.</p>
@@ -3169,7 +3177,9 @@
* {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} for details.</p>
* <p>For camera devices with the
* {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability, {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
+ * capability or devices where {@link CameraCharacteristics#getAvailableCaptureRequestKeys }
+ * lists {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}}</p>
+ * <p>{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
* {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.preCorrectionActiveArraySizeMaximumResolution} must be used as the
* coordinate system for requests where {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode} is set to
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }.</p>
@@ -3517,13 +3527,10 @@
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_DEFAULT } mode.
* When operating in
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_DEFAULT } mode, sensors
- * with {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability would typically perform pixel binning in order to improve low light
+ * would typically perform pixel binning in order to improve low light
* performance, noise reduction etc. However, in
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }
- * mode (supported only
- * by {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * sensors), sensors typically operate in unbinned mode allowing for a larger image size.
+ * mode, sensors typically operate in unbinned mode allowing for a larger image size.
* The stream configurations supported in
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }
* mode are also different from those of
@@ -3537,7 +3544,32 @@
* <code>{@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap}</code>
* must not be mixed in the same CaptureRequest. In other words, these outputs are
* exclusive to each other.
- * This key does not need to be set for reprocess requests.</p>
+ * This key does not need to be set for reprocess requests.
+ * This key will be be present on devices supporting the
+ * {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
+ * capability. It may also be present on devices which do not support the aforementioned
+ * capability. In that case:</p>
+ * <ul>
+ * <li>
+ * <p>The mandatory stream combinations listed in
+ * {@link android.hardware.camera2.CameraCharacteristics.mandatoryMaximumResolutionStreamCombinations }
+ * would not apply.</p>
+ * </li>
+ * <li>
+ * <p>The bayer pattern of {@code RAW} streams when
+ * {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }
+ * is selected will be the one listed in {@link android.sensor.info.binningFactor }.</p>
+ * </li>
+ * <li>
+ * <p>The following keys will always be present:</p>
+ * <ul>
+ * <li>{@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION android.scaler.streamConfigurationMapMaximumResolution}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.pixelArraySizeMaximumResolution}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.preCorrectionActiveArraySizeMaximumResolution}</li>
+ * </ul>
+ * </li>
+ * </ul>
* <p><b>Possible values:</b></p>
* <ul>
* <li>{@link #SENSOR_PIXEL_MODE_DEFAULT DEFAULT}</li>
@@ -3548,6 +3580,9 @@
*
* @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP
* @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION
+ * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION
+ * @see CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE_MAXIMUM_RESOLUTION
+ * @see CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION
* @see #SENSOR_PIXEL_MODE_DEFAULT
* @see #SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION
*/
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 635e79c..a429f30 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -849,7 +849,9 @@
* mode.</p>
* <p>For camera devices with the
* {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability,
+ * capability or devices where
+ * {@link CameraCharacteristics#getAvailableCaptureRequestKeys }
+ * lists {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}}
* {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
* {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.preCorrectionActiveArraySizeMaximumResolution} must be used as the
* coordinate system for requests where {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode} is set to
@@ -1329,7 +1331,10 @@
* mode.</p>
* <p>For camera devices with the
* {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability, {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
+ * capability or devices where
+ * {@link CameraCharacteristics#getAvailableCaptureRequestKeys }
+ * lists {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}},
+ * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
* {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.preCorrectionActiveArraySizeMaximumResolution} must be used as the
* coordinate system for requests where {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode} is set to
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }.</p>
@@ -1962,7 +1967,10 @@
* mode.</p>
* <p>For camera devices with the
* {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability, {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
+ * capability or devices where
+ * {@link CameraCharacteristics#getAvailableCaptureRequestKeys }
+ * lists {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}},
+ * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
* {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.preCorrectionActiveArraySizeMaximumResolution} must be used as the
* coordinate system for requests where {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode} is set to
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }.</p>
@@ -3831,7 +3839,9 @@
* {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} for details.</p>
* <p>For camera devices with the
* {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability, {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
+ * capability or devices where {@link CameraCharacteristics#getAvailableCaptureRequestKeys }
+ * lists {@link CaptureRequest#SENSOR_PIXEL_MODE {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode}}</p>
+ * <p>{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution} /
* {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.preCorrectionActiveArraySizeMaximumResolution} must be used as the
* coordinate system for requests where {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode} is set to
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }.</p>
@@ -4442,13 +4452,10 @@
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_DEFAULT } mode.
* When operating in
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_DEFAULT } mode, sensors
- * with {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * capability would typically perform pixel binning in order to improve low light
+ * would typically perform pixel binning in order to improve low light
* performance, noise reduction etc. However, in
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }
- * mode (supported only
- * by {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
- * sensors), sensors typically operate in unbinned mode allowing for a larger image size.
+ * mode, sensors typically operate in unbinned mode allowing for a larger image size.
* The stream configurations supported in
* {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }
* mode are also different from those of
@@ -4462,7 +4469,32 @@
* <code>{@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap}</code>
* must not be mixed in the same CaptureRequest. In other words, these outputs are
* exclusive to each other.
- * This key does not need to be set for reprocess requests.</p>
+ * This key does not need to be set for reprocess requests.
+ * This key will be be present on devices supporting the
+ * {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
+ * capability. It may also be present on devices which do not support the aforementioned
+ * capability. In that case:</p>
+ * <ul>
+ * <li>
+ * <p>The mandatory stream combinations listed in
+ * {@link android.hardware.camera2.CameraCharacteristics.mandatoryMaximumResolutionStreamCombinations }
+ * would not apply.</p>
+ * </li>
+ * <li>
+ * <p>The bayer pattern of {@code RAW} streams when
+ * {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }
+ * is selected will be the one listed in {@link android.sensor.info.binningFactor }.</p>
+ * </li>
+ * <li>
+ * <p>The following keys will always be present:</p>
+ * <ul>
+ * <li>{@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION android.scaler.streamConfigurationMapMaximumResolution}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.activeArraySizeMaximumResolution}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.pixelArraySizeMaximumResolution}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION android.sensor.info.preCorrectionActiveArraySizeMaximumResolution}</li>
+ * </ul>
+ * </li>
+ * </ul>
* <p><b>Possible values:</b></p>
* <ul>
* <li>{@link #SENSOR_PIXEL_MODE_DEFAULT DEFAULT}</li>
@@ -4473,6 +4505,9 @@
*
* @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP
* @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION
+ * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION
+ * @see CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE_MAXIMUM_RESOLUTION
+ * @see CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION
* @see #SENSOR_PIXEL_MODE_DEFAULT
* @see #SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION
*/
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index db83e62..2fa8b87 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -101,15 +101,15 @@
// Lock to synchronize cross-thread access to device public interface
- final Object mInterfaceLock = new Object(); // access from this class and Session only!
+ final Object mInterfaceLock;
/**
* @hide
*/
@RequiresPermission(android.Manifest.permission.CAMERA)
public static CameraAdvancedExtensionSessionImpl createCameraAdvancedExtensionSession(
- @NonNull CameraDevice cameraDevice, @NonNull Context ctx,
- @NonNull ExtensionSessionConfiguration config, int sessionId)
+ @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice,
+ @NonNull Context ctx, @NonNull ExtensionSessionConfiguration config, int sessionId)
throws CameraAccessException, RemoteException {
long clientId = CameraExtensionCharacteristics.registerClient(ctx);
if (clientId < 0) {
@@ -209,7 +209,8 @@
}
private CameraAdvancedExtensionSessionImpl(long extensionClientId,
- @NonNull IAdvancedExtenderImpl extender, @NonNull CameraDevice cameraDevice,
+ @NonNull IAdvancedExtenderImpl extender,
+ @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice,
@Nullable Surface repeatingRequestSurface, @Nullable Surface burstCaptureSurface,
@Nullable Surface postviewSurface,
@NonNull CameraExtensionSession.StateCallback callback, @NonNull Executor executor,
@@ -228,6 +229,7 @@
mInitialized = false;
mInitializeHandler = new InitializeSessionHandler();
mSessionId = sessionId;
+ mInterfaceLock = cameraDevice.mInterfaceLock;
}
/**
@@ -599,13 +601,14 @@
public void onConfigured(@NonNull CameraCaptureSession session) {
synchronized (mInterfaceLock) {
mCaptureSession = session;
- try {
- CameraExtensionCharacteristics.initializeSession(mInitializeHandler);
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to initialize session! Extension service does"
- + " not respond!");
- notifyConfigurationFailure();
- }
+ }
+
+ try {
+ CameraExtensionCharacteristics.initializeSession(mInitializeHandler);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to initialize session! Extension service does"
+ + " not respond!");
+ notifyConfigurationFailure();
}
}
}
@@ -613,46 +616,56 @@
private class InitializeSessionHandler extends IInitializeSessionCallback.Stub {
@Override
public void onSuccess() {
- boolean status = true;
- synchronized (mInterfaceLock) {
- try {
- if (mSessionProcessor != null) {
- mSessionProcessor.onCaptureSessionStart(mRequestProcessor);
- mInitialized = true;
- } else {
- Log.v(TAG, "Failed to start capture session, session released before " +
- "extension start!");
- status = false;
- mCaptureSession.close();
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ boolean status = true;
+ synchronized (mInterfaceLock) {
+ try {
+ if (mSessionProcessor != null) {
+ mSessionProcessor.onCaptureSessionStart(mRequestProcessor);
+ mInitialized = true;
+ } else {
+ Log.v(TAG, "Failed to start capture session, session " +
+ " released before extension start!");
+ status = false;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to start capture session,"
+ + " extension service does not respond!");
+ status = false;
+ mInitialized = false;
+ }
}
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to start capture session,"
- + " extension service does not respond!");
- status = false;
- mCaptureSession.close();
- }
- }
- if (status) {
- final long ident = Binder.clearCallingIdentity();
- try {
- mExecutor.execute(
- () -> mCallbacks.onConfigured(CameraAdvancedExtensionSessionImpl.this));
- } finally {
- Binder.restoreCallingIdentity(ident);
+ if (status) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallbacks.onConfigured(
+ CameraAdvancedExtensionSessionImpl.this));
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ } else {
+ onFailure();
+ }
}
- } else {
- notifyConfigurationFailure();
- }
+ });
}
@Override
public void onFailure() {
- mCaptureSession.close();
- Log.e(TAG, "Failed to initialize proxy service session!"
- + " This can happen when trying to configure multiple "
- + "concurrent extension sessions!");
- notifyConfigurationFailure();
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mCaptureSession.close();
+
+ Log.e(TAG, "Failed to initialize proxy service session!"
+ + " This can happen when trying to configure multiple "
+ + "concurrent extension sessions!");
+ notifyConfigurationFailure();
+ }
+ });
}
}
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index c2b3656..9c878c7 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -117,7 +117,7 @@
private boolean mInternalRepeatingRequestEnabled = true;
// Lock to synchronize cross-thread access to device public interface
- final Object mInterfaceLock = new Object(); // access from this class and Session only!
+ final Object mInterfaceLock;
private static int nativeGetSurfaceFormat(Surface surface) {
return SurfaceUtils.getSurfaceFormat(surface);
@@ -128,7 +128,7 @@
*/
@RequiresPermission(android.Manifest.permission.CAMERA)
public static CameraExtensionSessionImpl createCameraExtensionSession(
- @NonNull CameraDevice cameraDevice,
+ @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice,
@NonNull Context ctx,
@NonNull ExtensionSessionConfiguration config,
int sessionId)
@@ -251,7 +251,7 @@
@NonNull IPreviewExtenderImpl previewExtender,
@NonNull List<Size> previewSizes,
long extensionClientId,
- @NonNull CameraDevice cameraDevice,
+ @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice,
@Nullable Surface repeatingRequestSurface,
@Nullable Surface burstCaptureSurface,
@Nullable Surface postviewSurface,
@@ -279,6 +279,7 @@
mSupportedRequestKeys = requestKeys;
mSupportedResultKeys = resultKeys;
mCaptureResultsSupported = !resultKeys.isEmpty();
+ mInterfaceLock = cameraDevice.mInterfaceLock;
}
private void initializeRepeatingRequestPipeline() throws RemoteException {
@@ -969,46 +970,56 @@
private class InitializeSessionHandler extends IInitializeSessionCallback.Stub {
@Override
public void onSuccess() {
- boolean status = true;
- ArrayList<CaptureStageImpl> initialRequestList =
- compileInitialRequestList();
- if (!initialRequestList.isEmpty()) {
- try {
- setInitialCaptureRequest(initialRequestList,
- new InitialRequestHandler(
- mRepeatingRequestImageCallback));
- } catch (CameraAccessException e) {
- Log.e(TAG,
- "Failed to initialize the initial capture "
- + "request!");
- status = false;
- }
- } else {
- try {
- setRepeatingRequest(mPreviewExtender.getCaptureStage(),
- new PreviewRequestHandler(null, null, null,
- mRepeatingRequestImageCallback));
- } catch (CameraAccessException | RemoteException e) {
- Log.e(TAG,
- "Failed to initialize internal repeating "
- + "request!");
- status = false;
- }
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ boolean status = true;
+ ArrayList<CaptureStageImpl> initialRequestList =
+ compileInitialRequestList();
+ if (!initialRequestList.isEmpty()) {
+ try {
+ setInitialCaptureRequest(initialRequestList,
+ new InitialRequestHandler(
+ mRepeatingRequestImageCallback));
+ } catch (CameraAccessException e) {
+ Log.e(TAG,
+ "Failed to initialize the initial capture "
+ + "request!");
+ status = false;
+ }
+ } else {
+ try {
+ setRepeatingRequest(mPreviewExtender.getCaptureStage(),
+ new PreviewRequestHandler(null, null, null,
+ mRepeatingRequestImageCallback));
+ } catch (CameraAccessException | RemoteException e) {
+ Log.e(TAG,
+ "Failed to initialize internal repeating "
+ + "request!");
+ status = false;
+ }
- }
+ }
- if (!status) {
- notifyConfigurationFailure();
- }
+ if (!status) {
+ notifyConfigurationFailure();
+ }
+ }
+ });
}
@Override
public void onFailure() {
- mCaptureSession.close();
- Log.e(TAG, "Failed to initialize proxy service session!"
- + " This can happen when trying to configure multiple "
- + "concurrent extension sessions!");
- notifyConfigurationFailure();
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mCaptureSession.close();
+ Log.e(TAG, "Failed to initialize proxy service session!"
+ + " This can happen when trying to configure multiple "
+ + "concurrent extension sessions!");
+ notifyConfigurationFailure();
+ }
+ });
}
}
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index 631df01..9743c1f 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -1618,15 +1618,20 @@
}
private StreamConfigurationMap getStreamConfigurationMapMaximumResolution() {
- if (!isUltraHighResolutionSensor()) {
- return null;
- }
StreamConfiguration[] configurations = getBase(
CameraCharacteristics.SCALER_AVAILABLE_STREAM_CONFIGURATIONS_MAXIMUM_RESOLUTION);
StreamConfigurationDuration[] minFrameDurations = getBase(
CameraCharacteristics.SCALER_AVAILABLE_MIN_FRAME_DURATIONS_MAXIMUM_RESOLUTION);
StreamConfigurationDuration[] stallDurations = getBase(
CameraCharacteristics.SCALER_AVAILABLE_STALL_DURATIONS_MAXIMUM_RESOLUTION);
+ // If the at least these keys haven't been advertised, there cannot be a meaningful max
+ // resolution StreamConfigurationMap
+ if (configurations == null ||
+ minFrameDurations == null ||
+ stallDurations == null) {
+ return null;
+ }
+
StreamConfiguration[] depthConfigurations = getBase(
CameraCharacteristics.DEPTH_AVAILABLE_DEPTH_STREAM_CONFIGURATIONS_MAXIMUM_RESOLUTION);
StreamConfigurationDuration[] depthMinFrameDurations = getBase(
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 490589f..a1d640a 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -44,8 +44,6 @@
import android.os.IBinder;
import android.os.IVibratorStateListener;
import android.os.InputEventInjectionSync;
-import android.os.Looper;
-import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
@@ -53,7 +51,6 @@
import android.os.Vibrator;
import android.os.VibratorManager;
import android.util.Log;
-import android.util.SparseArray;
import android.view.Display;
import android.view.InputDevice;
import android.view.InputEvent;
@@ -66,9 +63,7 @@
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
-import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.os.SomeArgs;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -88,10 +83,6 @@
// To enable these logs, run: 'adb shell setprop log.tag.InputManager DEBUG' (requires restart)
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- private static final int MSG_DEVICE_ADDED = 1;
- private static final int MSG_DEVICE_REMOVED = 2;
- private static final int MSG_DEVICE_CHANGED = 3;
-
private static InputManager sInstance;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
@@ -112,32 +103,6 @@
@Nullable
private Boolean mIsStylusPointerIconEnabled = null;
- // Guarded by mInputDevicesLock
- private final Object mInputDevicesLock = new Object();
- private SparseArray<InputDevice> mInputDevices;
- private InputDevicesChangedListener mInputDevicesChangedListener;
- private final ArrayList<InputDeviceListenerDelegate> mInputDeviceListeners = new ArrayList<>();
-
- // Guarded by mTabletModeLock
- private final Object mTabletModeLock = new Object();
- @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"})
- private TabletModeChangedListener mTabletModeChangedListener;
- private ArrayList<OnTabletModeChangedListenerDelegate> mOnTabletModeChangedListeners;
-
- private final Object mBatteryListenersLock = new Object();
- // Maps a deviceId whose battery is currently being monitored to an entry containing the
- // registered listeners for that device.
- @GuardedBy("mBatteryListenersLock")
- private SparseArray<RegisteredBatteryListeners> mBatteryListeners;
- @GuardedBy("mBatteryListenersLock")
- private IInputDeviceBatteryListener mInputDeviceBatteryListener;
-
- private final Object mKeyboardBacklightListenerLock = new Object();
- @GuardedBy("mKeyboardBacklightListenerLock")
- private ArrayList<KeyboardBacklightListenerDelegate> mKeyboardBacklightListeners;
- @GuardedBy("mKeyboardBacklightListenerLock")
- private IKeyboardBacklightListener mKeyboardBacklightListener;
-
private InputDeviceSensorManager mInputDeviceSensorManager;
/**
* Broadcast Action: Query available keyboard layouts.
@@ -403,27 +368,7 @@
*/
@Nullable
public InputDevice getInputDevice(int id) {
- synchronized (mInputDevicesLock) {
- populateInputDevicesLocked();
-
- int index = mInputDevices.indexOfKey(id);
- if (index < 0) {
- return null;
- }
-
- InputDevice inputDevice = mInputDevices.valueAt(index);
- if (inputDevice == null) {
- try {
- inputDevice = mIm.getInputDevice(id);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
- if (inputDevice != null) {
- mInputDevices.setValueAt(index, inputDevice);
- }
- }
- return inputDevice;
- }
+ return mGlobal.getInputDevice(id);
}
/**
@@ -433,34 +378,7 @@
* @hide
*/
public InputDevice getInputDeviceByDescriptor(String descriptor) {
- if (descriptor == null) {
- throw new IllegalArgumentException("descriptor must not be null.");
- }
-
- synchronized (mInputDevicesLock) {
- populateInputDevicesLocked();
-
- int numDevices = mInputDevices.size();
- for (int i = 0; i < numDevices; i++) {
- InputDevice inputDevice = mInputDevices.valueAt(i);
- if (inputDevice == null) {
- int id = mInputDevices.keyAt(i);
- try {
- inputDevice = mIm.getInputDevice(id);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
- if (inputDevice == null) {
- continue;
- }
- mInputDevices.setValueAt(i, inputDevice);
- }
- if (descriptor.equals(inputDevice.getDescriptor())) {
- return inputDevice;
- }
- }
- return null;
- }
+ return mGlobal.getInputDeviceByDescriptor(descriptor);
}
/**
@@ -468,16 +386,7 @@
* @return The input device ids.
*/
public int[] getInputDeviceIds() {
- synchronized (mInputDevicesLock) {
- populateInputDevicesLocked();
-
- final int count = mInputDevices.size();
- final int[] ids = new int[count];
- for (int i = 0; i < count; i++) {
- ids[i] = mInputDevices.keyAt(i);
- }
- return ids;
- }
+ return mGlobal.getInputDeviceIds();
}
/**
@@ -547,17 +456,7 @@
* @see #unregisterInputDeviceListener
*/
public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) {
- if (listener == null) {
- throw new IllegalArgumentException("listener must not be null");
- }
-
- synchronized (mInputDevicesLock) {
- populateInputDevicesLocked();
- int index = findInputDeviceListenerLocked(listener);
- if (index < 0) {
- mInputDeviceListeners.add(new InputDeviceListenerDelegate(listener, handler));
- }
- }
+ mGlobal.registerInputDeviceListener(listener, handler);
}
/**
@@ -568,28 +467,7 @@
* @see #registerInputDeviceListener
*/
public void unregisterInputDeviceListener(InputDeviceListener listener) {
- if (listener == null) {
- throw new IllegalArgumentException("listener must not be null");
- }
-
- synchronized (mInputDevicesLock) {
- int index = findInputDeviceListenerLocked(listener);
- if (index >= 0) {
- InputDeviceListenerDelegate d = mInputDeviceListeners.get(index);
- d.removeCallbacksAndMessages(null);
- mInputDeviceListeners.remove(index);
- }
- }
- }
-
- private int findInputDeviceListenerLocked(InputDeviceListener listener) {
- final int numListeners = mInputDeviceListeners.size();
- for (int i = 0; i < numListeners; i++) {
- if (mInputDeviceListeners.get(i).mListener == listener) {
- return i;
- }
- }
- return -1;
+ mGlobal.unregisterInputDeviceListener(listener);
}
/**
@@ -618,20 +496,7 @@
*/
public void registerOnTabletModeChangedListener(
OnTabletModeChangedListener listener, Handler handler) {
- if (listener == null) {
- throw new IllegalArgumentException("listener must not be null");
- }
- synchronized (mTabletModeLock) {
- if (mOnTabletModeChangedListeners == null) {
- initializeTabletModeListenerLocked();
- }
- int idx = findOnTabletModeChangedListenerLocked(listener);
- if (idx < 0) {
- OnTabletModeChangedListenerDelegate d =
- new OnTabletModeChangedListenerDelegate(listener, handler);
- mOnTabletModeChangedListeners.add(d);
- }
- }
+ mGlobal.registerOnTabletModeChangedListener(listener, handler);
}
/**
@@ -641,37 +506,7 @@
* @hide
*/
public void unregisterOnTabletModeChangedListener(OnTabletModeChangedListener listener) {
- if (listener == null) {
- throw new IllegalArgumentException("listener must not be null");
- }
- synchronized (mTabletModeLock) {
- int idx = findOnTabletModeChangedListenerLocked(listener);
- if (idx >= 0) {
- OnTabletModeChangedListenerDelegate d = mOnTabletModeChangedListeners.remove(idx);
- d.removeCallbacksAndMessages(null);
- }
- }
- }
-
- private void initializeTabletModeListenerLocked() {
- final TabletModeChangedListener listener = new TabletModeChangedListener();
- try {
- mIm.registerTabletModeChangedListener(listener);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
- mTabletModeChangedListener = listener;
- mOnTabletModeChangedListeners = new ArrayList<>();
- }
-
- private int findOnTabletModeChangedListenerLocked(OnTabletModeChangedListener listener) {
- final int N = mOnTabletModeChangedListeners.size();
- for (int i = 0; i < N; i++) {
- if (mOnTabletModeChangedListeners.get(i).mListener == listener) {
- return i;
- }
- }
- return -1;
+ mGlobal.unregisterOnTabletModeChangedListener(listener);
}
/**
@@ -1543,133 +1378,7 @@
*/
@Nullable
public HostUsiVersion getHostUsiVersion(@NonNull Display display) {
- Objects.requireNonNull(display, "display should not be null");
-
- // Return the first valid USI version reported by any input device associated with
- // the display.
- synchronized (mInputDevicesLock) {
- populateInputDevicesLocked();
-
- for (int i = 0; i < mInputDevices.size(); i++) {
- final InputDevice device = getInputDevice(mInputDevices.keyAt(i));
- if (device != null && device.getAssociatedDisplayId() == display.getDisplayId()) {
- if (device.getHostUsiVersion() != null) {
- return device.getHostUsiVersion();
- }
- }
- }
- }
-
- // If there are no input devices that report a valid USI version, see if there is a config
- // that specifies the USI version for the display. This is to handle cases where the USI
- // input device is not registered by the kernel/driver all the time.
- try {
- return mIm.getHostUsiVersionFromDisplayConfig(display.getDisplayId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- private void populateInputDevicesLocked() {
- if (mInputDevicesChangedListener == null) {
- final InputDevicesChangedListener listener = new InputDevicesChangedListener();
- try {
- mIm.registerInputDevicesChangedListener(listener);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
- mInputDevicesChangedListener = listener;
- }
-
- if (mInputDevices == null) {
- final int[] ids;
- try {
- ids = mIm.getInputDeviceIds();
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
-
- mInputDevices = new SparseArray<>();
- for (int id : ids) {
- mInputDevices.put(id, null);
- }
- }
- }
-
- private void onInputDevicesChanged(int[] deviceIdAndGeneration) {
- if (DEBUG) {
- Log.d(TAG, "Received input devices changed.");
- }
-
- synchronized (mInputDevicesLock) {
- for (int i = mInputDevices.size(); --i > 0; ) {
- final int deviceId = mInputDevices.keyAt(i);
- if (!containsDeviceId(deviceIdAndGeneration, deviceId)) {
- if (DEBUG) {
- Log.d(TAG, "Device removed: " + deviceId);
- }
- mInputDevices.removeAt(i);
- sendMessageToInputDeviceListenersLocked(MSG_DEVICE_REMOVED, deviceId);
- }
- }
-
- for (int i = 0; i < deviceIdAndGeneration.length; i += 2) {
- final int deviceId = deviceIdAndGeneration[i];
- int index = mInputDevices.indexOfKey(deviceId);
- if (index >= 0) {
- final InputDevice device = mInputDevices.valueAt(index);
- if (device != null) {
- final int generation = deviceIdAndGeneration[i + 1];
- if (device.getGeneration() != generation) {
- if (DEBUG) {
- Log.d(TAG, "Device changed: " + deviceId);
- }
- mInputDevices.setValueAt(index, null);
- sendMessageToInputDeviceListenersLocked(MSG_DEVICE_CHANGED, deviceId);
- }
- }
- } else {
- if (DEBUG) {
- Log.d(TAG, "Device added: " + deviceId);
- }
- mInputDevices.put(deviceId, null);
- sendMessageToInputDeviceListenersLocked(MSG_DEVICE_ADDED, deviceId);
- }
- }
- }
- }
-
- private void sendMessageToInputDeviceListenersLocked(int what, int deviceId) {
- final int numListeners = mInputDeviceListeners.size();
- for (int i = 0; i < numListeners; i++) {
- InputDeviceListenerDelegate listener = mInputDeviceListeners.get(i);
- listener.sendMessage(listener.obtainMessage(what, deviceId, 0));
- }
- }
-
- private static boolean containsDeviceId(int[] deviceIdAndGeneration, int deviceId) {
- for (int i = 0; i < deviceIdAndGeneration.length; i += 2) {
- if (deviceIdAndGeneration[i] == deviceId) {
- return true;
- }
- }
- return false;
- }
-
-
- private void onTabletModeChanged(long whenNanos, boolean inTabletMode) {
- if (DEBUG) {
- Log.d(TAG, "Received tablet mode changed: "
- + "whenNanos=" + whenNanos + ", inTabletMode=" + inTabletMode);
- }
- synchronized (mTabletModeLock) {
- final int numListeners = mOnTabletModeChangedListeners.size();
- for (int i = 0; i < numListeners; i++) {
- OnTabletModeChangedListenerDelegate listener =
- mOnTabletModeChangedListeners.get(i);
- listener.sendTabletModeChanged(whenNanos, inTabletMode);
- }
- }
+ return mGlobal.getHostUsiVersion(display);
}
/**
@@ -1808,15 +1517,7 @@
*/
@NonNull
public BatteryState getInputDeviceBatteryState(int deviceId, boolean hasBattery) {
- if (!hasBattery) {
- return new LocalBatteryState();
- }
- try {
- final IInputDeviceBatteryState state = mIm.getBatteryState(deviceId);
- return new LocalBatteryState(state.isPresent, state.status, state.capacity);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
+ return mGlobal.getInputDeviceBatteryState(deviceId, hasBattery);
}
/**
@@ -1952,49 +1653,7 @@
*/
public void addInputDeviceBatteryListener(int deviceId, @NonNull Executor executor,
@NonNull InputDeviceBatteryListener listener) {
- Objects.requireNonNull(executor, "executor should not be null");
- Objects.requireNonNull(listener, "listener should not be null");
-
- synchronized (mBatteryListenersLock) {
- if (mBatteryListeners == null) {
- mBatteryListeners = new SparseArray<>();
- mInputDeviceBatteryListener = new LocalInputDeviceBatteryListener();
- }
- RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId);
- if (listenersForDevice == null) {
- // The deviceId is currently not being monitored for battery changes.
- // Start monitoring the device.
- listenersForDevice = new RegisteredBatteryListeners();
- mBatteryListeners.put(deviceId, listenersForDevice);
- try {
- mIm.registerBatteryListener(deviceId, mInputDeviceBatteryListener);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- } else {
- // The deviceId is already being monitored for battery changes.
- // Ensure that the listener is not already registered.
- final int numDelegates = listenersForDevice.mDelegates.size();
- for (int i = 0; i < numDelegates; i++) {
- InputDeviceBatteryListener registeredListener =
- listenersForDevice.mDelegates.get(i).mListener;
- if (Objects.equals(listener, registeredListener)) {
- throw new IllegalArgumentException(
- "Attempting to register an InputDeviceBatteryListener that has "
- + "already been registered for deviceId: "
- + deviceId);
- }
- }
- }
- final InputDeviceBatteryListenerDelegate delegate =
- new InputDeviceBatteryListenerDelegate(listener, executor);
- listenersForDevice.mDelegates.add(delegate);
-
- // Notify the listener immediately if we already have the latest battery state.
- if (listenersForDevice.mInputDeviceBatteryState != null) {
- delegate.notifyBatteryStateChanged(listenersForDevice.mInputDeviceBatteryState);
- }
- }
+ mGlobal.addInputDeviceBatteryListener(deviceId, executor, listener);
}
/**
@@ -2004,44 +1663,7 @@
*/
public void removeInputDeviceBatteryListener(int deviceId,
@NonNull InputDeviceBatteryListener listener) {
- Objects.requireNonNull(listener, "listener should not be null");
-
- synchronized (mBatteryListenersLock) {
- if (mBatteryListeners == null) {
- return;
- }
- RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId);
- if (listenersForDevice == null) {
- // The deviceId is not currently being monitored.
- return;
- }
- final List<InputDeviceBatteryListenerDelegate> delegates =
- listenersForDevice.mDelegates;
- for (int i = 0; i < delegates.size();) {
- if (Objects.equals(listener, delegates.get(i).mListener)) {
- delegates.remove(i);
- continue;
- }
- i++;
- }
- if (!delegates.isEmpty()) {
- return;
- }
-
- // There are no more battery listeners for this deviceId. Stop monitoring this device.
- mBatteryListeners.remove(deviceId);
- try {
- mIm.unregisterBatteryListener(deviceId, mInputDeviceBatteryListener);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- if (mBatteryListeners.size() == 0) {
- // There are no more devices being monitored, so the registered
- // IInputDeviceBatteryListener will be automatically dropped by the server.
- mBatteryListeners = null;
- mInputDeviceBatteryListener = null;
- }
- }
+ mGlobal.removeInputDeviceBatteryListener(deviceId, listener);
}
/**
@@ -2067,30 +1689,7 @@
@RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT)
public void registerKeyboardBacklightListener(@NonNull Executor executor,
@NonNull KeyboardBacklightListener listener) throws IllegalArgumentException {
- Objects.requireNonNull(executor, "executor should not be null");
- Objects.requireNonNull(listener, "listener should not be null");
-
- synchronized (mKeyboardBacklightListenerLock) {
- if (mKeyboardBacklightListener == null) {
- mKeyboardBacklightListeners = new ArrayList<>();
- mKeyboardBacklightListener = new LocalKeyboardBacklightListener();
-
- try {
- mIm.registerKeyboardBacklightListener(mKeyboardBacklightListener);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
- final int numListeners = mKeyboardBacklightListeners.size();
- for (int i = 0; i < numListeners; i++) {
- if (mKeyboardBacklightListeners.get(i).mListener == listener) {
- throw new IllegalArgumentException("Listener has already been registered!");
- }
- }
- KeyboardBacklightListenerDelegate delegate =
- new KeyboardBacklightListenerDelegate(listener, executor);
- mKeyboardBacklightListeners.add(delegate);
- }
+ mGlobal.registerKeyboardBacklightListener(executor, listener);
}
/**
@@ -2103,23 +1702,7 @@
@RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT)
public void unregisterKeyboardBacklightListener(
@NonNull KeyboardBacklightListener listener) {
- Objects.requireNonNull(listener, "listener should not be null");
-
- synchronized (mKeyboardBacklightListenerLock) {
- if (mKeyboardBacklightListeners == null) {
- return;
- }
- mKeyboardBacklightListeners.removeIf((delegate) -> delegate.mListener == listener);
- if (mKeyboardBacklightListeners.isEmpty()) {
- try {
- mIm.unregisterKeyboardBacklightListener(mKeyboardBacklightListener);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- mKeyboardBacklightListeners = null;
- mKeyboardBacklightListener = null;
- }
- }
+ mGlobal.unregisterKeyboardBacklightListener(listener);
}
/**
@@ -2149,7 +1732,7 @@
public interface InputDeviceListener {
/**
* Called whenever an input device has been added to the system.
- * Use {@link InputManager#getInputDevice} to get more information about the device.
+ * Use {@link InputManagerGlobal#getInputDevice} to get more information about the device.
*
* @param deviceId The id of the input device that was added.
*/
@@ -2172,37 +1755,6 @@
void onInputDeviceChanged(int deviceId);
}
- private final class InputDevicesChangedListener extends IInputDevicesChangedListener.Stub {
- @Override
- public void onInputDevicesChanged(int[] deviceIdAndGeneration) throws RemoteException {
- InputManager.this.onInputDevicesChanged(deviceIdAndGeneration);
- }
- }
-
- private static final class InputDeviceListenerDelegate extends Handler {
- public final InputDeviceListener mListener;
-
- public InputDeviceListenerDelegate(InputDeviceListener listener, Handler handler) {
- super(handler != null ? handler.getLooper() : Looper.myLooper());
- mListener = listener;
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_DEVICE_ADDED:
- mListener.onInputDeviceAdded(msg.arg1);
- break;
- case MSG_DEVICE_REMOVED:
- mListener.onInputDeviceRemoved(msg.arg1);
- break;
- case MSG_DEVICE_CHANGED:
- mListener.onInputDeviceChanged(msg.arg1);
- break;
- }
- }
- }
-
/** @hide */
public interface OnTabletModeChangedListener {
/**
@@ -2235,170 +1787,4 @@
void onKeyboardBacklightChanged(
int deviceId, @NonNull KeyboardBacklightState state, boolean isTriggeredByKeyPress);
}
-
- private final class TabletModeChangedListener extends ITabletModeChangedListener.Stub {
- @Override
- public void onTabletModeChanged(long whenNanos, boolean inTabletMode) {
- InputManager.this.onTabletModeChanged(whenNanos, inTabletMode);
- }
- }
-
- private static final class OnTabletModeChangedListenerDelegate extends Handler {
- private static final int MSG_TABLET_MODE_CHANGED = 0;
-
- public final OnTabletModeChangedListener mListener;
-
- public OnTabletModeChangedListenerDelegate(
- OnTabletModeChangedListener listener, Handler handler) {
- super(handler != null ? handler.getLooper() : Looper.myLooper());
- mListener = listener;
- }
-
- public void sendTabletModeChanged(long whenNanos, boolean inTabletMode) {
- SomeArgs args = SomeArgs.obtain();
- args.argi1 = (int) whenNanos;
- args.argi2 = (int) (whenNanos >> 32);
- args.arg1 = inTabletMode;
- obtainMessage(MSG_TABLET_MODE_CHANGED, args).sendToTarget();
- }
-
- @Override
- public void handleMessage(Message msg) {
- if (msg.what == MSG_TABLET_MODE_CHANGED) {
- SomeArgs args = (SomeArgs) msg.obj;
- long whenNanos = (args.argi1 & 0xFFFFFFFFL) | ((long) args.argi2 << 32);
- boolean inTabletMode = (boolean) args.arg1;
- mListener.onTabletModeChanged(whenNanos, inTabletMode);
- }
- }
- }
-
- // Implementation of the android.hardware.BatteryState interface used to report the battery
- // state via the InputDevice#getBatteryState() and InputDeviceBatteryListener interfaces.
- private static final class LocalBatteryState extends BatteryState {
- private final boolean mIsPresent;
- private final int mStatus;
- private final float mCapacity;
-
- LocalBatteryState() {
- this(false /*isPresent*/, BatteryState.STATUS_UNKNOWN, Float.NaN /*capacity*/);
- }
-
- LocalBatteryState(boolean isPresent, int status, float capacity) {
- mIsPresent = isPresent;
- mStatus = status;
- mCapacity = capacity;
- }
-
- @Override
- public boolean isPresent() {
- return mIsPresent;
- }
-
- @Override
- public int getStatus() {
- return mStatus;
- }
-
- @Override
- public float getCapacity() {
- return mCapacity;
- }
- }
-
- private static final class RegisteredBatteryListeners {
- final List<InputDeviceBatteryListenerDelegate> mDelegates = new ArrayList<>();
- IInputDeviceBatteryState mInputDeviceBatteryState;
- }
-
- private static final class InputDeviceBatteryListenerDelegate {
- final InputDeviceBatteryListener mListener;
- final Executor mExecutor;
-
- InputDeviceBatteryListenerDelegate(InputDeviceBatteryListener listener, Executor executor) {
- mListener = listener;
- mExecutor = executor;
- }
-
- void notifyBatteryStateChanged(IInputDeviceBatteryState state) {
- mExecutor.execute(() ->
- mListener.onBatteryStateChanged(state.deviceId, state.updateTime,
- new LocalBatteryState(state.isPresent, state.status, state.capacity)));
- }
- }
-
- private class LocalInputDeviceBatteryListener extends IInputDeviceBatteryListener.Stub {
- @Override
- public void onBatteryStateChanged(IInputDeviceBatteryState state) {
- synchronized (mBatteryListenersLock) {
- if (mBatteryListeners == null) return;
- final RegisteredBatteryListeners entry = mBatteryListeners.get(state.deviceId);
- if (entry == null) return;
-
- entry.mInputDeviceBatteryState = state;
- final int numDelegates = entry.mDelegates.size();
- for (int i = 0; i < numDelegates; i++) {
- entry.mDelegates.get(i)
- .notifyBatteryStateChanged(entry.mInputDeviceBatteryState);
- }
- }
- }
- }
-
- // Implementation of the android.hardware.input.KeyboardBacklightState interface used to report
- // the keyboard backlight state via the KeyboardBacklightListener interfaces.
- private static final class LocalKeyboardBacklightState extends KeyboardBacklightState {
-
- private final int mBrightnessLevel;
- private final int mMaxBrightnessLevel;
-
- LocalKeyboardBacklightState(int brightnessLevel, int maxBrightnessLevel) {
- mBrightnessLevel = brightnessLevel;
- mMaxBrightnessLevel = maxBrightnessLevel;
- }
-
- @Override
- public int getBrightnessLevel() {
- return mBrightnessLevel;
- }
-
- @Override
- public int getMaxBrightnessLevel() {
- return mMaxBrightnessLevel;
- }
- }
-
- private static final class KeyboardBacklightListenerDelegate {
- final KeyboardBacklightListener mListener;
- final Executor mExecutor;
-
- KeyboardBacklightListenerDelegate(KeyboardBacklightListener listener, Executor executor) {
- mListener = listener;
- mExecutor = executor;
- }
-
- void notifyKeyboardBacklightChange(int deviceId, IKeyboardBacklightState state,
- boolean isTriggeredByKeyPress) {
- mExecutor.execute(() ->
- mListener.onKeyboardBacklightChanged(deviceId,
- new LocalKeyboardBacklightState(state.brightnessLevel,
- state.maxBrightnessLevel), isTriggeredByKeyPress));
- }
- }
-
- private class LocalKeyboardBacklightListener extends IKeyboardBacklightListener.Stub {
-
- @Override
- public void onBrightnessChanged(int deviceId, IKeyboardBacklightState state,
- boolean isTriggeredByKeyPress) {
- synchronized (mKeyboardBacklightListenerLock) {
- if (mKeyboardBacklightListeners == null) return;
- final int numListeners = mKeyboardBacklightListeners.size();
- for (int i = 0; i < numListeners; i++) {
- mKeyboardBacklightListeners.get(i)
- .notifyKeyboardBacklightChange(deviceId, state, isTriggeredByKeyPress);
- }
- }
- }
- }
}
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index 82dddfc..3a6df84 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -16,18 +16,71 @@
package android.hardware.input;
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.content.Context;
+import android.hardware.BatteryState;
+import android.hardware.input.InputManager.InputDeviceBatteryListener;
+import android.hardware.input.InputManager.InputDeviceListener;
+import android.hardware.input.InputManager.KeyboardBacklightListener;
+import android.hardware.input.InputManager.OnTabletModeChangedListener;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
import android.os.ServiceManager;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.InputDevice;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.SomeArgs;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
/**
* Manages communication with the input manager service on behalf of
- * an application process. You're probably looking for {@link InputManager}.
+ * an application process. You're probably looking for {@link InputManager}.
*
* @hide
*/
public final class InputManagerGlobal {
private static final String TAG = "InputManagerGlobal";
+ // To enable these logs, run: 'adb shell setprop log.tag.InputManagerGlobal DEBUG'
+ // (requires restart)
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ @GuardedBy("mInputDeviceListeners")
+ @Nullable private SparseArray<InputDevice> mInputDevices;
+ @GuardedBy("mInputDeviceListeners")
+ @Nullable private InputDevicesChangedListener mInputDevicesChangedListener;
+ @GuardedBy("mInputDeviceListeners")
+ private final ArrayList<InputDeviceListenerDelegate> mInputDeviceListeners = new ArrayList<>();
+
+ @GuardedBy("mOnTabletModeChangedListeners")
+ private final ArrayList<OnTabletModeChangedListenerDelegate> mOnTabletModeChangedListeners =
+ new ArrayList<>();
+
+ private final Object mBatteryListenersLock = new Object();
+ // Maps a deviceId whose battery is currently being monitored to an entry containing the
+ // registered listeners for that device.
+ @GuardedBy("mBatteryListenersLock")
+ @Nullable private SparseArray<RegisteredBatteryListeners> mBatteryListeners;
+ @GuardedBy("mBatteryListenersLock")
+ @Nullable private IInputDeviceBatteryListener mInputDeviceBatteryListener;
+
+ private final Object mKeyboardBacklightListenerLock = new Object();
+ @GuardedBy("mKeyboardBacklightListenerLock")
+ @Nullable private ArrayList<KeyboardBacklightListenerDelegate> mKeyboardBacklightListeners;
+ @GuardedBy("mKeyboardBacklightListenerLock")
+ @Nullable private IKeyboardBacklightListener mKeyboardBacklightListener;
private static InputManagerGlobal sInstance;
@@ -79,4 +132,693 @@
sInstance = null;
}
}
+
+ /**
+ * @see InputManager#getInputDevice(int)
+ */
+ @Nullable
+ public InputDevice getInputDevice(int id) {
+ synchronized (mInputDeviceListeners) {
+ populateInputDevicesLocked();
+
+ int index = mInputDevices.indexOfKey(id);
+ if (index < 0) {
+ return null;
+ }
+
+ InputDevice inputDevice = mInputDevices.valueAt(index);
+ if (inputDevice == null) {
+ try {
+ inputDevice = mIm.getInputDevice(id);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ if (inputDevice != null) {
+ mInputDevices.setValueAt(index, inputDevice);
+ }
+ }
+ return inputDevice;
+ }
+ }
+
+ @GuardedBy("mInputDeviceListeners")
+ private void populateInputDevicesLocked() {
+ if (mInputDevicesChangedListener == null) {
+ final InputDevicesChangedListener
+ listener = new InputDevicesChangedListener();
+ try {
+ mIm.registerInputDevicesChangedListener(listener);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ mInputDevicesChangedListener = listener;
+ }
+
+ if (mInputDevices == null) {
+ final int[] ids;
+ try {
+ ids = mIm.getInputDeviceIds();
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+
+ mInputDevices = new SparseArray<>();
+ for (int id : ids) {
+ mInputDevices.put(id, null);
+ }
+ }
+ }
+
+ private final class InputDevicesChangedListener extends IInputDevicesChangedListener.Stub {
+ @Override
+ public void onInputDevicesChanged(int[] deviceIdAndGeneration) throws RemoteException {
+ InputManagerGlobal.this.onInputDevicesChanged(deviceIdAndGeneration);
+ }
+ }
+
+ private void onInputDevicesChanged(int[] deviceIdAndGeneration) {
+ if (DEBUG) {
+ Log.d(TAG, "Received input devices changed.");
+ }
+
+ synchronized (mInputDeviceListeners) {
+ for (int i = mInputDevices.size(); --i > 0; ) {
+ final int deviceId = mInputDevices.keyAt(i);
+ if (!containsDeviceId(deviceIdAndGeneration, deviceId)) {
+ if (DEBUG) {
+ Log.d(TAG, "Device removed: " + deviceId);
+ }
+ mInputDevices.removeAt(i);
+ sendMessageToInputDeviceListenersLocked(
+ InputDeviceListenerDelegate.MSG_DEVICE_REMOVED, deviceId);
+ }
+ }
+
+ for (int i = 0; i < deviceIdAndGeneration.length; i += 2) {
+ final int deviceId = deviceIdAndGeneration[i];
+ int index = mInputDevices.indexOfKey(deviceId);
+ if (index >= 0) {
+ final InputDevice device = mInputDevices.valueAt(index);
+ if (device != null) {
+ final int generation = deviceIdAndGeneration[i + 1];
+ if (device.getGeneration() != generation) {
+ if (DEBUG) {
+ Log.d(TAG, "Device changed: " + deviceId);
+ }
+ mInputDevices.setValueAt(index, null);
+ sendMessageToInputDeviceListenersLocked(
+ InputDeviceListenerDelegate.MSG_DEVICE_CHANGED, deviceId);
+ }
+ }
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Device added: " + deviceId);
+ }
+ mInputDevices.put(deviceId, null);
+ sendMessageToInputDeviceListenersLocked(
+ InputDeviceListenerDelegate.MSG_DEVICE_ADDED, deviceId);
+ }
+ }
+ }
+ }
+
+ private static final class InputDeviceListenerDelegate extends Handler {
+ public final InputDeviceListener mListener;
+ static final int MSG_DEVICE_ADDED = 1;
+ static final int MSG_DEVICE_REMOVED = 2;
+ static final int MSG_DEVICE_CHANGED = 3;
+
+ InputDeviceListenerDelegate(InputDeviceListener listener, Handler handler) {
+ super(handler != null ? handler.getLooper() : Looper.myLooper());
+ mListener = listener;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_DEVICE_ADDED:
+ mListener.onInputDeviceAdded(msg.arg1);
+ break;
+ case MSG_DEVICE_REMOVED:
+ mListener.onInputDeviceRemoved(msg.arg1);
+ break;
+ case MSG_DEVICE_CHANGED:
+ mListener.onInputDeviceChanged(msg.arg1);
+ break;
+ }
+ }
+ }
+
+ private static boolean containsDeviceId(int[] deviceIdAndGeneration, int deviceId) {
+ for (int i = 0; i < deviceIdAndGeneration.length; i += 2) {
+ if (deviceIdAndGeneration[i] == deviceId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @GuardedBy("mInputDeviceListeners")
+ private void sendMessageToInputDeviceListenersLocked(int what, int deviceId) {
+ final int numListeners = mInputDeviceListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ InputDeviceListenerDelegate listener = mInputDeviceListeners.get(i);
+ listener.sendMessage(listener.obtainMessage(what, deviceId, 0));
+ }
+ }
+
+ /**
+ * @see InputManager#registerInputDeviceListener
+ */
+ void registerInputDeviceListener(InputDeviceListener listener, Handler handler) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener must not be null");
+ }
+
+ synchronized (mInputDeviceListeners) {
+ populateInputDevicesLocked();
+ int index = findInputDeviceListenerLocked(listener);
+ if (index < 0) {
+ mInputDeviceListeners.add(new InputDeviceListenerDelegate(listener, handler));
+ }
+ }
+ }
+
+ /**
+ * @see InputManager#unregisterInputDeviceListener
+ */
+ void unregisterInputDeviceListener(InputDeviceListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener must not be null");
+ }
+
+ synchronized (mInputDeviceListeners) {
+ int index = findInputDeviceListenerLocked(listener);
+ if (index >= 0) {
+ InputDeviceListenerDelegate d = mInputDeviceListeners.get(index);
+ d.removeCallbacksAndMessages(null);
+ mInputDeviceListeners.remove(index);
+ }
+ }
+ }
+
+ @GuardedBy("mInputDeviceListeners")
+ private int findInputDeviceListenerLocked(InputDeviceListener listener) {
+ final int numListeners = mInputDeviceListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ if (mInputDeviceListeners.get(i).mListener == listener) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * @see InputManager#getInputDeviceIds
+ */
+ public int[] getInputDeviceIds() {
+ synchronized (mInputDeviceListeners) {
+ populateInputDevicesLocked();
+
+ final int count = mInputDevices.size();
+ final int[] ids = new int[count];
+ for (int i = 0; i < count; i++) {
+ ids[i] = mInputDevices.keyAt(i);
+ }
+ return ids;
+ }
+ }
+
+ /**
+ * @see InputManager#getInputDeviceByDescriptor
+ */
+ InputDevice getInputDeviceByDescriptor(String descriptor) {
+ if (descriptor == null) {
+ throw new IllegalArgumentException("descriptor must not be null.");
+ }
+
+ synchronized (mInputDeviceListeners) {
+ populateInputDevicesLocked();
+
+ int numDevices = mInputDevices.size();
+ for (int i = 0; i < numDevices; i++) {
+ InputDevice inputDevice = mInputDevices.valueAt(i);
+ if (inputDevice == null) {
+ int id = mInputDevices.keyAt(i);
+ try {
+ inputDevice = mIm.getInputDevice(id);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ if (inputDevice == null) {
+ continue;
+ }
+ mInputDevices.setValueAt(i, inputDevice);
+ }
+ if (descriptor.equals(inputDevice.getDescriptor())) {
+ return inputDevice;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * @see InputManager#getHostUsiVersion
+ */
+ @Nullable
+ HostUsiVersion getHostUsiVersion(@NonNull Display display) {
+ Objects.requireNonNull(display, "display should not be null");
+
+ // Return the first valid USI version reported by any input device associated with
+ // the display.
+ synchronized (mInputDeviceListeners) {
+ populateInputDevicesLocked();
+
+ for (int i = 0; i < mInputDevices.size(); i++) {
+ final InputDevice device = getInputDevice(mInputDevices.keyAt(i));
+ if (device != null && device.getAssociatedDisplayId() == display.getDisplayId()) {
+ if (device.getHostUsiVersion() != null) {
+ return device.getHostUsiVersion();
+ }
+ }
+ }
+ }
+
+ // If there are no input devices that report a valid USI version, see if there is a config
+ // that specifies the USI version for the display. This is to handle cases where the USI
+ // input device is not registered by the kernel/driver all the time.
+ try {
+ return mIm.getHostUsiVersionFromDisplayConfig(display.getDisplayId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private void onTabletModeChanged(long whenNanos, boolean inTabletMode) {
+ if (DEBUG) {
+ Log.d(TAG, "Received tablet mode changed: "
+ + "whenNanos=" + whenNanos + ", inTabletMode=" + inTabletMode);
+ }
+ synchronized (mOnTabletModeChangedListeners) {
+ final int numListeners = mOnTabletModeChangedListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ OnTabletModeChangedListenerDelegate listener =
+ mOnTabletModeChangedListeners.get(i);
+ listener.sendTabletModeChanged(whenNanos, inTabletMode);
+ }
+ }
+ }
+
+ private final class TabletModeChangedListener extends ITabletModeChangedListener.Stub {
+ @Override
+ public void onTabletModeChanged(long whenNanos, boolean inTabletMode) {
+ InputManagerGlobal.this.onTabletModeChanged(whenNanos, inTabletMode);
+ }
+ }
+
+ private static final class OnTabletModeChangedListenerDelegate extends Handler {
+ private static final int MSG_TABLET_MODE_CHANGED = 0;
+
+ public final OnTabletModeChangedListener mListener;
+
+ OnTabletModeChangedListenerDelegate(
+ OnTabletModeChangedListener listener, Handler handler) {
+ super(handler != null ? handler.getLooper() : Looper.myLooper());
+ mListener = listener;
+ }
+
+ public void sendTabletModeChanged(long whenNanos, boolean inTabletMode) {
+ SomeArgs args = SomeArgs.obtain();
+ args.argi1 = (int) whenNanos;
+ args.argi2 = (int) (whenNanos >> 32);
+ args.arg1 = inTabletMode;
+ obtainMessage(MSG_TABLET_MODE_CHANGED, args).sendToTarget();
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_TABLET_MODE_CHANGED) {
+ SomeArgs args = (SomeArgs) msg.obj;
+ long whenNanos = (args.argi1 & 0xFFFFFFFFL) | ((long) args.argi2 << 32);
+ boolean inTabletMode = (boolean) args.arg1;
+ mListener.onTabletModeChanged(whenNanos, inTabletMode);
+ }
+ }
+ }
+
+ /**
+ * @see InputManager#registerInputDeviceListener(InputDeviceListener, Handler)
+ */
+ void registerOnTabletModeChangedListener(
+ OnTabletModeChangedListener listener, Handler handler) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener must not be null");
+ }
+ synchronized (mOnTabletModeChangedListeners) {
+ if (mOnTabletModeChangedListeners == null) {
+ initializeTabletModeListenerLocked();
+ }
+ int idx = findOnTabletModeChangedListenerLocked(listener);
+ if (idx < 0) {
+ OnTabletModeChangedListenerDelegate d =
+ new OnTabletModeChangedListenerDelegate(listener, handler);
+ mOnTabletModeChangedListeners.add(d);
+ }
+ }
+ }
+
+ /**
+ * @see InputManager#unregisterOnTabletModeChangedListener(OnTabletModeChangedListener)
+ */
+ void unregisterOnTabletModeChangedListener(OnTabletModeChangedListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener must not be null");
+ }
+ synchronized (mOnTabletModeChangedListeners) {
+ int idx = findOnTabletModeChangedListenerLocked(listener);
+ if (idx >= 0) {
+ OnTabletModeChangedListenerDelegate d = mOnTabletModeChangedListeners.remove(idx);
+ d.removeCallbacksAndMessages(null);
+ }
+ }
+ }
+
+ @GuardedBy("mOnTabletModeChangedListeners")
+ private void initializeTabletModeListenerLocked() {
+ final TabletModeChangedListener listener = new TabletModeChangedListener();
+ try {
+ mIm.registerTabletModeChangedListener(listener);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ @GuardedBy("mOnTabletModeChangedListeners")
+ private int findOnTabletModeChangedListenerLocked(OnTabletModeChangedListener listener) {
+ final int n = mOnTabletModeChangedListeners.size();
+ for (int i = 0; i < n; i++) {
+ if (mOnTabletModeChangedListeners.get(i).mListener == listener) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private static final class RegisteredBatteryListeners {
+ final List<InputDeviceBatteryListenerDelegate> mDelegates = new ArrayList<>();
+ IInputDeviceBatteryState mInputDeviceBatteryState;
+ }
+
+ private static final class InputDeviceBatteryListenerDelegate {
+ final InputDeviceBatteryListener mListener;
+ final Executor mExecutor;
+
+ InputDeviceBatteryListenerDelegate(InputDeviceBatteryListener listener, Executor executor) {
+ mListener = listener;
+ mExecutor = executor;
+ }
+
+ void notifyBatteryStateChanged(IInputDeviceBatteryState state) {
+ mExecutor.execute(() ->
+ mListener.onBatteryStateChanged(state.deviceId, state.updateTime,
+ new LocalBatteryState(state.isPresent, state.status, state.capacity)));
+ }
+ }
+
+ /**
+ * @see InputManager#addInputDeviceBatteryListener(int, Executor, InputDeviceBatteryListener)
+ */
+ void addInputDeviceBatteryListener(int deviceId, @NonNull Executor executor,
+ @NonNull InputDeviceBatteryListener listener) {
+ Objects.requireNonNull(executor, "executor should not be null");
+ Objects.requireNonNull(listener, "listener should not be null");
+
+ synchronized (mBatteryListenersLock) {
+ if (mBatteryListeners == null) {
+ mBatteryListeners = new SparseArray<>();
+ mInputDeviceBatteryListener = new LocalInputDeviceBatteryListener();
+ }
+ RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId);
+ if (listenersForDevice == null) {
+ // The deviceId is currently not being monitored for battery changes.
+ // Start monitoring the device.
+ listenersForDevice = new RegisteredBatteryListeners();
+ mBatteryListeners.put(deviceId, listenersForDevice);
+ try {
+ mIm.registerBatteryListener(deviceId, mInputDeviceBatteryListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ } else {
+ // The deviceId is already being monitored for battery changes.
+ // Ensure that the listener is not already registered.
+ final int numDelegates = listenersForDevice.mDelegates.size();
+ for (int i = 0; i < numDelegates; i++) {
+ InputDeviceBatteryListener registeredListener =
+ listenersForDevice.mDelegates.get(i).mListener;
+ if (Objects.equals(listener, registeredListener)) {
+ throw new IllegalArgumentException(
+ "Attempting to register an InputDeviceBatteryListener that has "
+ + "already been registered for deviceId: "
+ + deviceId);
+ }
+ }
+ }
+ final InputDeviceBatteryListenerDelegate delegate =
+ new InputDeviceBatteryListenerDelegate(listener, executor);
+ listenersForDevice.mDelegates.add(delegate);
+
+ // Notify the listener immediately if we already have the latest battery state.
+ if (listenersForDevice.mInputDeviceBatteryState != null) {
+ delegate.notifyBatteryStateChanged(listenersForDevice.mInputDeviceBatteryState);
+ }
+ }
+ }
+
+ /**
+ * @see InputManager#removeInputDeviceBatteryListener(int, InputDeviceBatteryListener)
+ */
+ void removeInputDeviceBatteryListener(int deviceId,
+ @NonNull InputDeviceBatteryListener listener) {
+ Objects.requireNonNull(listener, "listener should not be null");
+
+ synchronized (mBatteryListenersLock) {
+ if (mBatteryListeners == null) {
+ return;
+ }
+ RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId);
+ if (listenersForDevice == null) {
+ // The deviceId is not currently being monitored.
+ return;
+ }
+ final List<InputDeviceBatteryListenerDelegate> delegates =
+ listenersForDevice.mDelegates;
+ for (int i = 0; i < delegates.size();) {
+ if (Objects.equals(listener, delegates.get(i).mListener)) {
+ delegates.remove(i);
+ continue;
+ }
+ i++;
+ }
+ if (!delegates.isEmpty()) {
+ return;
+ }
+
+ // There are no more battery listeners for this deviceId. Stop monitoring this device.
+ mBatteryListeners.remove(deviceId);
+ try {
+ mIm.unregisterBatteryListener(deviceId, mInputDeviceBatteryListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ if (mBatteryListeners.size() == 0) {
+ // There are no more devices being monitored, so the registered
+ // IInputDeviceBatteryListener will be automatically dropped by the server.
+ mBatteryListeners = null;
+ mInputDeviceBatteryListener = null;
+ }
+ }
+ }
+
+ private class LocalInputDeviceBatteryListener extends IInputDeviceBatteryListener.Stub {
+ @Override
+ public void onBatteryStateChanged(IInputDeviceBatteryState state) {
+ synchronized (mBatteryListenersLock) {
+ if (mBatteryListeners == null) return;
+ final RegisteredBatteryListeners entry = mBatteryListeners.get(state.deviceId);
+ if (entry == null) return;
+
+ entry.mInputDeviceBatteryState = state;
+ final int numDelegates = entry.mDelegates.size();
+ for (int i = 0; i < numDelegates; i++) {
+ entry.mDelegates.get(i)
+ .notifyBatteryStateChanged(entry.mInputDeviceBatteryState);
+ }
+ }
+ }
+ }
+
+ /**
+ * @see InputManager#getInputDeviceBatteryState(int, boolean)
+ */
+ @NonNull
+ BatteryState getInputDeviceBatteryState(int deviceId, boolean hasBattery) {
+ if (!hasBattery) {
+ return new LocalBatteryState();
+ }
+ try {
+ final IInputDeviceBatteryState state = mIm.getBatteryState(deviceId);
+ return new LocalBatteryState(state.isPresent, state.status, state.capacity);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ // Implementation of the android.hardware.BatteryState interface used to report the battery
+ // state via the InputDevice#getBatteryState() and InputDeviceBatteryListener interfaces.
+ private static final class LocalBatteryState extends BatteryState {
+ private final boolean mIsPresent;
+ private final int mStatus;
+ private final float mCapacity;
+
+ LocalBatteryState() {
+ this(false /*isPresent*/, BatteryState.STATUS_UNKNOWN, Float.NaN /*capacity*/);
+ }
+
+ LocalBatteryState(boolean isPresent, int status, float capacity) {
+ mIsPresent = isPresent;
+ mStatus = status;
+ mCapacity = capacity;
+ }
+
+ @Override
+ public boolean isPresent() {
+ return mIsPresent;
+ }
+
+ @Override
+ public int getStatus() {
+ return mStatus;
+ }
+
+ @Override
+ public float getCapacity() {
+ return mCapacity;
+ }
+ }
+
+ private static final class KeyboardBacklightListenerDelegate {
+ final InputManager.KeyboardBacklightListener mListener;
+ final Executor mExecutor;
+
+ KeyboardBacklightListenerDelegate(KeyboardBacklightListener listener, Executor executor) {
+ mListener = listener;
+ mExecutor = executor;
+ }
+
+ void notifyKeyboardBacklightChange(int deviceId, IKeyboardBacklightState state,
+ boolean isTriggeredByKeyPress) {
+ mExecutor.execute(() ->
+ mListener.onKeyboardBacklightChanged(deviceId,
+ new LocalKeyboardBacklightState(state.brightnessLevel,
+ state.maxBrightnessLevel), isTriggeredByKeyPress));
+ }
+ }
+
+ private class LocalKeyboardBacklightListener extends IKeyboardBacklightListener.Stub {
+
+ @Override
+ public void onBrightnessChanged(int deviceId, IKeyboardBacklightState state,
+ boolean isTriggeredByKeyPress) {
+ synchronized (mKeyboardBacklightListenerLock) {
+ if (mKeyboardBacklightListeners == null) return;
+ final int numListeners = mKeyboardBacklightListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ mKeyboardBacklightListeners.get(i)
+ .notifyKeyboardBacklightChange(deviceId, state, isTriggeredByKeyPress);
+ }
+ }
+ }
+ }
+
+ // Implementation of the android.hardware.input.KeyboardBacklightState interface used to report
+ // the keyboard backlight state via the KeyboardBacklightListener interfaces.
+ private static final class LocalKeyboardBacklightState extends KeyboardBacklightState {
+
+ private final int mBrightnessLevel;
+ private final int mMaxBrightnessLevel;
+
+ LocalKeyboardBacklightState(int brightnessLevel, int maxBrightnessLevel) {
+ mBrightnessLevel = brightnessLevel;
+ mMaxBrightnessLevel = maxBrightnessLevel;
+ }
+
+ @Override
+ public int getBrightnessLevel() {
+ return mBrightnessLevel;
+ }
+
+ @Override
+ public int getMaxBrightnessLevel() {
+ return mMaxBrightnessLevel;
+ }
+ }
+
+ /**
+ * @see InputManager#registerKeyboardBacklightListener(Executor, KeyboardBacklightListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT)
+ void registerKeyboardBacklightListener(@NonNull Executor executor,
+ @NonNull KeyboardBacklightListener listener) throws IllegalArgumentException {
+ Objects.requireNonNull(executor, "executor should not be null");
+ Objects.requireNonNull(listener, "listener should not be null");
+
+ synchronized (mKeyboardBacklightListenerLock) {
+ if (mKeyboardBacklightListener == null) {
+ mKeyboardBacklightListeners = new ArrayList<>();
+ mKeyboardBacklightListener = new LocalKeyboardBacklightListener();
+
+ try {
+ mIm.registerKeyboardBacklightListener(mKeyboardBacklightListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ final int numListeners = mKeyboardBacklightListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ if (mKeyboardBacklightListeners.get(i).mListener == listener) {
+ throw new IllegalArgumentException("Listener has already been registered!");
+ }
+ }
+ KeyboardBacklightListenerDelegate delegate =
+ new KeyboardBacklightListenerDelegate(listener, executor);
+ mKeyboardBacklightListeners.add(delegate);
+ }
+ }
+
+ /**
+ * @see InputManager#unregisterKeyboardBacklightListener(KeyboardBacklightListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT)
+ void unregisterKeyboardBacklightListener(
+ @NonNull KeyboardBacklightListener listener) {
+ Objects.requireNonNull(listener, "listener should not be null");
+
+ synchronized (mKeyboardBacklightListenerLock) {
+ if (mKeyboardBacklightListeners == null) {
+ return;
+ }
+ mKeyboardBacklightListeners.removeIf((delegate) -> delegate.mListener == listener);
+ if (mKeyboardBacklightListeners.isEmpty()) {
+ try {
+ mIm.unregisterKeyboardBacklightListener(mKeyboardBacklightListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mKeyboardBacklightListeners = null;
+ mKeyboardBacklightListener = null;
+ }
+ }
+ }
}
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index 9689be2..244632a 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -64,17 +64,27 @@
/**
* The product name for attestation. In non-default builds (like the AOSP build) the value of
* the 'PRODUCT' system property may be different to the one provisioned to KeyMint,
- * and Keymint attestation would still attest to the product name, it's running on.
+ * and Keymint attestation would still attest to the product name which was provisioned.
* @hide
*/
@Nullable
@TestApi
- public static final String PRODUCT_FOR_ATTESTATION =
- getString("ro.product.name_for_attestation");
+ public static final String PRODUCT_FOR_ATTESTATION = getVendorDeviceIdProperty("name");
/** The name of the industrial design. */
public static final String DEVICE = getString("ro.product.device");
+ /**
+ * The device name for attestation. In non-default builds (like the AOSP build) the value of
+ * the 'DEVICE' system property may be different to the one provisioned to KeyMint,
+ * and Keymint attestation would still attest to the device name which was provisioned.
+ * @hide
+ */
+ @Nullable
+ @TestApi
+ public static final String DEVICE_FOR_ATTESTATION =
+ getVendorDeviceIdProperty("device");
+
/** The name of the underlying board, like "goldfish". */
public static final String BOARD = getString("ro.product.board");
@@ -97,19 +107,29 @@
/** The manufacturer of the product/hardware. */
public static final String MANUFACTURER = getString("ro.product.manufacturer");
+ /**
+ * The manufacturer name for attestation. In non-default builds (like the AOSP build) the value
+ * of the 'MANUFACTURER' system property may be different to the one provisioned to KeyMint,
+ * and Keymint attestation would still attest to the manufacturer which was provisioned.
+ * @hide
+ */
+ @Nullable
+ @TestApi
+ public static final String MANUFACTURER_FOR_ATTESTATION =
+ getVendorDeviceIdProperty("manufacturer");
+
/** The consumer-visible brand with which the product/hardware will be associated, if any. */
public static final String BRAND = getString("ro.product.brand");
/**
* The product brand for attestation. In non-default builds (like the AOSP build) the value of
* the 'BRAND' system property may be different to the one provisioned to KeyMint,
- * and Keymint attestation would still attest to the product brand, it's running on.
+ * and Keymint attestation would still attest to the product brand which was provisioned.
* @hide
*/
@Nullable
@TestApi
- public static final String BRAND_FOR_ATTESTATION =
- getString("ro.product.brand_for_attestation");
+ public static final String BRAND_FOR_ATTESTATION = getVendorDeviceIdProperty("brand");
/** The end-user-visible name for the end product. */
public static final String MODEL = getString("ro.product.model");
@@ -117,13 +137,12 @@
/**
* The product model for attestation. In non-default builds (like the AOSP build) the value of
* the 'MODEL' system property may be different to the one provisioned to KeyMint,
- * and Keymint attestation would still attest to the product model, it's running on.
+ * and Keymint attestation would still attest to the product model which was provisioned.
* @hide
*/
@Nullable
@TestApi
- public static final String MODEL_FOR_ATTESTATION =
- getString("ro.product.model_for_attestation");
+ public static final String MODEL_FOR_ATTESTATION = getVendorDeviceIdProperty("model");
/** The manufacturer of the device's primary system-on-chip. */
@NonNull
@@ -1527,6 +1546,17 @@
private static String getString(String property) {
return SystemProperties.get(property, UNKNOWN);
}
+ /**
+ * Return attestation specific proerties.
+ * @param property model, name, brand, device or manufacturer.
+ * @return property value or UNKNOWN
+ */
+ private static String getVendorDeviceIdProperty(String property) {
+ String attestProp = getString(
+ TextUtils.formatSimple("ro.product.%s_for_attestation", property));
+ return attestProp.equals(UNKNOWN)
+ ? getString(TextUtils.formatSimple("ro.product.vendor.%s", property)) : UNKNOWN;
+ }
private static String[] getStringList(String property, String separator) {
String value = SystemProperties.get(property);
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index ac1583a..b2208d1 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -365,6 +365,11 @@
public static final int LAST_APPLICATION_CACHE_GID = 29999;
/**
+ * An invalid PID value.
+ */
+ public static final int INVALID_PID = -1;
+
+ /**
* Standard priority of application threads.
* Use with {@link #setThreadPriority(int)} and
* {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 290f929..86e678d 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -5601,16 +5601,15 @@
*
* @see #KEY_RESTRICTIONS_PENDING
*
- * @deprecated Use
+ * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * it is possible for there to be multiple managing apps on the device with the ability to set
+ * restrictions, e.g. an Enterprise Device Policy Controller (DPC) and a Supervision admin.
+ * This API will only to return the restrictions set by the DPCs. To retrieve restrictions
+ * set by all managing apps, use
* {@link android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin} instead.
- * Starting from Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, it is
- * possible for there to be multiple managing agents on the device with the ability to set
- * restrictions. This API will only to return the restrictions set by device policy controllers
- * (DPCs)
*
* @see DevicePolicyManager
*/
- @Deprecated
@WorkerThread
@UserHandleAware(enabledSinceTargetSdkVersion = Build.VERSION_CODES.TIRAMISU)
public Bundle getApplicationRestrictions(String packageName) {
@@ -5623,12 +5622,15 @@
}
/**
- * @deprecated Use
+ * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * it is possible for there to be multiple managing apps on the device with the ability to set
+ * restrictions, e.g. an Enterprise Device Policy Controller (DPC) and a Supervision admin.
+ * This API will only to return the restrictions set by the DPCs. To retrieve restrictions
+ * set by all managing apps, use
* {@link android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin} instead.
*
* @hide
*/
- @Deprecated
@WorkerThread
public Bundle getApplicationRestrictions(String packageName, UserHandle user) {
try {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 07d265b..127c7a0 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9541,6 +9541,14 @@
public static final String SCREENSAVER_COMPLICATIONS_ENABLED =
"screensaver_complications_enabled";
+ /**
+ * Whether home controls are enabled to be shown over the screensaver by the user.
+ *
+ * @hide
+ */
+ public static final String SCREENSAVER_HOME_CONTROLS_ENABLED =
+ "screensaver_home_controls_enabled";
+
/**
* Default, indicates that the user has not yet started the dock setup flow.
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 6e4535b..5469916 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -27,6 +27,8 @@
import android.util.Log;
import android.view.WindowManager;
+import java.util.concurrent.Executor;
+
/**
* Basic implementation of for {@link IDreamOverlay} for testing.
@@ -40,6 +42,12 @@
// The last client that started dreaming and hasn't ended
private OverlayClient mCurrentClient;
+ /**
+ * Executor used to run callbacks that subclasses will implement. Any calls coming over Binder
+ * from {@link OverlayClient} should perform the work they need to do on this executor.
+ */
+ private Executor mExecutor;
+
// An {@link IDreamOverlayClient} implementation that identifies itself when forwarding
// requests to the {@link DreamOverlayService}
private static class OverlayClient extends IDreamOverlayClient.Stub {
@@ -61,8 +69,6 @@
mService.startDream(this, params);
}
-
-
@Override
public void wakeUp() {
mService.wakeUp(this, () -> {
@@ -97,12 +103,20 @@
}
private void startDream(OverlayClient client, WindowManager.LayoutParams params) {
- endDream(mCurrentClient);
- mCurrentClient = client;
- onStartDream(params);
+ // Run on executor as this is a binder call from OverlayClient.
+ mExecutor.execute(() -> {
+ endDreamInternal(mCurrentClient);
+ mCurrentClient = client;
+ onStartDream(params);
+ });
}
private void endDream(OverlayClient client) {
+ // Run on executor as this is a binder call from OverlayClient.
+ mExecutor.execute(() -> endDreamInternal(client));
+ }
+
+ private void endDreamInternal(OverlayClient client) {
if (client == null || client != mCurrentClient) {
return;
}
@@ -112,11 +126,14 @@
}
private void wakeUp(OverlayClient client, Runnable callback) {
- if (mCurrentClient != client) {
- return;
- }
+ // Run on executor as this is a binder call from OverlayClient.
+ mExecutor.execute(() -> {
+ if (mCurrentClient != client) {
+ return;
+ }
- onWakeUp(callback);
+ onWakeUp(callback);
+ });
}
private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
@@ -134,6 +151,25 @@
public DreamOverlayService() {
}
+ /**
+ * This constructor allows providing an executor to run callbacks on.
+ *
+ * @hide
+ */
+ public DreamOverlayService(@NonNull Executor executor) {
+ mExecutor = executor;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (mExecutor == null) {
+ // If no executor was provided, use the main executor. onCreate is the earliest time
+ // getMainExecutor is available.
+ mExecutor = getMainExecutor();
+ }
+ }
+
@Nullable
@Override
public final IBinder onBind(@NonNull Intent intent) {
@@ -143,6 +179,10 @@
/**
* This method is overridden by implementations to handle when the dream has started and the
* window is ready to be interacted with.
+ *
+ * This callback will be run on the {@link Executor} provided in the constructor if provided, or
+ * on the main executor if none was provided.
+ *
* @param layoutParams The {@link android.view.WindowManager.LayoutParams} associated with the
* dream window.
*/
@@ -153,6 +193,9 @@
* to wakeup. This allows any overlay animations to run. By default, the method will invoke
* the callback immediately.
*
+ * This callback will be run on the {@link Executor} provided in the constructor if provided, or
+ * on the main executor if none was provided.
+ *
* @param onCompleteCallback The callback to trigger to notify the dream service that the
* overlay has completed waking up.
* @hide
@@ -164,6 +207,9 @@
/**
* This method is overridden by implementations to handle when the dream has ended. There may
* be earlier signals leading up to this step, such as @{@link #onWakeUp(Runnable)}.
+ *
+ * This callback will be run on the {@link Executor} provided in the constructor if provided, or
+ * on the main executor if none was provided.
*/
public void onEndDream() {
}
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index ce8af83..d0f3820 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -1362,6 +1362,11 @@
if (!ActivityTaskManager.getService().startDreamActivity(i)) {
detach();
}
+ } catch (SecurityException e) {
+ Log.w(mTag,
+ "Received SecurityException trying to start DreamActivity. "
+ + "Aborting dream start.");
+ detach();
} catch (RemoteException e) {
Log.w(mTag, "Could not connect to activity task manager to start dream activity");
e.rethrowFromSystemServer();
diff --git a/core/java/android/speech/IRecognitionService.aidl b/core/java/android/speech/IRecognitionService.aidl
index 3134dcd..1148fe3 100644
--- a/core/java/android/speech/IRecognitionService.aidl
+++ b/core/java/android/speech/IRecognitionService.aidl
@@ -78,23 +78,10 @@
* information see {@link #checkRecognitionSupport}, {@link #startListening} and
* {@link RecognizerIntent}.
*
- * Progress can be monitord by calling {@link #setModelDownloadListener} before a trigger.
+ * Progress updates can be received via {@link #IModelDownloadListener}.
*/
- void triggerModelDownload(in Intent recognizerIntent, in AttributionSource attributionSource);
-
- /**
- * Sets listener to received download progress updates. Clients still have to call
- * {@link #triggerModelDownload} to trigger a model download.
- */
- void setModelDownloadListener(
+ void triggerModelDownload(
in Intent recognizerIntent,
in AttributionSource attributionSource,
in IModelDownloadListener listener);
-
- /**
- * Clears the listener for model download events attached to a recognitionIntent if any.
- */
- void clearModelDownloadListener(
- in Intent recognizerIntent,
- in AttributionSource attributionSource);
}
diff --git a/core/java/android/speech/ModelDownloadListener.java b/core/java/android/speech/ModelDownloadListener.java
index 6c24399..a58ec90c 100644
--- a/core/java/android/speech/ModelDownloadListener.java
+++ b/core/java/android/speech/ModelDownloadListener.java
@@ -22,20 +22,27 @@
*/
public interface ModelDownloadListener {
/**
- * Called by {@link RecognitionService} when there's an update on the download progress.
+ * Called by {@link RecognitionService} only if the download has started after the request.
*
- * <p>RecognitionService will call this zero or more times during the download.</p>
+ * <p> The number of calls to this method varies depending of the {@link RecognitionService}
+ * implementation. If the download finished quickly enough, {@link #onSuccess()} may be called
+ * directly. In other cases, this method may be called any number of times during the download.
+ *
+ * @param completedPercent the percentage of download that is completed
*/
void onProgress(int completedPercent);
/**
- * Called when {@link RecognitionService} completed the download and it can now be used to
- * satisfy recognition requests.
+ * This method is called:
+ * <li> if the model is already available;
+ * <li> if the {@link RecognitionService} has started and completed the download.
+ *
+ * <p> Once this method is called, the model can be safely used to satisfy recognition requests.
*/
void onSuccess();
/**
- * Called when {@link RecognitionService} scheduled the download but won't satisfy it
+ * Called when {@link RecognitionService} scheduled the download, but won't satisfy it
* immediately. There will be no further updates on this listener.
*/
void onScheduled();
diff --git a/core/java/android/speech/RecognitionService.java b/core/java/android/speech/RecognitionService.java
index 4ecec8f..9656f36 100644
--- a/core/java/android/speech/RecognitionService.java
+++ b/core/java/android/speech/RecognitionService.java
@@ -36,9 +36,7 @@
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
-import android.text.TextUtils;
import android.util.Log;
-import android.util.Pair;
import com.android.internal.util.function.pooled.PooledLambda;
@@ -93,10 +91,6 @@
private static final int MSG_TRIGGER_MODEL_DOWNLOAD = 6;
- private static final int MSG_SET_MODEL_DOWNLOAD_LISTENER = 7;
-
- private static final int MSG_CLEAR_MODEL_DOWNLOAD_LISTENER = 8;
-
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
@@ -120,21 +114,11 @@
checkArgs.mIntent, checkArgs.callback, checkArgs.mAttributionSource);
break;
case MSG_TRIGGER_MODEL_DOWNLOAD:
- Pair<Intent, AttributionSource> params =
- (Pair<Intent, AttributionSource>) msg.obj;
- dispatchTriggerModelDownload(params.first, params.second);
- break;
- case MSG_SET_MODEL_DOWNLOAD_LISTENER:
- ModelDownloadListenerArgs dListenerArgs = (ModelDownloadListenerArgs) msg.obj;
- dispatchSetModelDownloadListener(
- dListenerArgs.mIntent,
- dListenerArgs.mListener,
- dListenerArgs.mAttributionSource);
- break;
- case MSG_CLEAR_MODEL_DOWNLOAD_LISTENER:
- Pair<Intent, AttributionSource> clearDlPair =
- (Pair<Intent, AttributionSource>) msg.obj;
- dispatchClearModelDownloadListener(clearDlPair.first, clearDlPair.second);
+ ModelDownloadArgs modelDownloadArgs = (ModelDownloadArgs) msg.obj;
+ dispatchTriggerModelDownload(
+ modelDownloadArgs.mIntent,
+ modelDownloadArgs.mAttributionSource,
+ modelDownloadArgs.mListener);
break;
}
}
@@ -239,59 +223,52 @@
private void dispatchTriggerModelDownload(
Intent intent,
- AttributionSource attributionSource) {
- RecognitionService.this.onTriggerModelDownload(intent, attributionSource);
- }
-
- private void dispatchSetModelDownloadListener(
- Intent intent,
- IModelDownloadListener listener,
- AttributionSource attributionSource) {
- RecognitionService.this.setModelDownloadListener(
- intent,
- attributionSource,
- new ModelDownloadListener() {
- @Override
- public void onProgress(int completedPercent) {
- try {
- listener.onProgress(completedPercent);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ AttributionSource attributionSource,
+ IModelDownloadListener listener) {
+ if (listener == null) {
+ RecognitionService.this.onTriggerModelDownload(intent, attributionSource);
+ } else {
+ RecognitionService.this.onTriggerModelDownload(
+ intent,
+ attributionSource,
+ new ModelDownloadListener() {
+ @Override
+ public void onProgress(int completedPercent) {
+ try {
+ listener.onProgress(completedPercent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
- }
- @Override
- public void onSuccess() {
- try {
- listener.onSuccess();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ @Override
+ public void onSuccess() {
+ try {
+ listener.onSuccess();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
- }
- @Override
- public void onScheduled() {
- try {
- listener.onScheduled();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ @Override
+ public void onScheduled() {
+ try {
+ listener.onScheduled();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
- }
- @Override
- public void onError(int error) {
- try {
- listener.onError(error);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ @Override
+ public void onError(int error) {
+ try {
+ listener.onError(error);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
- }
- });
- }
-
- private void dispatchClearModelDownloadListener(
- Intent intent, AttributionSource attributionSource) {
- RecognitionService.this.clearModelDownloadListener(intent, attributionSource);
+ });
+ }
}
private static class StartListeningArgs {
@@ -323,17 +300,18 @@
}
}
- private static class ModelDownloadListenerArgs {
+ private static class ModelDownloadArgs {
final Intent mIntent;
- final IModelDownloadListener mListener;
final AttributionSource mAttributionSource;
+ @Nullable final IModelDownloadListener mListener;
- private ModelDownloadListenerArgs(Intent intent,
- IModelDownloadListener listener,
- AttributionSource attributionSource) {
- mIntent = intent;
+ private ModelDownloadArgs(
+ Intent intent,
+ AttributionSource attributionSource,
+ @Nullable IModelDownloadListener listener) {
+ this.mIntent = intent;
+ this.mAttributionSource = attributionSource;
this.mListener = listener;
- mAttributionSource = attributionSource;
}
}
@@ -443,38 +421,39 @@
}
/**
- * Sets a {@link ModelDownloadListener} to receive progress updates after
- * {@link #onTriggerModelDownload} calls.
+ * Requests the download of the recognizer support for {@code recognizerIntent}.
*
- * @param recognizerIntent the request to monitor model download progress for.
- * @param modelDownloadListener the listener to keep updated.
+ * <p> Provides the calling {@link AttributionSource} to the service implementation so that
+ * permissions and bandwidth could be correctly blamed.
+ *
+ * <p> Client will receive the progress updates via the given {@link ModelDownloadListener}:
+ *
+ * <li> If the model is already available, {@link ModelDownloadListener#onSuccess()} will be
+ * called directly. The model can be safely used afterwards.
+ *
+ * <li> If the {@link RecognitionService} has started the download,
+ * {@link ModelDownloadListener#onProgress(int)} will be called an unspecified (zero or more)
+ * number of times until the download is complete.
+ * When the download finishes, {@link ModelDownloadListener#onSuccess()} will be called.
+ * The model can be safely used afterwards.
+ *
+ * <li> If the {@link RecognitionService} has only scheduled the download, but won't satisfy it
+ * immediately, {@link ModelDownloadListener#onScheduled()} will be called.
+ * There will be no further updates on this listener.
+ *
+ * <li> If the request fails at any time due to a network or scheduling error,
+ * {@link ModelDownloadListener#onError(int)} will be called.
+ *
+ * @param recognizerIntent contains parameters for the recognition to be performed. The intent
+ * may also contain optional extras, see {@link RecognizerIntent}.
+ * @param attributionSource the attribution source of the caller.
+ * @param listener on which to receive updates about the model download request.
*/
- public void setModelDownloadListener(
+ public void onTriggerModelDownload(
@NonNull Intent recognizerIntent,
@NonNull AttributionSource attributionSource,
- @NonNull ModelDownloadListener modelDownloadListener) {
- if (DBG) {
- Log.i(TAG, TextUtils.formatSimple(
- "#setModelDownloadListener [%s] [%s]",
- recognizerIntent,
- modelDownloadListener));
- }
- modelDownloadListener.onError(SpeechRecognizer.ERROR_CANNOT_LISTEN_TO_DOWNLOAD_EVENTS);
- }
-
- /**
- * Clears the {@link ModelDownloadListener} set to receive progress updates for the given
- * {@code recognizerIntent}, if any.
- *
- * @param recognizerIntent the request to monitor model download progress for.
- */
- public void clearModelDownloadListener(
- @NonNull Intent recognizerIntent,
- @NonNull AttributionSource attributionSource) {
- if (DBG) {
- Log.i(TAG, TextUtils.formatSimple(
- "#clearModelDownloadListener [%s]", recognizerIntent));
- }
+ @NonNull ModelDownloadListener listener) {
+ listener.onError(SpeechRecognizer.ERROR_CANNOT_LISTEN_TO_DOWNLOAD_EVENTS);
}
@Override
@@ -815,41 +794,18 @@
@Override
public void triggerModelDownload(
- Intent recognizerIntent, @NonNull AttributionSource attributionSource) {
+ Intent recognizerIntent,
+ @NonNull AttributionSource attributionSource,
+ IModelDownloadListener listener) {
final RecognitionService service = mServiceRef.get();
if (service != null) {
service.mHandler.sendMessage(
Message.obtain(
service.mHandler, MSG_TRIGGER_MODEL_DOWNLOAD,
- Pair.create(recognizerIntent, attributionSource)));
- }
- }
-
- @Override
- public void setModelDownloadListener(
- Intent recognizerIntent,
- AttributionSource attributionSource,
- IModelDownloadListener listener) throws RemoteException {
- final RecognitionService service = mServiceRef.get();
- if (service != null) {
- service.mHandler.sendMessage(
- Message.obtain(service.mHandler, MSG_SET_MODEL_DOWNLOAD_LISTENER,
- new ModelDownloadListenerArgs(
+ new ModelDownloadArgs(
recognizerIntent,
- listener,
- attributionSource)));
- }
- }
-
- @Override
- public void clearModelDownloadListener(
- Intent recognizerIntent,
- AttributionSource attributionSource) throws RemoteException {
- final RecognitionService service = mServiceRef.get();
- if (service != null) {
- service.mHandler.sendMessage(
- Message.obtain(service.mHandler, MSG_CLEAR_MODEL_DOWNLOAD_LISTENER,
- Pair.create(recognizerIntent, attributionSource)));
+ attributionSource,
+ listener)));
}
}
diff --git a/core/java/android/speech/SpeechRecognizer.java b/core/java/android/speech/SpeechRecognizer.java
index 76eb09e..dacb25c 100644
--- a/core/java/android/speech/SpeechRecognizer.java
+++ b/core/java/android/speech/SpeechRecognizer.java
@@ -297,8 +297,6 @@
private static final int MSG_SET_TEMPORARY_ON_DEVICE_COMPONENT = 5;
private static final int MSG_CHECK_RECOGNITION_SUPPORT = 6;
private static final int MSG_TRIGGER_MODEL_DOWNLOAD = 7;
- private static final int MSG_SET_MODEL_DOWNLOAD_LISTENER = 8;
- private static final int MSG_CLEAR_MODEL_DOWNLOAD_LISTENER = 9;
/** The actual RecognitionService endpoint */
private IRecognitionService mService;
@@ -341,19 +339,13 @@
args.mIntent, args.mCallbackExecutor, args.mCallback);
break;
case MSG_TRIGGER_MODEL_DOWNLOAD:
- handleTriggerModelDownload((Intent) msg.obj);
- break;
- case MSG_SET_MODEL_DOWNLOAD_LISTENER:
ModelDownloadListenerArgs modelDownloadListenerArgs =
(ModelDownloadListenerArgs) msg.obj;
- handleSetModelDownloadListener(
+ handleTriggerModelDownload(
modelDownloadListenerArgs.mIntent,
modelDownloadListenerArgs.mExecutor,
modelDownloadListenerArgs.mModelDownloadListener);
break;
- case MSG_CLEAR_MODEL_DOWNLOAD_LISTENER:
- handleClearModelDownloadListener((Intent) msg.obj);
- break;
}
}
};
@@ -657,17 +649,13 @@
* user interaction to approve the download. Callers can verify the status of the request via
* {@link #checkRecognitionSupport(Intent, Executor, RecognitionSupportCallback)}.
*
- * <p>Listeners set via
- * {@link #setModelDownloadListener(Intent, Executor, ModelDownloadListener)} will receive
- * updates about this download request.</p>
- *
* @param recognizerIntent contains parameters for the recognition to be performed. The intent
* may also contain optional extras, see {@link RecognizerIntent}.
*/
public void triggerModelDownload(@NonNull Intent recognizerIntent) {
Objects.requireNonNull(recognizerIntent, "intent must not be null");
if (DBG) {
- Slog.i(TAG, "#triggerModelDownload called");
+ Slog.i(TAG, "#triggerModelDownload without a listener called");
if (mService == null) {
Slog.i(TAG, "Connection is not established yet");
}
@@ -676,23 +664,47 @@
// First time connection: first establish a connection, then dispatch.
connectToSystemService();
}
- putMessage(Message.obtain(mHandler, MSG_TRIGGER_MODEL_DOWNLOAD, recognizerIntent));
+ putMessage(Message.obtain(
+ mHandler, MSG_TRIGGER_MODEL_DOWNLOAD,
+ new ModelDownloadListenerArgs(recognizerIntent, null, null)));
}
/**
- * Sets a listener to model download updates. Clients will have to call this method before
- * {@link #triggerModelDownload(Intent)}.
+ * Attempts to download the support for the given {@code recognizerIntent}. This might trigger
+ * user interaction to approve the download. Callers can verify the status of the request via
+ * {@link #checkRecognitionSupport(Intent, Executor, RecognitionSupportCallback)}.
*
- * @param recognizerIntent the request to monitor support for.
+ * <p> The updates about the model download request are received via the given
+ * {@link ModelDownloadListener}:
+ *
+ * <li> If the model is already available, {@link ModelDownloadListener#onSuccess()} will be
+ * called directly. The model can be safely used afterwards.
+ *
+ * <li> If the {@link RecognitionService} has started the download,
+ * {@link ModelDownloadListener#onProgress(int)} will be called an unspecified (zero or more)
+ * number of times until the download is complete.
+ * When the download finishes, {@link ModelDownloadListener#onSuccess()} will be called.
+ * The model can be safely used afterwards.
+ *
+ * <li> If the {@link RecognitionService} has only scheduled the download, but won't satisfy it
+ * immediately, {@link ModelDownloadListener#onScheduled()} will be called.
+ * There will be no further updates on this listener.
+ *
+ * <li> If the request fails at any time due to a network or scheduling error,
+ * {@link ModelDownloadListener#onError(int)} will be called.
+ *
+ * @param recognizerIntent contains parameters for the recognition to be performed. The intent
+ * may also contain optional extras, see {@link RecognizerIntent}.
+ * @param executor for dispatching listener callbacks
* @param listener on which to receive updates about the model download request.
*/
- public void setModelDownloadListener(
+ public void triggerModelDownload(
@NonNull Intent recognizerIntent,
@NonNull @CallbackExecutor Executor executor,
@NonNull ModelDownloadListener listener) {
Objects.requireNonNull(recognizerIntent, "intent must not be null");
if (DBG) {
- Slog.i(TAG, "#setModelDownloadListener called");
+ Slog.i(TAG, "#triggerModelDownload with a listener called");
if (mService == null) {
Slog.i(TAG, "Connection is not established yet");
}
@@ -702,32 +714,11 @@
connectToSystemService();
}
putMessage(Message.obtain(
- mHandler, MSG_SET_MODEL_DOWNLOAD_LISTENER,
+ mHandler, MSG_TRIGGER_MODEL_DOWNLOAD,
new ModelDownloadListenerArgs(recognizerIntent, executor, listener)));
}
/**
- * Clears the listener for model download updates if any.
- *
- * @param recognizerIntent the request to monitor support for.
- */
- public void clearModelDownloadListener(@NonNull Intent recognizerIntent) {
- Objects.requireNonNull(recognizerIntent, "intent must not be null");
- if (DBG) {
- Slog.i(TAG, "#clearModelDownloadListener called");
- if (mService == null) {
- Slog.i(TAG, "Connection is not established yet");
- }
- }
- if (mService == null) {
- // First time connection: first establish a connection, then dispatch.
- connectToSystemService();
- }
- putMessage(Message.obtain(
- mHandler, MSG_CLEAR_MODEL_DOWNLOAD_LISTENER, recognizerIntent));
- }
-
- /**
* Sets a temporary component to power on-device speech recognizer.
*
* <p>This is only expected to be called in tests, system would reject calls from client apps.
@@ -836,51 +827,36 @@
}
}
- private void handleTriggerModelDownload(Intent recognizerIntent) {
- if (!maybeInitializeManagerService()) {
- return;
- }
- try {
- mService.triggerModelDownload(recognizerIntent, mContext.getAttributionSource());
- } catch (final RemoteException e) {
- Log.e(TAG, "downloadModel() failed", e);
- mListener.onError(ERROR_CLIENT);
- }
- }
-
- private void handleSetModelDownloadListener(
+ private void handleTriggerModelDownload(
Intent recognizerIntent,
- Executor callbackExecutor,
+ @Nullable Executor callbackExecutor,
@Nullable ModelDownloadListener modelDownloadListener) {
if (!maybeInitializeManagerService()) {
return;
}
- try {
- InternalModelDownloadListener listener =
- modelDownloadListener == null
- ? null
- : new InternalModelDownloadListener(
- callbackExecutor,
- modelDownloadListener);
- mService.setModelDownloadListener(
- recognizerIntent, mContext.getAttributionSource(), listener);
- if (DBG) Log.d(TAG, "setModelDownloadListener()");
- } catch (final RemoteException e) {
- Log.e(TAG, "setModelDownloadListener() failed", e);
- callbackExecutor.execute(() -> modelDownloadListener.onError(ERROR_CLIENT));
- }
- }
- private void handleClearModelDownloadListener(Intent recognizerIntent) {
- if (!maybeInitializeManagerService()) {
- return;
+ // Trigger model download without a listener.
+ if (modelDownloadListener == null) {
+ try {
+ mService.triggerModelDownload(
+ recognizerIntent, mContext.getAttributionSource(), null);
+ if (DBG) Log.d(TAG, "triggerModelDownload() without a listener");
+ } catch (final RemoteException e) {
+ Log.e(TAG, "triggerModelDownload() without a listener failed", e);
+ mListener.onError(ERROR_CLIENT);
+ }
}
- try {
- mService.clearModelDownloadListener(
- recognizerIntent, mContext.getAttributionSource());
- if (DBG) Log.d(TAG, "clearModelDownloadListener()");
- } catch (final RemoteException e) {
- Log.e(TAG, "clearModelDownloadListener() failed", e);
+ // Trigger model download with a listener.
+ else {
+ try {
+ mService.triggerModelDownload(
+ recognizerIntent, mContext.getAttributionSource(),
+ new InternalModelDownloadListener(callbackExecutor, modelDownloadListener));
+ if (DBG) Log.d(TAG, "triggerModelDownload() with a listener");
+ } catch (final RemoteException e) {
+ Log.e(TAG, "triggerModelDownload() with a listener failed", e);
+ callbackExecutor.execute(() -> modelDownloadListener.onError(ERROR_CLIENT));
+ }
}
}
diff --git a/core/java/android/text/TextFlags.java b/core/java/android/text/TextFlags.java
new file mode 100644
index 0000000..9f11e31
--- /dev/null
+++ b/core/java/android/text/TextFlags.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+/**
+ * Flags in the "text" namespace.
+ *
+ * @hide
+ */
+public final class TextFlags {
+
+ /**
+ * The name space of the "text" feature.
+ *
+ * This needs to move to DeviceConfig constant.
+ */
+ public static final String NAMESPACE = "text";
+
+ /**
+ * Whether we use the new design of context menu.
+ */
+ public static final String ENABLE_NEW_CONTEXT_MENU =
+ "TextEditing__enable_new_context_menu";
+
+ /**
+ * The key name used in app core settings for {@link #ENABLE_NEW_CONTEXT_MENU}.
+ */
+ public static final String KEY_ENABLE_NEW_CONTEXT_MENU = "text__enable_new_context_menu";
+
+ /**
+ * Default value for the flag {@link #ENABLE_NEW_CONTEXT_MENU}.
+ */
+ public static final boolean ENABLE_NEW_CONTEXT_MENU_DEFAULT = false;
+
+}
diff --git a/core/java/android/text/method/QwertyKeyListener.java b/core/java/android/text/method/QwertyKeyListener.java
index b4a1e8c..c43864d 100644
--- a/core/java/android/text/method/QwertyKeyListener.java
+++ b/core/java/android/text/method/QwertyKeyListener.java
@@ -362,6 +362,15 @@
return true;
}
+ } else if (keyCode == KeyEvent.KEYCODE_ESCAPE && event.hasNoModifiers()) {
+ // If user is in the process of composing with a dead key, and
+ // presses Escape, cancel it. We need special handling because
+ // the Escape key will not produce a Unicode character
+ if (activeStart == selStart && activeEnd == selEnd) {
+ Selection.setSelection(content, selEnd);
+ content.removeSpan(TextKeyListener.ACTIVE);
+ return true;
+ }
}
return super.onKeyDown(view, content, keyCode, event);
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index 2ae882c..6201b3a 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -220,9 +220,9 @@
DEFAULT_FLAGS.put(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME, "true");
DEFAULT_FLAGS.put(SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, "true");
DEFAULT_FLAGS.put(SETTINGS_AUTO_TEXT_WRAPPING, "false");
- DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_UI, "false");
- DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY, "false");
- DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD, "false");
+ DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_UI, "true");
+ DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY, "true");
+ DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD, "true");
DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE, "false");
DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA, "true");
DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA_PHASE2, "false");
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index a47783c..6b60442 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -205,6 +205,16 @@
}
/**
+ * Notify HandwritingInitiator that a delegate view (see {@link View#isHandwritingDelegate})
+ * gained focus.
+ */
+ public void onDelegateViewFocused(@NonNull View view) {
+ if (view == getConnectedView()) {
+ tryAcceptStylusHandwritingDelegation(view);
+ }
+ }
+
+ /**
* Notify HandwritingInitiator that a new InputConnection is created.
* The caller of this method should guarantee that each onInputConnectionCreated call
* is paired with a onInputConnectionClosed call.
diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java
index 720f569..9225cd9 100644
--- a/core/java/android/view/InputDevice.java
+++ b/core/java/android/view/InputDevice.java
@@ -29,6 +29,7 @@
import android.hardware.input.HostUsiVersion;
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
+import android.hardware.input.InputManagerGlobal;
import android.hardware.lights.LightsManager;
import android.icu.util.ULocale;
import android.os.Build;
@@ -742,7 +743,7 @@
*/
@Nullable
public static InputDevice getDevice(int id) {
- return InputManager.getInstance().getInputDevice(id);
+ return InputManagerGlobal.getInstance().getInputDevice(id);
}
/**
@@ -750,7 +751,7 @@
* @return The input device ids.
*/
public static int[] getDeviceIds() {
- return InputManager.getInstance().getInputDeviceIds();
+ return InputManagerGlobal.getInstance().getInputDeviceIds();
}
/**
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 800fc97..aec3487 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -8324,6 +8324,13 @@
onFocusLost();
} else if (hasWindowFocus()) {
notifyFocusChangeToImeFocusController(true /* hasFocus */);
+
+ if (mIsHandwritingDelegate) {
+ ViewRootImpl viewRoot = getViewRootImpl();
+ if (viewRoot != null) {
+ viewRoot.getHandwritingInitiator().onDelegateViewFocused(this);
+ }
+ }
}
invalidate(true);
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index c96d298..d80819f 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -28,6 +28,7 @@
import android.content.res.Resources;
import android.graphics.Rect;
import android.hardware.input.InputManager;
+import android.hardware.input.InputManagerGlobal;
import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
@@ -1188,7 +1189,7 @@
}
private static boolean isInputDeviceInfoValid(int id, int axis, int source) {
- InputDevice device = InputManager.getInstance().getInputDevice(id);
+ InputDevice device = InputManagerGlobal.getInstance().getInputDevice(id);
return device != null && device.getMotionRange(axis, source) != null;
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 24dcb69..fb25e7a6 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -11626,7 +11626,8 @@
mNumPausedForSync++;
mHandler.removeMessages(MSG_PAUSED_FOR_SYNC_TIMEOUT);
- mHandler.sendEmptyMessageDelayed(MSG_PAUSED_FOR_SYNC_TIMEOUT, 1000);
+ mHandler.sendEmptyMessageDelayed(MSG_PAUSED_FOR_SYNC_TIMEOUT,
+ 1000 * Build.HW_TIMEOUT_MULTIPLIER);
return mActiveSurfaceSyncGroup;
};
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 9f9a781..dce5432 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -74,6 +74,7 @@
import android.text.Spanned;
import android.text.SpannedString;
import android.text.StaticLayout;
+import android.text.TextFlags;
import android.text.TextUtils;
import android.text.method.InsertModeTransformationMethod;
import android.text.method.KeyListener;
@@ -169,9 +170,6 @@
private static final String TAG = "Editor";
private static final boolean DEBUG_UNDO = false;
- // TODO(nona): Make this configurable.
- private static final boolean FLAG_USE_NEW_CONTEXT_MENU = false;
-
// Specifies whether to use the magnifier when pressing the insertion or selection handles.
private static final boolean FLAG_USE_MAGNIFIER = true;
@@ -470,6 +468,7 @@
private static final int LINE_CHANGE_SLOP_MIN_DP = 8;
private int mLineChangeSlopMax;
private int mLineChangeSlopMin;
+ private boolean mUseNewContextMenu;
private final AccessibilitySmartActions mA11ySmartActions;
private InsertModeController mInsertModeController;
@@ -500,6 +499,9 @@
mLineSlopRatio = AppGlobals.getFloatCoreSetting(
WidgetFlags.KEY_LINE_SLOP_RATIO,
WidgetFlags.LINE_SLOP_RATIO_DEFAULT);
+ mUseNewContextMenu = AppGlobals.getIntCoreSetting(
+ TextFlags.KEY_ENABLE_NEW_CONTEXT_MENU,
+ TextFlags.ENABLE_NEW_CONTEXT_MENU_DEFAULT ? 1 : 0) != 0;
if (TextView.DEBUG_CURSOR) {
logCursor("Editor", "Cursor drag from anywhere is %s.",
mFlagCursorDragFromAnywhereEnabled ? "enabled" : "disabled");
@@ -3171,7 +3173,7 @@
final int menuItemOrderSelectAll;
final int menuItemOrderShare;
final int menuItemOrderAutofill;
- if (FLAG_USE_NEW_CONTEXT_MENU) {
+ if (mUseNewContextMenu) {
menuItemOrderPasteAsPlainText = 7;
menuItemOrderSelectAll = 8;
menuItemOrderShare = 9;
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 1600a16..fd80981 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -462,12 +462,12 @@
private static final int CHANGE_WATCHER_PRIORITY = 100;
/**
- * The span priority of the {@link TransformationMethod} that is set on the text. It must be
+ * The span priority of the {@link OffsetMapping} that is set on the text. It must be
* higher than the {@link DynamicLayout}'s {@link TextWatcher}, so that the transformed text is
* updated before {@link DynamicLayout#reflow(CharSequence, int, int, int)} being triggered
* by {@link TextWatcher#onTextChanged(CharSequence, int, int, int)}.
*/
- private static final int TRANSFORMATION_SPAN_PRIORITY = 200;
+ private static final int OFFSET_MAPPING_SPAN_PRIORITY = 200;
// New state used to change background based on whether this TextView is multiline.
private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
@@ -7033,9 +7033,9 @@
}
final int textLength = text.length();
+ final boolean isOffsetMapping = mTransformed instanceof OffsetMapping;
- if (text instanceof Spannable && (!mAllowTransformationLengthChange
- || text instanceof OffsetMapping)) {
+ if (text instanceof Spannable && (!mAllowTransformationLengthChange || isOffsetMapping)) {
Spannable sp = (Spannable) text;
// Remove any ChangeWatchers that might have come from other TextViews.
@@ -7053,8 +7053,9 @@
if (mEditor != null) mEditor.addSpanWatchers(sp);
if (mTransformation != null) {
+ final int priority = isOffsetMapping ? OFFSET_MAPPING_SPAN_PRIORITY : 0;
sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE
- | (TRANSFORMATION_SPAN_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
+ | (priority << Spanned.SPAN_PRIORITY_SHIFT));
}
if (mMovement != null) {
diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java
index 071c20f..52e17ca 100644
--- a/core/java/android/window/SnapshotDrawerUtils.java
+++ b/core/java/android/window/SnapshotDrawerUtils.java
@@ -34,6 +34,7 @@
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_USE_BLAST;
@@ -223,6 +224,11 @@
PixelFormat.RGBA_8888,
GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_HW_COMPOSER
| GraphicBuffer.USAGE_SW_WRITE_RARELY);
+ if (background == null) {
+ Log.e(TAG, "Unable to draw snapshot: failed to allocate graphic buffer for "
+ + mTitle);
+ return;
+ }
// TODO: Support this on HardwareBuffer
final Canvas c = background.lockCanvas();
drawBackgroundAndBars(c, frame);
@@ -410,6 +416,7 @@
layoutParams.setFitInsetsIgnoringVisibility(attrs.isFitInsetsIgnoringVisibility());
layoutParams.setTitle(title);
+ layoutParams.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL;
return layoutParams;
}
diff --git a/core/java/android/window/SurfaceSyncGroup.java b/core/java/android/window/SurfaceSyncGroup.java
index 0672d63..7f99fb7 100644
--- a/core/java/android/window/SurfaceSyncGroup.java
+++ b/core/java/android/window/SurfaceSyncGroup.java
@@ -21,6 +21,7 @@
import android.annotation.UiThread;
import android.os.Binder;
import android.os.BinderProxy;
+import android.os.Build;
import android.os.Debug;
import android.os.Handler;
import android.os.HandlerThread;
@@ -62,7 +63,7 @@
private static final int MAX_COUNT = 100;
private static final AtomicInteger sCounter = new AtomicInteger(0);
- private static final int TRANSACTION_READY_TIMEOUT = 1000;
+ private static final int TRANSACTION_READY_TIMEOUT = 1000 * Build.HW_TIMEOUT_MULTIPLIER;
private static Supplier<Transaction> sTransactionFactory = Transaction::new;
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index b8bd703..4c482460 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -24,6 +24,7 @@
import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN;
import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP;
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
@@ -58,6 +59,7 @@
* @hide
*/
public final class TransitionInfo implements Parcelable {
+ private static final String TAG = "TransitionInfo";
/**
* Modes are only a sub-set of all the transit-types since they are per-container
@@ -184,9 +186,7 @@
private final @TransitionType int mType;
private final @TransitionFlags int mFlags;
private final ArrayList<Change> mChanges = new ArrayList<>();
-
- private SurfaceControl mRootLeash;
- private final Point mRootOffset = new Point();
+ private final ArrayList<Root> mRoots = new ArrayList<>();
private AnimationOptions mOptions;
@@ -200,10 +200,7 @@
mType = in.readInt();
mFlags = in.readInt();
in.readTypedList(mChanges, Change.CREATOR);
- mRootLeash = new SurfaceControl();
- mRootLeash.readFromParcel(in);
- mRootLeash.setUnreleasedWarningCallSite("TransitionInfo");
- mRootOffset.readFromParcel(in);
+ in.readTypedList(mRoots, Root.CREATOR);
mOptions = in.readTypedObject(AnimationOptions.CREATOR);
}
@@ -213,8 +210,7 @@
dest.writeInt(mType);
dest.writeInt(mFlags);
dest.writeTypedList(mChanges);
- mRootLeash.writeToParcel(dest, flags);
- mRootOffset.writeToParcel(dest, flags);
+ dest.writeTypedList(mRoots, flags);
dest.writeTypedObject(mOptions, flags);
}
@@ -238,10 +234,15 @@
return 0;
}
- /** @see #getRootLeash() */
- public void setRootLeash(@NonNull SurfaceControl leash, int offsetLeft, int offsetTop) {
- mRootLeash = leash;
- mRootOffset.set(offsetLeft, offsetTop);
+ /** @see #getRoot */
+ public void addRootLeash(int displayId, @NonNull SurfaceControl leash,
+ int offsetLeft, int offsetTop) {
+ mRoots.add(new Root(displayId, leash, offsetLeft, offsetTop));
+ }
+
+ /** @see #getRoot */
+ public void addRoot(Root other) {
+ mRoots.add(other);
}
public void setAnimationOptions(AnimationOptions options) {
@@ -257,23 +258,52 @@
}
/**
+ * @return The number of animation roots. Most transitions should have 1, but there may be more
+ * in some cases (such as a transition spanning multiple displays).
+ */
+ public int getRootCount() {
+ return mRoots.size();
+ }
+
+ /**
+ * @return the transition-root at a specific index.
+ */
+ @NonNull
+ public Root getRoot(int idx) {
+ return mRoots.get(idx);
+ }
+
+ /**
+ * @return the index of the transition-root associated with `displayId` or -1 if not found.
+ */
+ public int findRootIndex(int displayId) {
+ for (int i = 0; i < mRoots.size(); ++i) {
+ if (mRoots.get(i).mDisplayId == displayId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
* @return a surfacecontrol that can serve as a parent surfacecontrol for all the changing
* participants to animate within. This will generally be placed at the highest-z-order
* shared ancestor of all participants. While this is non-null, it's possible for the rootleash
* to be invalid if the transition is a no-op.
+ *
+ * @deprecated Use {@link #getRoot} instead. This call assumes there is only one root.
*/
+ @Deprecated
@NonNull
public SurfaceControl getRootLeash() {
- if (mRootLeash == null) {
- throw new IllegalStateException("Trying to get a leash which wasn't set");
+ if (mRoots.isEmpty()) {
+ throw new IllegalStateException("Trying to get a root leash from a no-op transition.");
}
- return mRootLeash;
- }
-
- /** @return the offset (relative to the screen) of the root leash. */
- @NonNull
- public Point getRootOffset() {
- return mRootOffset;
+ if (mRoots.size() > 1) {
+ android.util.Log.e(TAG, "Assuming one animation root when there are more.",
+ new Throwable());
+ }
+ return mRoots.get(0).mLeash;
}
public AnimationOptions getAnimationOptions() {
@@ -320,8 +350,15 @@
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
- sb.append("{t=" + transitTypeToString(mType) + " f=0x" + Integer.toHexString(mFlags)
- + " ro=" + mRootOffset + " c=[");
+ sb.append("{t=").append(transitTypeToString(mType)).append(" f=0x")
+ .append(Integer.toHexString(mFlags)).append(" r=[");
+ for (int i = 0; i < mRoots.size(); ++i) {
+ if (i > 0) {
+ sb.append(',');
+ }
+ sb.append(mRoots.get(i).mDisplayId).append("@").append(mRoots.get(i).mOffset);
+ }
+ sb.append("] c=[");
for (int i = 0; i < mChanges.size(); ++i) {
if (i > 0) {
sb.append(',');
@@ -448,8 +485,8 @@
c.mSnapshot = null;
}
}
- if (mRootLeash != null) {
- mRootLeash.release();
+ for (int i = 0; i < mRoots.size(); ++i) {
+ mRoots.get(i).mLeash.release();
}
}
@@ -476,10 +513,11 @@
for (int i = 0; i < mChanges.size(); ++i) {
out.mChanges.add(mChanges.get(i).localRemoteCopy());
}
- out.mRootLeash = mRootLeash != null ? new SurfaceControl(mRootLeash, "localRemote") : null;
+ for (int i = 0; i < mRoots.size(); ++i) {
+ out.mRoots.add(mRoots.get(i).localRemoteCopy());
+ }
// Doesn't have any native stuff, so no need for actual copy
out.mOptions = mOptions;
- out.mRootOffset.set(mRootOffset);
return out;
}
@@ -496,6 +534,8 @@
private final Point mEndRelOffset = new Point();
private ActivityManager.RunningTaskInfo mTaskInfo = null;
private boolean mAllowEnterPip;
+ private int mStartDisplayId = INVALID_DISPLAY;
+ private int mEndDisplayId = INVALID_DISPLAY;
private @Surface.Rotation int mStartRotation = ROTATION_UNDEFINED;
private @Surface.Rotation int mEndRotation = ROTATION_UNDEFINED;
/**
@@ -526,6 +566,8 @@
mEndRelOffset.readFromParcel(in);
mTaskInfo = in.readTypedObject(ActivityManager.RunningTaskInfo.CREATOR);
mAllowEnterPip = in.readBoolean();
+ mStartDisplayId = in.readInt();
+ mEndDisplayId = in.readInt();
mStartRotation = in.readInt();
mEndRotation = in.readInt();
mEndFixedRotation = in.readInt();
@@ -546,6 +588,8 @@
out.mEndRelOffset.set(mEndRelOffset);
out.mTaskInfo = mTaskInfo;
out.mAllowEnterPip = mAllowEnterPip;
+ out.mStartDisplayId = mStartDisplayId;
+ out.mEndDisplayId = mEndDisplayId;
out.mStartRotation = mStartRotation;
out.mEndRotation = mEndRotation;
out.mEndFixedRotation = mEndFixedRotation;
@@ -608,6 +652,12 @@
}
/** Sets the start and end rotation of this container. */
+ public void setDisplayId(int start, int end) {
+ mStartDisplayId = start;
+ mEndDisplayId = end;
+ }
+
+ /** Sets the start and end rotation of this container. */
public void setRotation(@Surface.Rotation int start, @Surface.Rotation int end) {
mStartRotation = start;
mEndRotation = end;
@@ -725,6 +775,14 @@
return mAllowEnterPip;
}
+ public int getStartDisplayId() {
+ return mStartDisplayId;
+ }
+
+ public int getEndDisplayId() {
+ return mEndDisplayId;
+ }
+
@Surface.Rotation
public int getStartRotation() {
return mStartRotation;
@@ -776,6 +834,8 @@
mEndRelOffset.writeToParcel(dest, flags);
dest.writeTypedObject(mTaskInfo, flags);
dest.writeBoolean(mAllowEnterPip);
+ dest.writeInt(mStartDisplayId);
+ dest.writeInt(mEndDisplayId);
dest.writeInt(mStartRotation);
dest.writeInt(mEndRotation);
dest.writeInt(mEndFixedRotation);
@@ -822,6 +882,11 @@
if (mEndRelOffset.x != 0 || mEndRelOffset.y != 0) {
sb.append(" eo="); sb.append(mEndRelOffset);
}
+ sb.append(" d=");
+ if (mStartDisplayId != mEndDisplayId) {
+ sb.append(mStartDisplayId).append("->");
+ }
+ sb.append(mEndDisplayId);
if (mStartRotation != mEndRotation) {
sb.append(" r="); sb.append(mStartRotation);
sb.append("->"); sb.append(mEndRotation);
@@ -1108,4 +1173,86 @@
};
}
}
+
+ /**
+ * An animation root in a transition. There is one of these for each display that contains
+ * participants. It will be placed, in z-order, right above the top-most participant and at the
+ * same position in the hierarchy. As a result, if all participants are animating within a
+ * part of the screen, the root-leash will only be in that part of the screen. In these cases,
+ * it's relative position (from the screen) is stored in {@link Root#getOffset}.
+ */
+ public static final class Root implements Parcelable {
+ private final int mDisplayId;
+ private final SurfaceControl mLeash;
+ private final Point mOffset = new Point();
+
+ public Root(int displayId, @NonNull SurfaceControl leash, int offsetLeft, int offsetTop) {
+ mDisplayId = displayId;
+ mLeash = leash;
+ mOffset.set(offsetLeft, offsetTop);
+ }
+
+ private Root(Parcel in) {
+ mDisplayId = in.readInt();
+ mLeash = new SurfaceControl();
+ mLeash.readFromParcel(in);
+ mLeash.setUnreleasedWarningCallSite("TransitionInfo.Root");
+ mOffset.readFromParcel(in);
+ }
+
+ private Root localRemoteCopy() {
+ return new Root(mDisplayId, new SurfaceControl(mLeash, "localRemote"),
+ mOffset.x, mOffset.y);
+ }
+
+ /** @return the id of the display this root is on. */
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /** @return the root's leash. Surfaces should be parented to this while animating. */
+ @NonNull
+ public SurfaceControl getLeash() {
+ return mLeash;
+ }
+
+ /** @return the offset (relative to its screen) of the root leash. */
+ @NonNull
+ public Point getOffset() {
+ return mOffset;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mDisplayId);
+ mLeash.writeToParcel(dest, flags);
+ mOffset.writeToParcel(dest, flags);
+ }
+
+ @NonNull
+ public static final Creator<Root> CREATOR =
+ new Creator<Root>() {
+ @Override
+ public Root createFromParcel(Parcel in) {
+ return new Root(in);
+ }
+
+ @Override
+ public Root[] newArray(int size) {
+ return new Root[size];
+ }
+ };
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return mDisplayId + "@" + mOffset + ":" + mLeash;
+ }
+ }
}
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index a3209f6..fabb089 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -131,6 +131,19 @@
}
/**
+ * Sets the densityDpi value in the configuration for the given container.
+ * @hide
+ */
+ @NonNull
+ public WindowContainerTransaction setDensityDpi(@NonNull WindowContainerToken container,
+ int densityDpi) {
+ Change chg = getOrCreateChange(container.asBinder());
+ chg.mConfiguration.densityDpi = densityDpi;
+ chg.mConfigSetMask |= ActivityInfo.CONFIG_DENSITY;
+ return this;
+ }
+
+ /**
* Notify {@link com.android.server.wm.PinnedTaskController} that the picture-in-picture task
* has finished the enter animation with the given bounds.
*/
diff --git a/core/java/android/window/WindowInfosListenerForTest.java b/core/java/android/window/WindowInfosListenerForTest.java
index 429156f..01e577f 100644
--- a/core/java/android/window/WindowInfosListenerForTest.java
+++ b/core/java/android/window/WindowInfosListenerForTest.java
@@ -63,10 +63,17 @@
@NonNull
public final Rect bounds;
- WindowInfo(@NonNull IBinder windowToken, @NonNull String name, @NonNull Rect bounds) {
+ /**
+ * True if the window is a trusted overlay.
+ */
+ public final boolean isTrustedOverlay;
+
+ WindowInfo(@NonNull IBinder windowToken, @NonNull String name, @NonNull Rect bounds,
+ int inputConfig) {
this.windowToken = windowToken;
this.name = name;
this.bounds = bounds;
+ this.isTrustedOverlay = (inputConfig & InputConfig.TRUSTED_OVERLAY) != 0;
}
}
@@ -129,7 +136,8 @@
}
var bounds = new Rect(handle.frameLeft, handle.frameTop, handle.frameRight,
handle.frameBottom);
- windowInfos.add(new WindowInfo(handle.getWindowToken(), handle.name, bounds));
+ windowInfos.add(new WindowInfo(handle.getWindowToken(), handle.name, bounds,
+ handle.inputConfig));
}
return windowInfos;
}
diff --git a/core/java/com/android/internal/app/LocaleHelper.java b/core/java/com/android/internal/app/LocaleHelper.java
index 57bd3f9..d521866 100644
--- a/core/java/com/android/internal/app/LocaleHelper.java
+++ b/core/java/com/android/internal/app/LocaleHelper.java
@@ -20,6 +20,7 @@
import android.compat.annotation.UnsupportedAppUsage;
import android.icu.text.CaseMap;
import android.icu.text.ListFormatter;
+import android.icu.text.NumberingSystem;
import android.icu.util.ULocale;
import android.os.LocaleList;
import android.text.TextUtils;
@@ -173,6 +174,21 @@
}
/**
+ * Returns numbering system value of a locale for display in the provided locale.
+ *
+ * @param locale The locale whose key value is displayed.
+ * @param displayLocale The locale in which to display the key value.
+ * @return The string of numbering system.
+ */
+ public static String getDisplayNumberingSystemKeyValue(
+ Locale locale, Locale displayLocale) {
+ ULocale uLocale = new ULocale.Builder()
+ .setUnicodeLocaleKeyword("nu", NumberingSystem.getInstance(locale).getName())
+ .build();
+ return uLocale.getDisplayKeywordValue("numbers", ULocale.forLocale(displayLocale));
+ }
+
+ /**
* Adds the likely subtags for a provided locale ID.
*
* @param locale the locale to maximize.
diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java
index 685bd9a..5dfc0ea 100644
--- a/core/java/com/android/internal/app/LocalePickerWithRegion.java
+++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java
@@ -61,6 +61,7 @@
private int mTopDistance = 0;
private CharSequence mTitle = null;
private OnActionExpandListener mOnActionExpandListener;
+ private boolean mIsNumberingSystem = false;
/**
* Other classes can register to be notified when a locale was selected.
@@ -90,6 +91,18 @@
boolean hasSpecificPackageName();
}
+ private static LocalePickerWithRegion createNumberingSystemPicker(
+ LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
+ boolean translatedOnly, OnActionExpandListener onActionExpandListener,
+ LocaleCollectorBase localePickerCollector) {
+ LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
+ localePicker.setOnActionExpandListener(onActionExpandListener);
+ localePicker.setIsNumberingSystem(true);
+ boolean shouldShowTheList = localePicker.setListener(listener, parent,
+ translatedOnly, localePickerCollector);
+ return shouldShowTheList ? localePicker : null;
+ }
+
private static LocalePickerWithRegion createCountryPicker(
LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
boolean translatedOnly, OnActionExpandListener onActionExpandListener,
@@ -128,6 +141,10 @@
return localePicker;
}
+ private void setIsNumberingSystem(boolean isNumberingSystem) {
+ mIsNumberingSystem = isNumberingSystem;
+ }
+
/**
* Sets the listener and initializes the locale list.
*
@@ -184,6 +201,7 @@
final boolean hasSpecificPackageName =
mLocalePickerCollector != null && mLocalePickerCollector.hasSpecificPackageName();
mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode, hasSpecificPackageName);
+ mAdapter.setNumberingSystemMode(mIsNumberingSystem);
final LocaleHelper.LocaleInfoComparator comp =
new LocaleHelper.LocaleInfoComparator(sortingLocale, countryMode);
mAdapter.sort(comp);
@@ -213,7 +231,6 @@
@Override
public void onResume() {
super.onResume();
-
if (mParentLocale != null) {
getActivity().setTitle(mParentLocale.getFullNameNative());
} else {
@@ -250,16 +267,28 @@
// Special case for resetting the app locale to equal the system locale.
boolean isSystemLocale = locale.isSystemLocale();
boolean isRegionLocale = locale.getParent() != null;
+ boolean mayHaveDifferentNumberingSystem = locale.hasNumberingSystems();
- if (isSystemLocale || isRegionLocale) {
+ if (isSystemLocale
+ || (isRegionLocale && !mayHaveDifferentNumberingSystem)
+ || mIsNumberingSystem) {
if (mListener != null) {
mListener.onLocaleSelected(locale);
}
returnToParentFrame();
} else {
- LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker(
- mListener, locale, mTranslatedOnly /* translate only */,
- mOnActionExpandListener, this.mLocalePickerCollector);
+ LocalePickerWithRegion selector;
+ if (mayHaveDifferentNumberingSystem) {
+ selector =
+ LocalePickerWithRegion.createNumberingSystemPicker(
+ mListener, locale, mTranslatedOnly /* translate only */,
+ mOnActionExpandListener, this.mLocalePickerCollector);
+ } else {
+ selector = LocalePickerWithRegion.createCountryPicker(
+ mListener, locale, mTranslatedOnly /* translate only */,
+ mOnActionExpandListener, this.mLocalePickerCollector);
+ }
+
if (selector != null) {
getFragmentManager().beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java
index 8b41829..bcbfdc9 100644
--- a/core/java/com/android/internal/app/LocaleStore.java
+++ b/core/java/com/android/internal/app/LocaleStore.java
@@ -39,6 +39,9 @@
import java.util.Set;
public class LocaleStore {
+ private static final int TIER_LANGUAGE = 1;
+ private static final int TIER_REGION = 2;
+ private static final int TIER_NUMBERING = 3;
private static final HashMap<String, LocaleInfo> sLocaleCache = new HashMap<>();
private static final String TAG = LocaleStore.class.getSimpleName();
private static boolean sFullyInitialized = false;
@@ -68,10 +71,13 @@
private String mFullCountryNameNative;
private String mLangScriptKey;
+ private boolean mHasNumberingSystems;
+
private LocaleInfo(Locale locale) {
this.mLocale = locale;
this.mId = locale.toLanguageTag();
this.mParent = getParent(locale);
+ this.mHasNumberingSystems = false;
this.mIsChecked = false;
this.mSuggestionFlags = SUGGESTION_TYPE_NONE;
this.mIsTranslated = false;
@@ -93,6 +99,11 @@
.build();
}
+ /** Return true if there are any same locales with different numbering system. */
+ public boolean hasNumberingSystems() {
+ return mHasNumberingSystems;
+ }
+
@Override
public String toString() {
return mId;
@@ -195,6 +206,10 @@
}
}
+ String getNumberingSystem() {
+ return LocaleHelper.getDisplayNumberingSystemKeyValue(mLocale, mLocale);
+ }
+
String getContentDescription(boolean countryMode) {
if (countryMode) {
return getFullCountryNameInUiLanguage();
@@ -383,6 +398,12 @@
final boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
+ Set<Locale> numberSystemLocaleList = new HashSet<>();
+ for (String localeId : LocalePicker.getSupportedLocales(context)) {
+ if (Locale.forLanguageTag(localeId).getUnicodeLocaleType("nu") != null) {
+ numberSystemLocaleList.add(Locale.forLanguageTag(localeId));
+ }
+ }
for (String localeId : LocalePicker.getSupportedLocales(context)) {
if (localeId.isEmpty()) {
throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
@@ -403,6 +424,12 @@
if (simCountries.contains(li.getLocale().getCountry())) {
li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
}
+ numberSystemLocaleList.forEach(l -> {
+ if (li.getLocale().stripExtensions().equals(l.stripExtensions())) {
+ li.mHasNumberingSystems = true;
+ }
+ });
+
sLocaleCache.put(li.getId(), li);
final Locale parent = li.getParent();
if (parent != null) {
@@ -445,20 +472,43 @@
sFullyInitialized = true;
}
- private static int getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
- if (ignorables.contains(li.getId())) return 0;
- if (li.mIsPseudo) return 2;
- if (translatedOnly && !li.isTranslated()) return 0;
- if (li.getParent() != null) return 2;
- return 0;
+ private static boolean isShallIgnore(
+ Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
+ if (ignorables.stream().anyMatch(tag ->
+ Locale.forLanguageTag(tag).stripExtensions()
+ .equals(li.getLocale().stripExtensions()))) {
+ return true;
+ }
+ if (li.mIsPseudo) return false;
+ if (translatedOnly && !li.isTranslated()) return true;
+ if (li.getParent() != null) return false;
+ return true;
+ }
+
+ private static int getLocaleTier(LocaleInfo parent) {
+ if (parent == null) {
+ return TIER_LANGUAGE;
+ } else if (parent.getLocale().getCountry().isEmpty()) {
+ return TIER_REGION;
+ } else {
+ return TIER_NUMBERING;
+ }
}
/**
* Returns a list of locales for language or region selection.
+ *
* If the parent is null, then it is the language list.
+ *
* If it is not null, then the list will contain all the locales that belong to that parent.
* Example: if the parent is "ar", then the region list will contain all Arabic locales.
- * (this is not language based, but language-script, so that it works for zh-Hant and so on.
+ * (this is not language based, but language-script, so that it works for zh-Hant and so on.)
+ *
+ * If it is not null and has country, then the list will contain all locales with that parent's
+ * language and country, i.e. containing alternate numbering systems.
+ *
+ * Example: if the parent is "ff-Adlm-BF", then the numbering list will contain all
+ * Fula (Adlam, Burkina Faso) i.e. "ff-Adlm-BF" and "ff-Adlm-BF-u-nu-latn"
*/
@UnsupportedAppUsage
public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
@@ -478,28 +528,49 @@
*/
public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
LocaleInfo parent, boolean translatedOnly, LocaleList explicitLocales) {
- fillCache(context);
- String parentId = parent == null ? null : parent.getId();
- HashSet<LocaleInfo> result = new HashSet<>();
+ if (context != null) {
+ fillCache(context);
+ }
HashMap<String, LocaleInfo> supportedLcoaleInfos =
explicitLocales == null
? sLocaleCache
: convertExplicitLocales(explicitLocales, sLocaleCache.values());
+ return getTierLocales(ignorables, parent, translatedOnly, supportedLcoaleInfos);
+ }
+ private static Set<LocaleInfo> getTierLocales(
+ Set<String> ignorables,
+ LocaleInfo parent,
+ boolean translatedOnly,
+ HashMap<String, LocaleInfo> supportedLcoaleInfos) {
+
+ boolean hasTargetParent = parent != null;
+ String parentId = hasTargetParent ? parent.getId() : null;
+ HashSet<LocaleInfo> result = new HashSet<>();
for (LocaleStore.LocaleInfo li : supportedLcoaleInfos.values()) {
- int level = getLevel(ignorables, li, translatedOnly);
- if (level == 2) {
- if (parent != null) { // region selection
- if (parentId.equals(li.getParent().toLanguageTag())) {
- result.add(li);
- }
- } else { // language selection
+ if (isShallIgnore(ignorables, li, translatedOnly)) {
+ continue;
+ }
+ switch(getLocaleTier(parent)) {
+ case TIER_LANGUAGE:
if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
result.add(li);
} else {
- result.add(getLocaleInfo(li.getParent()));
+ result.add(getLocaleInfo(li.getParent(), supportedLcoaleInfos));
}
- }
+ break;
+ case TIER_REGION:
+ if (parentId.equals(li.getParent().toLanguageTag())) {
+ result.add(getLocaleInfo(
+ li.getLocale().stripExtensions(), supportedLcoaleInfos));
+ }
+ break;
+ case TIER_NUMBERING:
+ if (parent.getLocale().stripExtensions()
+ .equals(li.getLocale().stripExtensions())) {
+ result.add(li);
+ }
+ break;
}
}
return result;
@@ -538,18 +609,21 @@
}
private static LocaleList matchLocaleFromSupportedLocaleList(
- LocaleList explicitLocales, Collection<LocaleInfo> localeinfo) {
+ LocaleList explicitLocales, Collection<LocaleInfo> localeInfos) {
+ if (localeInfos == null) {
+ return explicitLocales;
+ }
//TODO: Adds a function for unicode extension if needed.
Locale[] resultLocales = new Locale[explicitLocales.size()];
for (int i = 0; i < explicitLocales.size(); i++) {
- Locale locale = explicitLocales.get(i).stripExtensions();
+ Locale locale = explicitLocales.get(i);
if (!TextUtils.isEmpty(locale.getCountry())) {
- for (LocaleInfo localeInfo :localeinfo) {
+ for (LocaleInfo localeInfo :localeInfos) {
if (LocaleList.matchesLanguageAndScript(locale, localeInfo.getLocale())
&& TextUtils.equals(locale.getCountry(),
localeInfo.getLocale().getCountry())) {
resultLocales[i] = localeInfo.getLocale();
- continue;
+ break;
}
}
}
@@ -562,18 +636,23 @@
@UnsupportedAppUsage
public static LocaleInfo getLocaleInfo(Locale locale) {
+ return getLocaleInfo(locale, sLocaleCache);
+ }
+
+ private static LocaleInfo getLocaleInfo(
+ Locale locale, HashMap<String, LocaleInfo> localeInfos) {
String id = locale.toLanguageTag();
LocaleInfo result;
- if (!sLocaleCache.containsKey(id)) {
+ if (!localeInfos.containsKey(id)) {
// Locale preferences can modify the language tag to current system languages, so we
// need to check the input locale without extra u extension except numbering system.
Locale filteredLocale = new Locale.Builder()
.setLocale(locale.stripExtensions())
.setUnicodeLocaleKeyword("nu", locale.getUnicodeLocaleType("nu"))
.build();
- if (sLocaleCache.containsKey(filteredLocale.toLanguageTag())) {
+ if (localeInfos.containsKey(filteredLocale.toLanguageTag())) {
result = new LocaleInfo(locale);
- LocaleInfo localeInfo = sLocaleCache.get(filteredLocale.toLanguageTag());
+ LocaleInfo localeInfo = localeInfos.get(filteredLocale.toLanguageTag());
// This locale is included in supported locales, so follow the settings
// of supported locales.
result.mIsPseudo = localeInfo.mIsPseudo;
@@ -582,9 +661,9 @@
return result;
}
result = new LocaleInfo(locale);
- sLocaleCache.put(id, result);
+ localeInfos.put(id, result);
} else {
- result = sLocaleCache.get(id);
+ result = localeInfos.get(id);
}
return result;
}
diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
index a61a6d7..08de4dfb 100644
--- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
+++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
@@ -64,6 +64,7 @@
protected ArrayList<LocaleStore.LocaleInfo> mOriginalLocaleOptions;
protected int mSuggestionCount;
protected final boolean mCountryMode;
+ protected boolean mIsNumberingMode;
protected LayoutInflater mInflater;
protected Locale mDisplayLocale = null;
@@ -89,6 +90,14 @@
}
}
+ public void setNumberingSystemMode(boolean isNumberSystemMode) {
+ mIsNumberingMode = isNumberSystemMode;
+ }
+
+ public boolean getIsForNumberingSystem() {
+ return mIsNumberingMode;
+ }
+
@Override
public boolean areAllItemsEnabled() {
return false;
@@ -209,7 +218,6 @@
if (convertView == null && mInflater == null) {
mInflater = LayoutInflater.from(parent.getContext());
}
-
int itemType = getItemViewType(position);
View itemView = getNewViewIfNeeded(convertView, parent, itemType, position);
switch (itemType) {
@@ -217,13 +225,13 @@
case TYPE_HEADER_ALL_OTHERS:
TextView textView = (TextView) itemView;
if (itemType == TYPE_HEADER_SUGGESTED) {
- if (mCountryMode) {
+ if (mCountryMode && !mIsNumberingMode) {
setTextTo(textView, R.string.language_picker_regions_section_suggested);
} else {
setTextTo(textView, R.string.language_picker_section_suggested);
}
} else {
- if (mCountryMode) {
+ if (mCountryMode && !mIsNumberingMode) {
setTextTo(textView, R.string.region_picker_section_all);
} else {
setTextTo(textView, R.string.language_picker_section_all);
@@ -419,9 +427,11 @@
private void updateTextView(View convertView, TextView text, int position) {
LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position);
- text.setText(item.getLabel(mCountryMode));
+ text.setText(mIsNumberingMode
+ ? item.getNumberingSystem() : item.getLabel(mCountryMode));
text.setTextLocale(item.getLocale());
- text.setContentDescription(item.getContentDescription(mCountryMode));
+ text.setContentDescription(mIsNumberingMode
+ ? item.getNumberingSystem() : item.getContentDescription(mCountryMode));
if (mCountryMode) {
int layoutDir = TextUtils.getLayoutDirectionFromLocale(item.getParent());
//noinspection ResourceType
diff --git a/core/java/com/android/internal/app/procstats/DumpUtils.java b/core/java/com/android/internal/app/procstats/DumpUtils.java
index bce0d60..f6bcc46 100644
--- a/core/java/com/android/internal/app/procstats/DumpUtils.java
+++ b/core/java/com/android/internal/app/procstats/DumpUtils.java
@@ -27,12 +27,12 @@
import static com.android.internal.app.procstats.ProcessStats.ADJ_SCREEN_OFF;
import static com.android.internal.app.procstats.ProcessStats.ADJ_SCREEN_ON;
import static com.android.internal.app.procstats.ProcessStats.STATE_BACKUP;
-import static com.android.internal.app.procstats.ProcessStats.STATE_BOUND_TOP_OR_FGS;
-import static com.android.internal.app.procstats.ProcessStats.STATE_CACHED_ACTIVITY;
-import static com.android.internal.app.procstats.ProcessStats.STATE_CACHED_ACTIVITY_CLIENT;
-import static com.android.internal.app.procstats.ProcessStats.STATE_CACHED_EMPTY;
+import static com.android.internal.app.procstats.ProcessStats.STATE_BOUND_FGS;
+import static com.android.internal.app.procstats.ProcessStats.STATE_BOUND_TOP;
+import static com.android.internal.app.procstats.ProcessStats.STATE_CACHED;
import static com.android.internal.app.procstats.ProcessStats.STATE_COUNT;
import static com.android.internal.app.procstats.ProcessStats.STATE_FGS;
+import static com.android.internal.app.procstats.ProcessStats.STATE_FROZEN;
import static com.android.internal.app.procstats.ProcessStats.STATE_HEAVY_WEIGHT;
import static com.android.internal.app.procstats.ProcessStats.STATE_HOME;
import static com.android.internal.app.procstats.ProcessStats.STATE_IMPORTANT_BACKGROUND;
@@ -72,7 +72,8 @@
STATE_NAMES = new String[STATE_COUNT];
STATE_NAMES[STATE_PERSISTENT] = "Persist";
STATE_NAMES[STATE_TOP] = "Top";
- STATE_NAMES[STATE_BOUND_TOP_OR_FGS] = "BTopFgs";
+ STATE_NAMES[STATE_BOUND_FGS] = "BFgs";
+ STATE_NAMES[STATE_BOUND_TOP] = "BTop";
STATE_NAMES[STATE_FGS] = "Fgs";
STATE_NAMES[STATE_IMPORTANT_FOREGROUND] = "ImpFg";
STATE_NAMES[STATE_IMPORTANT_BACKGROUND] = "ImpBg";
@@ -83,14 +84,14 @@
STATE_NAMES[STATE_HEAVY_WEIGHT] = "HeavyWt";
STATE_NAMES[STATE_HOME] = "Home";
STATE_NAMES[STATE_LAST_ACTIVITY] = "LastAct";
- STATE_NAMES[STATE_CACHED_ACTIVITY] = "CchAct";
- STATE_NAMES[STATE_CACHED_ACTIVITY_CLIENT] = "CchCAct";
- STATE_NAMES[STATE_CACHED_EMPTY] = "CchEmty";
+ STATE_NAMES[STATE_CACHED] = "Cached";
+ STATE_NAMES[STATE_FROZEN] = "Frozen";
STATE_LABELS = new String[STATE_COUNT];
STATE_LABELS[STATE_PERSISTENT] = "Persistent";
STATE_LABELS[STATE_TOP] = " Top";
- STATE_LABELS[STATE_BOUND_TOP_OR_FGS] = "Bnd TopFgs";
+ STATE_LABELS[STATE_BOUND_FGS] = " Bnd Fgs";
+ STATE_LABELS[STATE_BOUND_TOP] = " Bnd Top";
STATE_LABELS[STATE_FGS] = " Fgs";
STATE_LABELS[STATE_IMPORTANT_FOREGROUND] = " Imp Fg";
STATE_LABELS[STATE_IMPORTANT_BACKGROUND] = " Imp Bg";
@@ -101,16 +102,16 @@
STATE_LABELS[STATE_HEAVY_WEIGHT] = " Heavy Wgt";
STATE_LABELS[STATE_HOME] = " (Home)";
STATE_LABELS[STATE_LAST_ACTIVITY] = "(Last Act)";
- STATE_LABELS[STATE_CACHED_ACTIVITY] = " (Cch Act)";
- STATE_LABELS[STATE_CACHED_ACTIVITY_CLIENT] = "(Cch CAct)";
- STATE_LABELS[STATE_CACHED_EMPTY] = "(Cch Emty)";
+ STATE_LABELS[STATE_CACHED] = " (Cached)";
+ STATE_LABELS[STATE_FROZEN] = " Frozen";
STATE_LABEL_CACHED = " (Cached)";
STATE_LABEL_TOTAL = " TOTAL";
STATE_NAMES_CSV = new String[STATE_COUNT];
STATE_NAMES_CSV[STATE_PERSISTENT] = "pers";
STATE_NAMES_CSV[STATE_TOP] = "top";
- STATE_NAMES_CSV[STATE_BOUND_TOP_OR_FGS] = "btopfgs";
+ STATE_NAMES_CSV[STATE_BOUND_FGS] = "bfgs";
+ STATE_NAMES_CSV[STATE_BOUND_TOP] = "btop";
STATE_NAMES_CSV[STATE_FGS] = "fgs";
STATE_NAMES_CSV[STATE_IMPORTANT_FOREGROUND] = "impfg";
STATE_NAMES_CSV[STATE_IMPORTANT_BACKGROUND] = "impbg";
@@ -121,14 +122,14 @@
STATE_NAMES_CSV[STATE_HEAVY_WEIGHT] = "heavy";
STATE_NAMES_CSV[STATE_HOME] = "home";
STATE_NAMES_CSV[STATE_LAST_ACTIVITY] = "lastact";
- STATE_NAMES_CSV[STATE_CACHED_ACTIVITY] = "cch-activity";
- STATE_NAMES_CSV[STATE_CACHED_ACTIVITY_CLIENT] = "cch-aclient";
- STATE_NAMES_CSV[STATE_CACHED_EMPTY] = "cch-empty";
+ STATE_NAMES_CSV[STATE_CACHED] = "cached";
+ STATE_NAMES_CSV[STATE_FROZEN] = "frzn";
STATE_TAGS = new String[STATE_COUNT];
STATE_TAGS[STATE_PERSISTENT] = "p";
STATE_TAGS[STATE_TOP] = "t";
- STATE_TAGS[STATE_BOUND_TOP_OR_FGS] = "d";
+ STATE_TAGS[STATE_BOUND_FGS] = "y";
+ STATE_TAGS[STATE_BOUND_TOP] = "z";
STATE_TAGS[STATE_FGS] = "g";
STATE_TAGS[STATE_IMPORTANT_FOREGROUND] = "f";
STATE_TAGS[STATE_IMPORTANT_BACKGROUND] = "b";
@@ -139,15 +140,14 @@
STATE_TAGS[STATE_HEAVY_WEIGHT] = "w";
STATE_TAGS[STATE_HOME] = "h";
STATE_TAGS[STATE_LAST_ACTIVITY] = "l";
- STATE_TAGS[STATE_CACHED_ACTIVITY] = "a";
- STATE_TAGS[STATE_CACHED_ACTIVITY_CLIENT] = "c";
- STATE_TAGS[STATE_CACHED_EMPTY] = "e";
+ STATE_TAGS[STATE_CACHED] = "a";
+ STATE_TAGS[STATE_FROZEN] = "e";
STATE_PROTO_ENUMS = new int[STATE_COUNT];
STATE_PROTO_ENUMS[STATE_PERSISTENT] = ProcessStatsEnums.PROCESS_STATE_PERSISTENT;
STATE_PROTO_ENUMS[STATE_TOP] = ProcessStatsEnums.PROCESS_STATE_TOP;
- STATE_PROTO_ENUMS[STATE_BOUND_TOP_OR_FGS] =
- ProcessStatsEnums.PROCESS_STATE_BOUND_TOP_OR_FGS;
+ STATE_PROTO_ENUMS[STATE_BOUND_FGS] = ProcessStatsEnums.PROCESS_STATE_BOUND_FGS;
+ STATE_PROTO_ENUMS[STATE_BOUND_TOP] = ProcessStatsEnums.PROCESS_STATE_BOUND_TOP;
STATE_PROTO_ENUMS[STATE_FGS] = ProcessStatsEnums.PROCESS_STATE_FGS;
STATE_PROTO_ENUMS[STATE_IMPORTANT_FOREGROUND] =
ProcessStatsEnums.PROCESS_STATE_IMPORTANT_FOREGROUND;
@@ -161,10 +161,8 @@
STATE_PROTO_ENUMS[STATE_HEAVY_WEIGHT] = ProcessStatsEnums.PROCESS_STATE_HEAVY_WEIGHT;
STATE_PROTO_ENUMS[STATE_HOME] = ProcessStatsEnums.PROCESS_STATE_HOME;
STATE_PROTO_ENUMS[STATE_LAST_ACTIVITY] = ProcessStatsEnums.PROCESS_STATE_LAST_ACTIVITY;
- STATE_PROTO_ENUMS[STATE_CACHED_ACTIVITY] = ProcessStatsEnums.PROCESS_STATE_CACHED_ACTIVITY;
- STATE_PROTO_ENUMS[STATE_CACHED_ACTIVITY_CLIENT] =
- ProcessStatsEnums.PROCESS_STATE_CACHED_ACTIVITY_CLIENT;
- STATE_PROTO_ENUMS[STATE_CACHED_EMPTY] = ProcessStatsEnums.PROCESS_STATE_CACHED_EMPTY;
+ STATE_PROTO_ENUMS[STATE_CACHED] = ProcessStatsEnums.PROCESS_STATE_CACHED_ACTIVITY;
+ STATE_PROTO_ENUMS[STATE_FROZEN] = ProcessStatsEnums.PROCESS_STATE_FROZEN;
// Remap states, as defined by ProcessStats.java, to a reduced subset of states for data
// aggregation / size reduction purposes.
@@ -173,7 +171,9 @@
ProcessStatsEnums.AGGREGATED_PROCESS_STATE_PERSISTENT;
PROCESS_STATS_STATE_TO_AGGREGATED_STATE[STATE_TOP] =
ProcessStatsEnums.AGGREGATED_PROCESS_STATE_TOP;
- PROCESS_STATS_STATE_TO_AGGREGATED_STATE[STATE_BOUND_TOP_OR_FGS] =
+ PROCESS_STATS_STATE_TO_AGGREGATED_STATE[STATE_BOUND_FGS] =
+ ProcessStatsEnums.AGGREGATED_PROCESS_STATE_BOUND_TOP_OR_FGS;
+ PROCESS_STATS_STATE_TO_AGGREGATED_STATE[STATE_BOUND_TOP] =
ProcessStatsEnums.AGGREGATED_PROCESS_STATE_BOUND_TOP_OR_FGS;
PROCESS_STATS_STATE_TO_AGGREGATED_STATE[STATE_FGS] =
ProcessStatsEnums.AGGREGATED_PROCESS_STATE_FGS;
@@ -196,11 +196,9 @@
ProcessStatsEnums.AGGREGATED_PROCESS_STATE_CACHED;
PROCESS_STATS_STATE_TO_AGGREGATED_STATE[STATE_LAST_ACTIVITY] =
ProcessStatsEnums.AGGREGATED_PROCESS_STATE_CACHED;
- PROCESS_STATS_STATE_TO_AGGREGATED_STATE[STATE_CACHED_ACTIVITY] =
+ PROCESS_STATS_STATE_TO_AGGREGATED_STATE[STATE_CACHED] =
ProcessStatsEnums.AGGREGATED_PROCESS_STATE_CACHED;
- PROCESS_STATS_STATE_TO_AGGREGATED_STATE[STATE_CACHED_ACTIVITY_CLIENT] =
- ProcessStatsEnums.AGGREGATED_PROCESS_STATE_CACHED;
- PROCESS_STATS_STATE_TO_AGGREGATED_STATE[STATE_CACHED_EMPTY] =
+ PROCESS_STATS_STATE_TO_AGGREGATED_STATE[STATE_FROZEN] =
ProcessStatsEnums.AGGREGATED_PROCESS_STATE_CACHED;
}
diff --git a/core/java/com/android/internal/app/procstats/ProcessState.java b/core/java/com/android/internal/app/procstats/ProcessState.java
index 818a503..fff778c 100644
--- a/core/java/com/android/internal/app/procstats/ProcessState.java
+++ b/core/java/com/android/internal/app/procstats/ProcessState.java
@@ -28,10 +28,9 @@
import static com.android.internal.app.procstats.ProcessStats.PSS_USS_MAXIMUM;
import static com.android.internal.app.procstats.ProcessStats.PSS_USS_MINIMUM;
import static com.android.internal.app.procstats.ProcessStats.STATE_BACKUP;
-import static com.android.internal.app.procstats.ProcessStats.STATE_BOUND_TOP_OR_FGS;
-import static com.android.internal.app.procstats.ProcessStats.STATE_CACHED_ACTIVITY;
-import static com.android.internal.app.procstats.ProcessStats.STATE_CACHED_ACTIVITY_CLIENT;
-import static com.android.internal.app.procstats.ProcessStats.STATE_CACHED_EMPTY;
+import static com.android.internal.app.procstats.ProcessStats.STATE_BOUND_FGS;
+import static com.android.internal.app.procstats.ProcessStats.STATE_BOUND_TOP;
+import static com.android.internal.app.procstats.ProcessStats.STATE_CACHED;
import static com.android.internal.app.procstats.ProcessStats.STATE_COUNT;
import static com.android.internal.app.procstats.ProcessStats.STATE_FGS;
import static com.android.internal.app.procstats.ProcessStats.STATE_HEAVY_WEIGHT;
@@ -85,9 +84,9 @@
STATE_PERSISTENT, // ActivityManager.PROCESS_STATE_PERSISTENT
STATE_PERSISTENT, // ActivityManager.PROCESS_STATE_PERSISTENT_UI
STATE_TOP, // ActivityManager.PROCESS_STATE_TOP
- STATE_BOUND_TOP_OR_FGS, // ActivityManager.PROCESS_STATE_BOUND_TOP
+ STATE_BOUND_TOP, // ActivityManager.PROCESS_STATE_BOUND_TOP
STATE_FGS, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
- STATE_BOUND_TOP_OR_FGS, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
+ STATE_BOUND_FGS, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
STATE_IMPORTANT_FOREGROUND, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
STATE_IMPORTANT_BACKGROUND, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
STATE_IMPORTANT_BACKGROUND, // ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND
@@ -98,10 +97,10 @@
STATE_HEAVY_WEIGHT, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
STATE_HOME, // ActivityManager.PROCESS_STATE_HOME
STATE_LAST_ACTIVITY, // ActivityManager.PROCESS_STATE_LAST_ACTIVITY
- STATE_CACHED_ACTIVITY, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY
- STATE_CACHED_ACTIVITY_CLIENT, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT
- STATE_CACHED_ACTIVITY, // ActivityManager.PROCESS_STATE_CACHED_RECENT
- STATE_CACHED_EMPTY, // ActivityManager.PROCESS_STATE_CACHED_EMPTY
+ STATE_CACHED, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY
+ STATE_CACHED, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT
+ STATE_CACHED, // ActivityManager.PROCESS_STATE_CACHED_RECENT
+ STATE_CACHED, // ActivityManager.PROCESS_STATE_CACHED_EMPTY
};
public static final Comparator<ProcessState> COMPARATOR = new Comparator<ProcessState>() {
@@ -926,8 +925,11 @@
screenStates, memStates, new int[] { STATE_PERSISTENT }, now, totalTime, true);
dumpProcessSummaryDetails(pw, prefix, DumpUtils.STATE_LABELS[STATE_TOP],
screenStates, memStates, new int[] {STATE_TOP}, now, totalTime, true);
- dumpProcessSummaryDetails(pw, prefix, DumpUtils.STATE_LABELS[STATE_BOUND_TOP_OR_FGS],
- screenStates, memStates, new int[] { STATE_BOUND_TOP_OR_FGS}, now, totalTime,
+ dumpProcessSummaryDetails(pw, prefix, DumpUtils.STATE_LABELS[STATE_BOUND_TOP],
+ screenStates, memStates, new int[] { STATE_BOUND_TOP }, now, totalTime,
+ true);
+ dumpProcessSummaryDetails(pw, prefix, DumpUtils.STATE_LABELS[STATE_BOUND_FGS],
+ screenStates, memStates, new int[] { STATE_BOUND_FGS }, now, totalTime,
true);
dumpProcessSummaryDetails(pw, prefix, DumpUtils.STATE_LABELS[STATE_FGS],
screenStates, memStates, new int[] { STATE_FGS}, now, totalTime,
@@ -953,9 +955,6 @@
screenStates, memStates, new int[] {STATE_HOME}, now, totalTime, true);
dumpProcessSummaryDetails(pw, prefix, DumpUtils.STATE_LABELS[STATE_LAST_ACTIVITY],
screenStates, memStates, new int[] {STATE_LAST_ACTIVITY}, now, totalTime, true);
- dumpProcessSummaryDetails(pw, prefix, DumpUtils.STATE_LABEL_CACHED,
- screenStates, memStates, new int[] {STATE_CACHED_ACTIVITY,
- STATE_CACHED_ACTIVITY_CLIENT, STATE_CACHED_EMPTY}, now, totalTime, true);
}
public void dumpProcessState(PrintWriter pw, String prefix,
@@ -1563,7 +1562,10 @@
case STATE_TOP:
topMs += duration;
break;
- case STATE_BOUND_TOP_OR_FGS:
+ case STATE_BOUND_FGS:
+ boundFgsMs += duration;
+ break;
+ case STATE_BOUND_TOP:
boundTopMs += duration;
break;
case STATE_FGS:
@@ -1583,13 +1585,10 @@
case STATE_PERSISTENT:
otherMs += duration;
break;
- case STATE_CACHED_ACTIVITY:
- case STATE_CACHED_ACTIVITY_CLIENT:
- case STATE_CACHED_EMPTY:
+ case STATE_CACHED:
cachedMs += duration;
break;
- // TODO (b/261910877) Add support for tracking boundFgsMs and
- // frozenMs.
+ // TODO (b/261910877) Add support for tracking frozenMs.
}
}
statsEventOutput.write(
diff --git a/core/java/com/android/internal/app/procstats/ProcessStats.java b/core/java/com/android/internal/app/procstats/ProcessStats.java
index f3ed09a..3ce234b 100644
--- a/core/java/com/android/internal/app/procstats/ProcessStats.java
+++ b/core/java/com/android/internal/app/procstats/ProcessStats.java
@@ -81,21 +81,21 @@
public static final int STATE_NOTHING = -1;
public static final int STATE_PERSISTENT = 0;
public static final int STATE_TOP = 1;
- public static final int STATE_BOUND_TOP_OR_FGS = 2;
+ public static final int STATE_BOUND_TOP = 2;
public static final int STATE_FGS = 3;
- public static final int STATE_IMPORTANT_FOREGROUND = 4;
- public static final int STATE_IMPORTANT_BACKGROUND = 5;
- public static final int STATE_BACKUP = 6;
- public static final int STATE_SERVICE = 7;
- public static final int STATE_SERVICE_RESTARTING = 8;
- public static final int STATE_RECEIVER = 9;
- public static final int STATE_HEAVY_WEIGHT = 10;
- public static final int STATE_HOME = 11;
- public static final int STATE_LAST_ACTIVITY = 12;
- public static final int STATE_CACHED_ACTIVITY = 13;
- public static final int STATE_CACHED_ACTIVITY_CLIENT = 14;
- public static final int STATE_CACHED_EMPTY = 15;
- public static final int STATE_COUNT = STATE_CACHED_EMPTY+1;
+ public static final int STATE_BOUND_FGS = 4;
+ public static final int STATE_IMPORTANT_FOREGROUND = 5;
+ public static final int STATE_IMPORTANT_BACKGROUND = 6;
+ public static final int STATE_BACKUP = 7;
+ public static final int STATE_SERVICE = 8;
+ public static final int STATE_SERVICE_RESTARTING = 9;
+ public static final int STATE_RECEIVER = 10;
+ public static final int STATE_HEAVY_WEIGHT = 11;
+ public static final int STATE_HOME = 12;
+ public static final int STATE_LAST_ACTIVITY = 13;
+ public static final int STATE_CACHED = 14;
+ public static final int STATE_FROZEN = 15;
+ public static final int STATE_COUNT = STATE_FROZEN + 1;
public static final int PSS_SAMPLE_COUNT = 0;
public static final int PSS_MINIMUM = 1;
@@ -154,9 +154,10 @@
public static final int[] ALL_SCREEN_ADJ = new int[] { ADJ_SCREEN_OFF, ADJ_SCREEN_ON };
public static final int[] NON_CACHED_PROC_STATES = new int[] {
- STATE_PERSISTENT, STATE_TOP, STATE_BOUND_TOP_OR_FGS, STATE_FGS,
+ STATE_PERSISTENT, STATE_TOP, STATE_FGS,
STATE_IMPORTANT_FOREGROUND, STATE_IMPORTANT_BACKGROUND, STATE_BACKUP,
- STATE_SERVICE, STATE_SERVICE_RESTARTING, STATE_RECEIVER, STATE_HEAVY_WEIGHT
+ STATE_SERVICE, STATE_SERVICE_RESTARTING, STATE_RECEIVER, STATE_HEAVY_WEIGHT,
+ STATE_BOUND_TOP, STATE_BOUND_FGS
};
public static final int[] BACKGROUND_PROC_STATES = new int[] {
@@ -165,11 +166,11 @@
};
public static final int[] ALL_PROC_STATES = new int[] { STATE_PERSISTENT,
- STATE_TOP, STATE_BOUND_TOP_OR_FGS, STATE_FGS, STATE_IMPORTANT_FOREGROUND,
+ STATE_TOP, STATE_FGS, STATE_IMPORTANT_FOREGROUND,
STATE_IMPORTANT_BACKGROUND, STATE_BACKUP,
STATE_SERVICE, STATE_SERVICE_RESTARTING, STATE_RECEIVER,
- STATE_HEAVY_WEIGHT, STATE_HOME, STATE_LAST_ACTIVITY, STATE_CACHED_ACTIVITY,
- STATE_CACHED_ACTIVITY_CLIENT, STATE_CACHED_EMPTY
+ STATE_HEAVY_WEIGHT, STATE_HOME, STATE_LAST_ACTIVITY, STATE_CACHED,
+ STATE_BOUND_TOP, STATE_BOUND_FGS, STATE_FROZEN
};
// Should report process stats.
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 496cc3a..479ea4e 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3161,7 +3161,7 @@
<!-- @SystemApi @hide Allows an application to call APIs that allow it to query users on the
device. -->
<permission android:name="android.permission.QUERY_USERS"
- android:protectionLevel="signature|role" />
+ android:protectionLevel="signature|privileged|role" />
<!-- Allows an application to access data blobs across users. -->
<permission android:name="android.permission.ACCESS_BLOBS_ACROSS_USERS"
@@ -4492,7 +4492,7 @@
<!-- Allows an application to be able to store and retrieve credentials from a remote
device.
- @hide @SystemApi -->
+ <p>Protection level: signature|privileged|role -->
<permission android:name="android.permission.PROVIDE_REMOTE_CREDENTIALS"
android:protectionLevel="signature|privileged|role" />
diff --git a/core/res/res/layout/miniresolver.xml b/core/res/res/layout/miniresolver.xml
index d07ad89..1ad3acd 100644
--- a/core/res/res/layout/miniresolver.xml
+++ b/core/res/res/layout/miniresolver.xml
@@ -56,6 +56,7 @@
android:paddingTop="16dp"
android:layout_below="@id/icon"
android:layout_centerHorizontal="true"
+ android:fontFamily="@string/config_headlineFontFamily"
android:textSize="24sp"
android:lineHeight="32sp"
android:gravity="center"
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 4d2747a..2ed1817 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -711,6 +711,9 @@
mode. -->
<integer name="config_unfoldTransitionHalfFoldedTimeout">1000</integer>
+ <!-- Timeout for receiving the keyguard drawn event from System UI. -->
+ <integer name="config_keyguardDrawnTimeout">1000</integer>
+
<!-- Indicates that the device supports having more than one internal display on at the same
time. Only applicable to devices with more than one internal display. If this option is
set to false, DisplayManager will make additional effort to ensure no more than 1 internal
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 3074906..d8e69d7 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -327,6 +327,7 @@
<item>@string/wfcSpnFormat_wifi_calling_wo_hyphen</item>
<item>@string/wfcSpnFormat_vowifi</item>
<item>@string/wfcSpnFormat_spn_wifi_calling_vo_hyphen</item>
+ <item>@string/wfcSpnFormat_wifi_call</item>
</string-array>
<!-- Spn during Wi-Fi Calling: "<operator>" -->
@@ -353,6 +354,8 @@
<string name="wfcSpnFormat_wifi_calling_wo_hyphen">WiFi Calling</string>
<!-- Spn during Wi-Fi Calling: "VoWifi" -->
<string name="wfcSpnFormat_vowifi">VoWifi</string>
+ <!-- Spn during Wi_Fi Calling: "WiFi Call" (without hyphen). This string is shown in the call banner for calls which take place over a WiFi network. -->
+ <string name="wfcSpnFormat_wifi_call">WiFi Call</string>
<!-- WFC, summary for Disabled -->
<string name="wifi_calling_off_summary">Off</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index a15833d..c10612e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1939,6 +1939,7 @@
<java-symbol type="bool" name="config_allowTheaterModeWakeFromDock" />
<java-symbol type="bool" name="config_allowTheaterModeWakeFromWindowLayout" />
<java-symbol type="bool" name="config_keepDreamingWhenUndocking" />
+ <java-symbol type="integer" name="config_keyguardDrawnTimeout" />
<java-symbol type="bool" name="config_goToSleepOnButtonPressTheaterMode" />
<java-symbol type="bool" name="config_supportLongPressPowerWhenNonInteractive" />
<java-symbol type="bool" name="config_wimaxEnabled" />
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
index 1ec2613..fccb177 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
@@ -24,6 +24,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -224,7 +225,7 @@
}
@Test
- public void onTouchEvent_startHandwriting_delegate() {
+ public void onTouchEvent_tryAcceptDelegation_delegatorCallbackCreatesInputConnection() {
View delegateView = new View(mContext);
delegateView.setIsHandwritingDelegate(true);
@@ -245,6 +246,29 @@
}
@Test
+ public void onTouchEvent_tryAcceptDelegation_delegatorCallbackFocusesDelegate() {
+ View delegateView = new View(mContext);
+ delegateView.setIsHandwritingDelegate(true);
+ mHandwritingInitiator.onInputConnectionCreated(delegateView);
+ reset(mHandwritingInitiator);
+
+ mTestView1.setHandwritingDelegatorCallback(
+ () -> mHandwritingInitiator.onDelegateViewFocused(delegateView));
+
+ final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
+ final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
+ MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
+ mHandwritingInitiator.onTouchEvent(stylusEvent1);
+
+ final int x2 = x1 + mHandwritingSlop * 2;
+ final int y2 = y1;
+ MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
+ mHandwritingInitiator.onTouchEvent(stylusEvent2);
+
+ verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(delegateView);
+ }
+
+ @Test
public void onTouchEvent_notStartHandwriting_whenHandwritingNotAvailable() {
final Rect rect = new Rect(600, 600, 900, 900);
final View testView = createView(rect, true /* autoHandwritingEnabled */,
diff --git a/core/tests/coretests/src/com/android/internal/app/procstats/ProcessStatsTest.java b/core/tests/coretests/src/com/android/internal/app/procstats/ProcessStatsTest.java
index 35b3267..6189914 100644
--- a/core/tests/coretests/src/com/android/internal/app/procstats/ProcessStatsTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/procstats/ProcessStatsTest.java
@@ -23,6 +23,8 @@
import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;
+import android.app.ActivityManager;
+
import androidx.test.filters.SmallTest;
import com.android.internal.util.FrameworkStatsLog;
@@ -128,6 +130,34 @@
}
@SmallTest
+ public void testDumpBoundFgsDuration() throws Exception {
+ ProcessStats processStats = new ProcessStats();
+ ProcessState processState =
+ processStats.getProcessStateLocked(
+ APP_1_PACKAGE_NAME, APP_1_UID, APP_1_VERSION, APP_1_PROCESS_NAME);
+ processState.setState(ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE,
+ ProcessStats.ADJ_MEM_FACTOR_NORMAL, NOW_MS, /* pkgList */ null);
+ processState.commitStateTime(NOW_MS + TimeUnit.SECONDS.toMillis(DURATION_SECS));
+ processStats.dumpProcessState(FrameworkStatsLog.PROCESS_STATE, mStatsEventOutput);
+ verify(mStatsEventOutput)
+ .write(
+ eq(FrameworkStatsLog.PROCESS_STATE),
+ eq(APP_1_UID),
+ eq(APP_1_PROCESS_NAME),
+ anyInt(),
+ anyInt(),
+ eq(0),
+ eq(0),
+ eq(0),
+ eq(0),
+ eq(DURATION_SECS),
+ eq(0),
+ eq(0),
+ eq(0),
+ eq(0));
+ }
+
+ @SmallTest
public void testDumpProcessAssociation() throws Exception {
ProcessStats processStats = new ProcessStats();
AssociationState associationState =
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index e287867..5d303cf 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -275,6 +275,7 @@
<!-- Permission required to test onPermissionsChangedListener -->
<permission name="android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS"/>
<permission name="android.permission.INTERACT_ACROSS_USERS"/>
+ <permission name="android.permission.QUERY_USERS"/>
<permission name="android.permission.LOCAL_MAC_ADDRESS"/>
<permission name="android.permission.MANAGE_ACCESSIBILITY"/>
<permission name="android.permission.MANAGE_DEVICE_ADMINS"/>
diff --git a/data/fonts/fonts.xml b/data/fonts/fonts.xml
index 8e2a59c..e7ddc88 100644
--- a/data/fonts/fonts.xml
+++ b/data/fonts/fonts.xml
@@ -255,9 +255,12 @@
</family>
<family name="cursive">
- <font weight="400" style="normal" postScriptName="DancingScript">DancingScript-Regular.ttf
- </font>
- <font weight="700" style="normal">DancingScript-Bold.ttf</font>
+ <font weight="400" style="normal">DancingScript-Regular.ttf
+ <axis tag="wght" stylevalue="400" />
+ </font>
+ <font weight="700" style="normal">DancingScript-Regular.ttf
+ <axis tag="wght" stylevalue="700" />
+ </font>
</family>
<family name="sans-serif-smallcaps">
diff --git a/data/keyboards/Generic.kcm b/data/keyboards/Generic.kcm
index fe6eeeb..1048742 100644
--- a/data/keyboards/Generic.kcm
+++ b/data/keyboards/Generic.kcm
@@ -42,9 +42,10 @@
label: 'C'
base: 'c'
shift, capslock: 'C'
- alt: '\u00e7'
- shift+alt: '\u00c7'
shift+capslock: 'c'
+ alt: '\u00e7'
+ shift+alt, capslock+alt: '\u00c7'
+ shift+capslock+alt: '\u00e7'
}
key D {
@@ -58,8 +59,8 @@
label: 'E'
base: 'e'
shift, capslock: 'E'
- alt: '\u0301'
shift+capslock: 'e'
+ alt: '\u0301'
}
key F {
@@ -87,8 +88,8 @@
label: 'I'
base: 'i'
shift, capslock: 'I'
- alt: '\u0302'
shift+capslock: 'i'
+ alt: '\u0302'
}
key J {
@@ -123,8 +124,8 @@
label: 'N'
base: 'n'
shift, capslock: 'N'
- alt: '\u0303'
shift+capslock: 'n'
+ alt: '\u0303'
}
key O {
@@ -159,8 +160,8 @@
label: 'S'
base: 's'
shift, capslock: 'S'
- alt: '\u00df'
shift+capslock: 's'
+ alt: '\u00df'
}
key T {
@@ -174,8 +175,8 @@
label: 'U'
base: 'u'
shift, capslock: 'U'
- alt: '\u0308'
shift+capslock: 'u'
+ alt: '\u0308'
}
key V {
diff --git a/data/keyboards/Vendor_004c_Product_0265.idc b/data/keyboards/Vendor_004c_Product_0265.idc
deleted file mode 120000
index 707dfcf..0000000
--- a/data/keyboards/Vendor_004c_Product_0265.idc
+++ /dev/null
@@ -1 +0,0 @@
-Vendor_05ac_Product_0265.idc
\ No newline at end of file
diff --git a/data/keyboards/Vendor_004c_Product_0265.idc b/data/keyboards/Vendor_004c_Product_0265.idc
new file mode 100644
index 0000000..bfea4db
--- /dev/null
+++ b/data/keyboards/Vendor_004c_Product_0265.idc
@@ -0,0 +1,34 @@
+# Copyright 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Apple Magic Trackpad 2 (Bluetooth) configuration file
+#
+# WHEN MODIFYING, also change the USB file (Vendor_05ac_Product_0265.idc)
+#
+
+gestureProp.Pressure_Calibration_Offset = 30
+gestureProp.Palm_Pressure = 250.0
+gestureProp.Palm_Width = 20.0
+gestureProp.Multiple_Palm_Width = 20.0
+
+# Enable Stationary Wiggle Filter
+gestureProp.Stationary_Wiggle_Filter_Enabled = 1
+gestureProp.Finger_Moving_Energy = 0.0008
+gestureProp.Finger_Moving_Hysteresis = 0.0004
+
+# Avoid accidental scroll/move on finger lift
+gestureProp.Max_Stationary_Move_Speed = 47
+gestureProp.Max_Stationary_Move_Speed_Hysteresis = 1
+gestureProp.Max_Stationary_Move_Suppress_Distance = 0.2
diff --git a/data/keyboards/Vendor_05ac_Product_0265.idc b/data/keyboards/Vendor_05ac_Product_0265.idc
index d72de64..520d188 100644
--- a/data/keyboards/Vendor_05ac_Product_0265.idc
+++ b/data/keyboards/Vendor_05ac_Product_0265.idc
@@ -13,9 +13,9 @@
# limitations under the License.
#
-# Apple Magic Trackpad 2 configuration file
-# Bluetooth vendor ID 004c
-# USB vendor ID 05ac
+# Apple Magic Trackpad 2 (USB) configuration file
+#
+# WHEN MODIFYING, also change the Bluetooth file (Vendor_004c_Product_0265.idc)
#
gestureProp.Pressure_Calibration_Offset = 30
diff --git a/data/keyboards/Virtual.kcm b/data/keyboards/Virtual.kcm
index 53308e3..06b8237 100644
--- a/data/keyboards/Virtual.kcm
+++ b/data/keyboards/Virtual.kcm
@@ -39,9 +39,10 @@
label: 'C'
base: 'c'
shift, capslock: 'C'
- alt: '\u00e7'
- shift+alt: '\u00c7'
shift+capslock: 'c'
+ alt: '\u00e7'
+ shift+alt, capslock+alt: '\u00c7'
+ shift+capslock+alt: '\u00e7'
}
key D {
@@ -55,8 +56,8 @@
label: 'E'
base: 'e'
shift, capslock: 'E'
- alt: '\u0301'
shift+capslock: 'e'
+ alt: '\u0301'
}
key F {
@@ -84,8 +85,8 @@
label: 'I'
base: 'i'
shift, capslock: 'I'
- alt: '\u0302'
shift+capslock: 'i'
+ alt: '\u0302'
}
key J {
@@ -120,8 +121,8 @@
label: 'N'
base: 'n'
shift, capslock: 'N'
- alt: '\u0303'
shift+capslock: 'n'
+ alt: '\u0303'
}
key O {
@@ -156,8 +157,8 @@
label: 'S'
base: 's'
shift, capslock: 'S'
- alt: '\u00df'
shift+capslock: 's'
+ alt: '\u00df'
}
key T {
@@ -171,8 +172,8 @@
label: 'U'
base: 'u'
shift, capslock: 'U'
- alt: '\u0308'
shift+capslock: 'u'
+ alt: '\u0308'
}
key V {
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
index c1f6c29..c3b0f9b 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
@@ -808,9 +808,12 @@
KeymasterDefs.KM_TAG_ATTESTATION_ID_BRAND,
platformReportedBrand.getBytes(StandardCharsets.UTF_8)
));
+ final String platformReportedDevice =
+ isPropertyEmptyOrUnknown(Build.DEVICE_FOR_ATTESTATION)
+ ? Build.DEVICE : Build.DEVICE_FOR_ATTESTATION;
params.add(KeyStore2ParameterUtils.makeBytes(
KeymasterDefs.KM_TAG_ATTESTATION_ID_DEVICE,
- Build.DEVICE.getBytes(StandardCharsets.UTF_8)
+ platformReportedDevice.getBytes(StandardCharsets.UTF_8)
));
final String platformReportedProduct =
isPropertyEmptyOrUnknown(Build.PRODUCT_FOR_ATTESTATION)
@@ -819,9 +822,12 @@
KeymasterDefs.KM_TAG_ATTESTATION_ID_PRODUCT,
platformReportedProduct.getBytes(StandardCharsets.UTF_8)
));
+ final String platformReportedManufacturer =
+ isPropertyEmptyOrUnknown(Build.MANUFACTURER_FOR_ATTESTATION)
+ ? Build.MANUFACTURER : Build.MANUFACTURER_FOR_ATTESTATION;
params.add(KeyStore2ParameterUtils.makeBytes(
KeymasterDefs.KM_TAG_ATTESTATION_ID_MANUFACTURER,
- Build.MANUFACTURER.getBytes(StandardCharsets.UTF_8)
+ platformReportedManufacturer.getBytes(StandardCharsets.UTF_8)
));
final String platformReportedModel =
isPropertyEmptyOrUnknown(Build.MODEL_FOR_ATTESTATION)
diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS
index 4b12590..852edef 100644
--- a/libs/WindowManager/Shell/OWNERS
+++ b/libs/WindowManager/Shell/OWNERS
@@ -1,4 +1,4 @@
xutan@google.com
# Give submodule owners in shell resource approval
-per-file res*/*/*.xml = hwwang@google.com, lbill@google.com, madym@google.com
+per-file res*/*/*.xml = hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
index bf226283..cb1a6e7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
@@ -22,10 +22,12 @@
import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FOLDABLE;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.app.WindowConfiguration;
import android.content.Context;
import android.content.res.Configuration;
+import android.os.SystemProperties;
import android.util.ArraySet;
import android.view.Surface;
@@ -34,6 +36,8 @@
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellInit;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@@ -49,8 +53,34 @@
public class TabletopModeController implements
DevicePostureController.OnDevicePostureChangedListener,
DisplayController.OnDisplaysChangedListener {
+ /**
+ * When {@code true}, floating windows like PiP would auto move to the position
+ * specified by {@link #PREFER_TOP_HALF_IN_TABLETOP} when in tabletop mode.
+ */
+ private static final boolean ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP =
+ SystemProperties.getBoolean(
+ "persist.wm.debug.enable_move_floating_window_in_tabletop", false);
+
+ /**
+ * Prefer the {@link #PREFERRED_TABLETOP_HALF_TOP} if this flag is enabled,
+ * {@link #PREFERRED_TABLETOP_HALF_BOTTOM} otherwise.
+ * See also {@link #getPreferredHalfInTabletopMode()}.
+ */
+ private static final boolean PREFER_TOP_HALF_IN_TABLETOP =
+ SystemProperties.getBoolean("persist.wm.debug.prefer_top_half_in_tabletop", true);
+
private static final long TABLETOP_MODE_DELAY_MILLIS = 1_000;
+ @IntDef(prefix = {"PREFERRED_TABLETOP_HALF_"}, value = {
+ PREFERRED_TABLETOP_HALF_TOP,
+ PREFERRED_TABLETOP_HALF_BOTTOM
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PreferredTabletopHalf {}
+
+ public static final int PREFERRED_TABLETOP_HALF_TOP = 0;
+ public static final int PREFERRED_TABLETOP_HALF_BOTTOM = 1;
+
private final Context mContext;
private final DevicePostureController mDevicePostureController;
@@ -132,6 +162,22 @@
}
}
+ /**
+ * @return {@code true} if floating windows like PiP would auto move to the position
+ * specified by {@link #getPreferredHalfInTabletopMode()} when in tabletop mode.
+ */
+ public boolean enableMoveFloatingWindowInTabletop() {
+ return ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP;
+ }
+
+ /** @return Preferred half for floating windows like PiP when in tabletop mode. */
+ @PreferredTabletopHalf
+ public int getPreferredHalfInTabletopMode() {
+ return PREFER_TOP_HALF_IN_TABLETOP
+ ? PREFERRED_TABLETOP_HALF_TOP
+ : PREFERRED_TABLETOP_HALF_BOTTOM;
+ }
+
/** Register {@link OnTabletopModeChangedListener} to listen for tabletop mode change. */
public void registerOnTabletopModeChangedListener(
@NonNull OnTabletopModeChangedListener listener) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index ba0f073..7a83d10 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -45,6 +45,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.SystemWindows;
+import com.android.wm.shell.common.TabletopModeController;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.annotations.ShellBackgroundThread;
@@ -357,6 +358,7 @@
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayInsetsController displayInsetsController,
+ TabletopModeController pipTabletopController,
Optional<OneHandedController> oneHandedController,
@ShellMainThread ShellExecutor mainExecutor) {
return Optional.ofNullable(PipController.create(
@@ -366,7 +368,7 @@
pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer,
pipTransitionState, pipTouchHandler, pipTransitionController,
windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
- displayInsetsController, oneHandedController, mainExecutor));
+ displayInsetsController, pipTabletopController, oneHandedController, mainExecutor));
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 73a7403..31c5e33 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -25,6 +25,7 @@
import android.app.WindowConfiguration.WindowingMode
import android.content.Context
import android.os.IBinder
+import android.os.SystemProperties
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
import android.view.WindowManager.TRANSIT_NONE
@@ -32,6 +33,7 @@
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.TransitionInfo
import android.window.TransitionRequestInfo
+import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import androidx.annotation.BinderThread
import com.android.internal.protolog.common.ProtoLog
@@ -115,10 +117,7 @@
val wct = WindowContainerTransaction()
// Bring other apps to front first
bringDesktopAppsToFront(wct)
-
- wct.setWindowingMode(task.getToken(), WINDOWING_MODE_FREEFORM)
- wct.reorder(task.getToken(), true /* onTop */)
-
+ addMoveToDesktopChanges(wct, task.token)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
} else {
@@ -136,8 +135,7 @@
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToFullscreen: %d", task.taskId)
val wct = WindowContainerTransaction()
- wct.setWindowingMode(task.getToken(), WINDOWING_MODE_FULLSCREEN)
- wct.setBounds(task.getToken(), null)
+ addMoveToFullscreenChanges(wct, task.token)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
} else {
@@ -234,8 +232,8 @@
" taskId=%d",
task.taskId
)
- return WindowContainerTransaction().apply {
- setWindowingMode(task.token, WINDOWING_MODE_FREEFORM)
+ return WindowContainerTransaction().also { wct ->
+ addMoveToDesktopChanges(wct, task.token)
}
}
}
@@ -251,15 +249,44 @@
" taskId=%d",
task.taskId
)
- return WindowContainerTransaction().apply {
- setWindowingMode(task.token, WINDOWING_MODE_FULLSCREEN)
- setBounds(task.token, null)
+ return WindowContainerTransaction().also { wct ->
+ addMoveToFullscreenChanges(wct, task.token)
}
}
}
return null
}
+ private fun addMoveToDesktopChanges(
+ wct: WindowContainerTransaction,
+ token: WindowContainerToken
+ ) {
+ wct.setWindowingMode(token, WINDOWING_MODE_FREEFORM)
+ wct.reorder(token, true /* onTop */)
+ if (isDesktopDensityOverrideSet()) {
+ wct.setDensityDpi(token, getDesktopDensityDpi())
+ }
+ }
+
+ private fun addMoveToFullscreenChanges(
+ wct: WindowContainerTransaction,
+ token: WindowContainerToken
+ ) {
+ wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN)
+ wct.setBounds(token, null)
+ if (isDesktopDensityOverrideSet()) {
+ wct.setDensityDpi(token, getFullscreenDensityDpi())
+ }
+ }
+
+ private fun getFullscreenDensityDpi(): Int {
+ return context.resources.displayMetrics.densityDpi
+ }
+
+ private fun getDesktopDensityDpi(): Int {
+ return DESKTOP_DENSITY_OVERRIDE
+ }
+
/** Creates a new instance of the external interface to pass to another process. */
private fun createExternalInterface(): ExternalInterfaceBinder {
return IDesktopModeImpl(this)
@@ -318,4 +345,18 @@
return result[0]
}
}
+
+ companion object {
+ private val DESKTOP_DENSITY_OVERRIDE =
+ SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 0)
+ private val DESKTOP_DENSITY_ALLOWED_RANGE = (100..1000)
+
+ /**
+ * Check if desktop density override is enabled
+ */
+ @JvmStatic
+ fun isDesktopDensityOverrideSet(): Boolean {
+ return DESKTOP_DENSITY_OVERRIDE in DESKTOP_DENSITY_ALLOWED_RANGE
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
index f664808..f08742d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
@@ -43,7 +43,9 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
@@ -113,6 +115,12 @@
* @see android.view.View#setPreferKeepClearRects
*/
private final Set<Rect> mUnrestrictedKeepClearAreas = new ArraySet<>();
+ /**
+ * Additional to {@link #mUnrestrictedKeepClearAreas}, allow the caller to append named bounds
+ * as unrestricted keep clear area. Values in this map would be appended to
+ * {@link #getUnrestrictedKeepClearAreas()} and this is meant for internal usage only.
+ */
+ private final Map<String, Rect> mNamedUnrestrictedKeepClearAreas = new HashMap<>();
private @Nullable Runnable mOnMinimalSizeChangeCallback;
private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback;
@@ -378,6 +386,16 @@
mUnrestrictedKeepClearAreas.addAll(unrestrictedAreas);
}
+ /** Add a named unrestricted keep clear area. */
+ public void addNamedUnrestrictedKeepClearArea(@NonNull String name, Rect unrestrictedArea) {
+ mNamedUnrestrictedKeepClearAreas.put(name, unrestrictedArea);
+ }
+
+ /** Remove a named unrestricted keep clear area. */
+ public void removeNamedUnrestrictedKeepClearArea(@NonNull String name) {
+ mNamedUnrestrictedKeepClearAreas.remove(name);
+ }
+
@NonNull
public Set<Rect> getRestrictedKeepClearAreas() {
return mRestrictedKeepClearAreas;
@@ -385,7 +403,10 @@
@NonNull
public Set<Rect> getUnrestrictedKeepClearAreas() {
- return mUnrestrictedKeepClearAreas;
+ if (mNamedUnrestrictedKeepClearAreas.isEmpty()) return mUnrestrictedKeepClearAreas;
+ final Set<Rect> unrestrictedAreas = new ArraySet<>(mUnrestrictedKeepClearAreas);
+ unrestrictedAreas.addAll(mNamedUnrestrictedKeepClearAreas.values());
+ return unrestrictedAreas;
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 87cf655..45bb73b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -489,10 +489,11 @@
// Reparent the pip leash to the root with max layer so that we can animate it outside of
// parent crop, and make sure it is not covered by other windows.
final SurfaceControl pipLeash = pipChange.getLeash();
- startTransaction.reparent(pipLeash, info.getRootLeash());
+ final int rootIdx = TransitionUtil.rootIndexFor(pipChange, info);
+ startTransaction.reparent(pipLeash, info.getRoot(rootIdx).getLeash());
startTransaction.setLayer(pipLeash, Integer.MAX_VALUE);
// Note: because of this, the bounds to animate should be translated to the root coordinate.
- final Point offset = info.getRootOffset();
+ final Point offset = info.getRoot(rootIdx).getOffset();
final Rect currentBounds = mPipBoundsState.getBounds();
currentBounds.offset(-offset.x, -offset.y);
startTransaction.setPosition(pipLeash, currentBounds.left, currentBounds.top);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index db6ef1d..be9b529 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -43,6 +43,7 @@
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.SystemProperties;
@@ -71,6 +72,7 @@
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SingleInstanceRemoteListener;
+import com.android.wm.shell.common.TabletopModeController;
import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.onehanded.OneHandedController;
@@ -147,6 +149,7 @@
private TaskStackListenerImpl mTaskStackListener;
private PipParamsChangedForwarder mPipParamsChangedForwarder;
private DisplayInsetsController mDisplayInsetsController;
+ private TabletopModeController mTabletopModeController;
private Optional<OneHandedController> mOneHandedController;
private final ShellCommandHandler mShellCommandHandler;
private final ShellController mShellController;
@@ -403,6 +406,7 @@
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayInsetsController displayInsetsController,
+ TabletopModeController pipTabletopController,
Optional<OneHandedController> oneHandedController,
ShellExecutor mainExecutor) {
if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
@@ -417,7 +421,7 @@
pipDisplayLayoutState, pipMotionHelper, pipMediaController, phonePipMenuController,
pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController,
windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
- displayInsetsController, oneHandedController, mainExecutor)
+ displayInsetsController, pipTabletopController, oneHandedController, mainExecutor)
.mImpl;
}
@@ -444,6 +448,7 @@
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayInsetsController displayInsetsController,
+ TabletopModeController tabletopModeController,
Optional<OneHandedController> oneHandedController,
ShellExecutor mainExecutor
) {
@@ -477,6 +482,7 @@
.getInteger(R.integer.config_pipEnterAnimationDuration);
mPipParamsChangedForwarder = pipParamsChangedForwarder;
mDisplayInsetsController = displayInsetsController;
+ mTabletopModeController = tabletopModeController;
shellInit.addInitCallback(this::onInit, this);
}
@@ -659,6 +665,42 @@
}
});
+ mTabletopModeController.registerOnTabletopModeChangedListener((isInTabletopMode) -> {
+ if (!mTabletopModeController.enableMoveFloatingWindowInTabletop()) return;
+ final String tag = "tabletop-mode";
+ if (!isInTabletopMode) {
+ mPipBoundsState.removeNamedUnrestrictedKeepClearArea(tag);
+ return;
+ }
+
+ // To prepare for the entry bounds.
+ final Rect displayBounds = mPipBoundsState.getDisplayBounds();
+ if (mTabletopModeController.getPreferredHalfInTabletopMode()
+ == TabletopModeController.PREFERRED_TABLETOP_HALF_TOP) {
+ // Prefer top, avoid the bottom half of the display.
+ mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect(
+ displayBounds.left, displayBounds.centerY(),
+ displayBounds.right, displayBounds.bottom));
+ } else {
+ // Prefer bottom, avoid the top half of the display.
+ mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect(
+ displayBounds.left, displayBounds.top,
+ displayBounds.right, displayBounds.centerY()));
+ }
+
+ // Try to move the PiP window if we have entered PiP mode.
+ if (mPipTransitionState.hasEnteredPip()) {
+ final Rect pipBounds = mPipBoundsState.getBounds();
+ final Point edgeInsets = mPipSizeSpecHandler.getScreenEdgeInsets();
+ if ((pipBounds.height() + 2 * edgeInsets.y) > (displayBounds.height() / 2)) {
+ // PiP bounds is too big to fit either half, bail early.
+ return;
+ }
+ mMainExecutor.removeCallbacks(mMovePipInResponseToKeepClearAreasChangeCallback);
+ mMainExecutor.execute(mMovePipInResponseToKeepClearAreasChangeCallback);
+ }
+ });
+
mOneHandedController.ifPresent(controller -> {
controller.registerTransitionCallback(
new OneHandedTransitionCallback() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index e1c0895..e09c3c9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -37,6 +37,7 @@
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
import android.view.SurfaceControl;
@@ -128,6 +129,7 @@
final int mode = info.getChanges().get(i).getMode();
if (mode == TRANSIT_CHANGE) {
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
if (change.getParent() != null) {
// This is probably reparented, so we want the parent to be immediately visible
final TransitionInfo.Change parentChange = info.getChange(change.getParent());
@@ -135,7 +137,7 @@
t.setAlpha(parentChange.getLeash(), 1.f);
// and then animate this layer outside the parent (since, for example, this is
// the home task animating from fullscreen to part-screen).
- t.reparent(leash, info.getRootLeash());
+ t.reparent(leash, info.getRoot(rootIdx).getLeash());
t.setLayer(leash, info.getChanges().size() - i);
// build the finish reparent/reposition
mFinishTransaction.reparent(leash, parentChange.getLeash());
@@ -145,8 +147,9 @@
// TODO(shell-transitions): screenshot here
final Rect startBounds = new Rect(change.getStartAbsBounds());
final Rect endBounds = new Rect(change.getEndAbsBounds());
- startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
- endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
+ final Point rootOffset = info.getRoot(rootIdx).getOffset();
+ startBounds.offset(-rootOffset.x, -rootOffset.y);
+ endBounds.offset(-rootOffset.x, -rootOffset.y);
startExampleResizeAnimation(leash, startBounds, endBounds);
}
boolean isRootOrSplitSideRoot = change.getParent() == null
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 75112b6..2e86448 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -179,7 +179,9 @@
out.getChanges().add(info.getChanges().get(i));
}
}
- out.setRootLeash(info.getRootLeash(), info.getRootOffset().x, info.getRootOffset().y);
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ out.addRoot(info.getRoot(i));
+ }
out.setAnimationOptions(info.getAnimationOptions());
return out;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index f66c26b..63c7969 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -383,9 +383,10 @@
continue;
}
// No default animation for this, so just update bounds/position.
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
startTransaction.setPosition(change.getLeash(),
- change.getEndAbsBounds().left - info.getRootOffset().x,
- change.getEndAbsBounds().top - info.getRootOffset().y);
+ change.getEndAbsBounds().left - info.getRoot(rootIdx).getOffset().x,
+ change.getEndAbsBounds().top - info.getRoot(rootIdx).getOffset().y);
// Seamless display transition doesn't need to animate.
if (isSeamlessDisplayChange) continue;
if (isTask || (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)
@@ -474,8 +475,10 @@
}
if (backgroundColorForTransition != 0) {
- addBackgroundToTransition(info.getRootLeash(), backgroundColorForTransition,
- startTransaction, finishTransaction);
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ addBackgroundToTransition(info.getRoot(i).getLeash(), backgroundColorForTransition,
+ startTransaction, finishTransaction);
+ }
}
if (postStartTransactionCallbacks.size() > 0) {
@@ -520,8 +523,10 @@
private void startRotationAnimation(SurfaceControl.Transaction startTransaction,
TransitionInfo.Change change, TransitionInfo info, int animHint,
ArrayList<Animator> animations, Runnable onAnimFinish) {
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mSurfaceSession,
- mTransactionPool, startTransaction, change, info.getRootLeash(), animHint);
+ mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(),
+ animHint);
// The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real
// content, and background color. The item of "animGroup" will be removed if the sub
// animation is finished. Then if the list becomes empty, the rotation animation is done.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 27b82c0..039bde9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -75,6 +75,7 @@
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.Arrays;
@@ -415,8 +416,8 @@
private static void setupAnimHierarchy(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) {
boolean isOpening = isOpeningType(info.getType());
- if (info.getRootLeash().isValid()) {
- t.show(info.getRootLeash());
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ t.show(info.getRoot(i).getLeash());
}
final int numChanges = info.getChanges().size();
// Put animating stuff above this line and put static stuff below it.
@@ -434,10 +435,12 @@
boolean hasParent = change.getParent() != null;
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
if (!hasParent) {
- t.reparent(leash, info.getRootLeash());
- t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x,
- change.getStartAbsBounds().top - info.getRootOffset().y);
+ t.reparent(leash, info.getRoot(rootIdx).getLeash());
+ t.setPosition(leash,
+ change.getStartAbsBounds().left - info.getRoot(rootIdx).getOffset().x,
+ change.getStartAbsBounds().top - info.getRoot(rootIdx).getOffset().y);
}
final int layer;
// Put all the OPEN/SHOW on top
@@ -532,12 +535,6 @@
if (info.getType() == TRANSIT_SLEEP) {
if (activeIdx > 0) {
- if (!info.getRootLeash().isValid()) {
- // Shell has some debug settings which makes calling binders with invalid
- // surfaces crash, so replace it with a "real" one.
- info.setRootLeash(new SurfaceControl.Builder().setName("Invalid")
- .setContainerLayer().build(), 0, 0);
- }
// Sleep starts a process of forcing all prior transitions to finish immediately
finishForSleep(null /* forceFinish */);
return;
@@ -546,10 +543,10 @@
// Allow to notify keyguard un-occluding state to KeyguardService, which can happen while
// screen-off, so there might no visibility change involved.
- if (!info.getRootLeash().isValid() && info.getType() != TRANSIT_KEYGUARD_UNOCCLUDE) {
- // Invalid root-leash implies that the transition is empty/no-op, so just do
+ if (info.getRootCount() == 0 && info.getType() != TRANSIT_KEYGUARD_UNOCCLUDE) {
+ // No root-leashes implies that the transition is empty/no-op, so just do
// housekeeping and return.
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Invalid root leash (%s): %s",
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "No transition roots (%s): %s",
transitionToken, info);
onAbort(active);
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java
index 8c6e1e7..7595c96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java
@@ -139,11 +139,12 @@
// changes should be ordered top-to-bottom in z
final int mode = change.getMode();
- t.reparent(leash, info.getRootLeash());
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
+ t.reparent(leash, info.getRoot(rootIdx).getLeash());
final Rect absBounds =
(mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds();
- t.setPosition(leash, absBounds.left - info.getRootOffset().x,
- absBounds.top - info.getRootOffset().y);
+ t.setPosition(leash, absBounds.left - info.getRoot(rootIdx).getOffset().x,
+ absBounds.top - info.getRoot(rootIdx).getOffset().y);
// Put all the OPEN/SHOW on top
if (TransitionUtil.isOpeningType(mode)) {
@@ -179,12 +180,13 @@
// making leashes means we have to handle them specially.
return change.getLeash();
}
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
SurfaceControl leashSurface = new SurfaceControl.Builder()
.setName(change.getLeash().toString() + "_transition-leash")
.setContainerLayer()
// Initial the surface visible to respect the visibility of the original surface.
.setHidden(false)
- .setParent(info.getRootLeash())
+ .setParent(info.getRoot(rootIdx).getLeash())
.build();
// Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
setupLeash(leashSurface, change, info.getChanges().size() - order, info, t);
@@ -261,4 +263,18 @@
target.setRotationChange(change.getEndRotation() - change.getStartRotation());
return target;
}
+
+ /**
+ * Finds the "correct" root idx for a change. The change's end display is prioritized, then
+ * the start display. If there is no display, it will fallback on the 0th root in the
+ * transition. There MUST be at-least 1 root in the transition (ie. it's not a no-op).
+ */
+ public static int rootIndexFor(@NonNull TransitionInfo.Change change,
+ @NonNull TransitionInfo info) {
+ int rootIdx = info.findRootIndex(change.getEndDisplayId());
+ if (rootIdx >= 0) return rootIdx;
+ rootIdx = info.findRootIndex(change.getStartDisplayId());
+ if (rootIdx >= 0) return rootIdx;
+ return 0;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 72da108..3c0ef96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -23,6 +23,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
+import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Point;
@@ -46,6 +47,7 @@
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.desktopmode.DesktopTasksController;
/**
* Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
@@ -95,6 +97,17 @@
mDesktopActive = DesktopModeStatus.isActive(mContext);
}
+ @Override
+ protected Configuration getConfigurationWithOverrides(
+ ActivityManager.RunningTaskInfo taskInfo) {
+ Configuration configuration = taskInfo.getConfiguration();
+ if (DesktopTasksController.isDesktopDensityOverrideSet()) {
+ // Density is overridden for desktop tasks. Keep system density for window decoration.
+ configuration.densityDpi = mContext.getResources().getConfiguration().densityDpi;
+ }
+ return configuration;
+ }
+
void setCaptionListeners(
View.OnClickListener onCaptionButtonClickListener,
View.OnTouchListener onCaptionTouchListener) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS
new file mode 100644
index 0000000..4417209
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS
@@ -0,0 +1 @@
+jorgegil@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 7a7ac47..ddd3b44 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -131,7 +131,17 @@
mSurfaceControlViewHostFactory = surfaceControlViewHostFactory;
mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId);
- mDecorWindowContext = mContext.createConfigurationContext(mTaskInfo.getConfiguration());
+ mDecorWindowContext = mContext.createConfigurationContext(
+ getConfigurationWithOverrides(mTaskInfo));
+ }
+
+ /**
+ * Get {@link Configuration} from supplied {@link RunningTaskInfo}.
+ *
+ * Allows values to be overridden before returning the configuration.
+ */
+ protected Configuration getConfigurationWithOverrides(RunningTaskInfo taskInfo) {
+ return taskInfo.getConfiguration();
}
/**
@@ -165,7 +175,7 @@
outResult.mRootView = rootView;
rootView = null; // Clear it just in case we use it accidentally
- final Configuration taskConfig = mTaskInfo.getConfiguration();
+ final Configuration taskConfig = getConfigurationWithOverrides(mTaskInfo);
if (oldTaskConfig.densityDpi != taskConfig.densityDpi
|| mDisplay == null
|| mDisplay.getDisplayId() != mTaskInfo.displayId) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java
index 35c374d..26b787f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java
@@ -30,6 +30,7 @@
*/
public class TransitionInfoBuilder {
final TransitionInfo mInfo;
+ static final int DISPLAY_ID = 0;
public TransitionInfoBuilder(@WindowManager.TransitionType int type) {
this(type, 0 /* flags */);
@@ -38,7 +39,7 @@
public TransitionInfoBuilder(@WindowManager.TransitionType int type,
@WindowManager.TransitionFlags int flags) {
mInfo = new TransitionInfo(type, flags);
- mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0);
+ mInfo.addRootLeash(DISPLAY_ID, createMockSurface(true /* valid */), 0, 0);
}
public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
@@ -61,6 +62,7 @@
}
public TransitionInfoBuilder addChange(TransitionInfo.Change change) {
+ change.setDisplayId(DISPLAY_ID, DISPLAY_ID);
mInfo.addChange(change);
return this;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 0e14c69..108e273 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -53,6 +53,7 @@
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.TabletopModeController;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.PipAnimationController;
@@ -115,6 +116,7 @@
@Mock private Optional<OneHandedController> mMockOneHandedController;
@Mock private PipParamsChangedForwarder mMockPipParamsChangedForwarder;
@Mock private DisplayInsetsController mMockDisplayInsetsController;
+ @Mock private TabletopModeController mMockTabletopModeController;
@Mock private DisplayLayout mMockDisplayLayout1;
@Mock private DisplayLayout mMockDisplayLayout2;
@@ -137,7 +139,8 @@
mMockPipTaskOrganizer, mMockPipTransitionState, mMockPipTouchHandler,
mMockPipTransitionController, mMockWindowManagerShellWrapper,
mMockTaskStackListener, mMockPipParamsChangedForwarder,
- mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor);
+ mMockDisplayInsetsController, mMockTabletopModeController,
+ mMockOneHandedController, mMockExecutor);
mShellInit.init();
when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm);
when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper);
@@ -228,7 +231,8 @@
mMockPipTaskOrganizer, mMockPipTransitionState, mMockPipTouchHandler,
mMockPipTransitionController, mMockWindowManagerShellWrapper,
mMockTaskStackListener, mMockPipParamsChangedForwarder,
- mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor));
+ mMockDisplayInsetsController, mMockTabletopModeController,
+ mMockOneHandedController, mMockExecutor));
}
@Test
diff --git a/libs/hwui/effects/GainmapRenderer.cpp b/libs/hwui/effects/GainmapRenderer.cpp
index 8977d3c..bfe4eaf 100644
--- a/libs/hwui/effects/GainmapRenderer.cpp
+++ b/libs/hwui/effects/GainmapRenderer.cpp
@@ -23,21 +23,55 @@
#include "utils/Trace.h"
#ifdef __ANDROID__
+#include "include/core/SkColorSpace.h"
+#include "include/core/SkImage.h"
+#include "include/core/SkShader.h"
+#include "include/effects/SkRuntimeEffect.h"
+#include "include/private/SkGainmapInfo.h"
#include "renderthread/CanvasContext.h"
+#include "src/core/SkColorFilterPriv.h"
+#include "src/core/SkImageInfoPriv.h"
+#include "src/core/SkRuntimeEffectPriv.h"
#endif
namespace android::uirenderer {
using namespace renderthread;
+static float getTargetHdrSdrRatio(const SkColorSpace* destColorspace) {
+ // We should always have a known destination colorspace. If we don't we must be in some
+ // legacy mode where we're lost and also definitely not going to HDR
+ if (destColorspace == nullptr) {
+ return 1.f;
+ }
+
+ constexpr float GenericSdrWhiteNits = 203.f;
+ constexpr float maxPQLux = 10000.f;
+ constexpr float maxHLGLux = 1000.f;
+ skcms_TransferFunction destTF;
+ destColorspace->transferFn(&destTF);
+ if (skcms_TransferFunction_isPQish(&destTF)) {
+ return maxPQLux / GenericSdrWhiteNits;
+ } else if (skcms_TransferFunction_isHLGish(&destTF)) {
+ return maxHLGLux / GenericSdrWhiteNits;
+ } else {
+#ifdef __ANDROID__
+ CanvasContext* context = CanvasContext::getActiveContext();
+ return context ? context->targetSdrHdrRatio() : 1.f;
+#else
+ return 1.f;
+#endif
+ }
+}
+
void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkRect& src,
const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint* paint,
SkCanvas::SrcRectConstraint constraint,
const sk_sp<const SkImage>& gainmapImage, const SkGainmapInfo& gainmapInfo) {
ATRACE_CALL();
#ifdef __ANDROID__
- CanvasContext* context = CanvasContext::getActiveContext();
- float targetSdrHdrRatio = context ? context->targetSdrHdrRatio() : 1.f;
+ auto destColorspace = c->imageInfo().refColorSpace();
+ float targetSdrHdrRatio = getTargetHdrSdrRatio(destColorspace.get());
if (targetSdrHdrRatio > 1.f && gainmapImage) {
SkPaint gainmapPaint = *paint;
float sX = gainmapImage->width() / (float)image->width();
@@ -48,9 +82,9 @@
gainmapSrc.fRight *= sX;
gainmapSrc.fTop *= sY;
gainmapSrc.fBottom *= sY;
- auto shader = SkGainmapShader::Make(image, src, sampling, gainmapImage, gainmapSrc,
- sampling, gainmapInfo, dst, targetSdrHdrRatio,
- c->imageInfo().refColorSpace());
+ auto shader =
+ SkGainmapShader::Make(image, src, sampling, gainmapImage, gainmapSrc, sampling,
+ gainmapInfo, dst, targetSdrHdrRatio, destColorspace);
gainmapPaint.setShader(shader);
c->drawRect(dst, gainmapPaint);
} else
@@ -58,4 +92,213 @@
c->drawImageRect(image.get(), src, dst, sampling, paint, constraint);
}
+#ifdef __ANDROID__
+
+static constexpr char gGainmapSKSL[] = R"SKSL(
+ uniform shader base;
+ uniform shader gainmap;
+ uniform colorFilter workingSpaceToLinearSrgb;
+ uniform half4 logRatioMin;
+ uniform half4 logRatioMax;
+ uniform half4 gainmapGamma;
+ uniform half4 epsilonSdr;
+ uniform half4 epsilonHdr;
+ uniform half W;
+ uniform int gainmapIsAlpha;
+ uniform int gainmapIsRed;
+ uniform int singleChannel;
+ uniform int noGamma;
+
+ half4 toDest(half4 working) {
+ half4 ls = workingSpaceToLinearSrgb.eval(working);
+ vec3 dest = fromLinearSrgb(ls.rgb);
+ return half4(dest.r, dest.g, dest.b, ls.a);
+ }
+
+ half4 main(float2 coord) {
+ half4 S = base.eval(coord);
+ half4 G = gainmap.eval(coord);
+ if (gainmapIsAlpha == 1) {
+ G = half4(G.a, G.a, G.a, 1.0);
+ }
+ if (gainmapIsRed == 1) {
+ G = half4(G.r, G.r, G.r, 1.0);
+ }
+ if (singleChannel == 1) {
+ half L;
+ if (noGamma == 1) {
+ L = mix(logRatioMin.r, logRatioMax.r, G.r);
+ } else {
+ L = mix(logRatioMin.r, logRatioMax.r, pow(G.r, gainmapGamma.r));
+ }
+ half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb;
+ return toDest(half4(H.r, H.g, H.b, S.a));
+ } else {
+ half3 L;
+ if (noGamma == 1) {
+ L = mix(logRatioMin.rgb, logRatioMax.rgb, G.rgb);
+ } else {
+ L = mix(logRatioMin.rgb, logRatioMax.rgb, pow(G.rgb, gainmapGamma.rgb));
+ }
+ half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb;
+ return toDest(half4(H.r, H.g, H.b, S.a));
+ }
+ }
+)SKSL";
+
+static sk_sp<SkRuntimeEffect> gainmap_apply_effect() {
+ static const SkRuntimeEffect* effect = []() -> SkRuntimeEffect* {
+ auto buildResult = SkRuntimeEffect::MakeForShader(SkString(gGainmapSKSL), {});
+ if (buildResult.effect) {
+ return buildResult.effect.release();
+ } else {
+ LOG_ALWAYS_FATAL("Failed to build gainmap shader: %s", buildResult.errorText.c_str());
+ }
+ }();
+ SkASSERT(effect);
+ return sk_ref_sp(effect);
+}
+
+static bool all_channels_equal(const SkColor4f& c) {
+ return c.fR == c.fG && c.fR == c.fB;
+}
+
+class DeferredGainmapShader {
+private:
+ sk_sp<SkRuntimeEffect> mShader{gainmap_apply_effect()};
+ SkRuntimeShaderBuilder mBuilder{mShader};
+ SkGainmapInfo mGainmapInfo;
+ std::mutex mUniformGuard;
+
+ void setupChildren(const sk_sp<const SkImage>& baseImage,
+ const sk_sp<const SkImage>& gainmapImage, SkTileMode tileModeX,
+ SkTileMode tileModeY, const SkSamplingOptions& samplingOptions) {
+ sk_sp<SkColorSpace> baseColorSpace =
+ baseImage->colorSpace() ? baseImage->refColorSpace() : SkColorSpace::MakeSRGB();
+
+ // Determine the color space in which the gainmap math is to be applied.
+ sk_sp<SkColorSpace> gainmapMathColorSpace = baseColorSpace->makeLinearGamma();
+
+ // Create a color filter to transform from the base image's color space to the color space
+ // in which the gainmap is to be applied.
+ auto colorXformSdrToGainmap =
+ SkColorFilterPriv::MakeColorSpaceXform(baseColorSpace, gainmapMathColorSpace);
+
+ // The base image shader will convert into the color space in which the gainmap is applied.
+ auto baseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions)
+ ->makeWithColorFilter(colorXformSdrToGainmap);
+
+ // The gainmap image shader will ignore any color space that the gainmap has.
+ const SkMatrix gainmapRectToDstRect =
+ SkMatrix::RectToRect(SkRect::MakeWH(gainmapImage->width(), gainmapImage->height()),
+ SkRect::MakeWH(baseImage->width(), baseImage->height()));
+ auto gainmapImageShader = gainmapImage->makeRawShader(tileModeX, tileModeY, samplingOptions,
+ &gainmapRectToDstRect);
+
+ // Create a color filter to transform from the color space in which the gainmap is applied
+ // to the intermediate destination color space.
+ auto colorXformGainmapToDst = SkColorFilterPriv::MakeColorSpaceXform(
+ gainmapMathColorSpace, SkColorSpace::MakeSRGBLinear());
+
+ mBuilder.child("base") = std::move(baseImageShader);
+ mBuilder.child("gainmap") = std::move(gainmapImageShader);
+ mBuilder.child("workingSpaceToLinearSrgb") = std::move(colorXformGainmapToDst);
+ }
+
+ void setupGenericUniforms(const sk_sp<const SkImage>& gainmapImage,
+ const SkGainmapInfo& gainmapInfo) {
+ const SkColor4f logRatioMin({sk_float_log(gainmapInfo.fGainmapRatioMin.fR),
+ sk_float_log(gainmapInfo.fGainmapRatioMin.fG),
+ sk_float_log(gainmapInfo.fGainmapRatioMin.fB), 1.f});
+ const SkColor4f logRatioMax({sk_float_log(gainmapInfo.fGainmapRatioMax.fR),
+ sk_float_log(gainmapInfo.fGainmapRatioMax.fG),
+ sk_float_log(gainmapInfo.fGainmapRatioMax.fB), 1.f});
+ const int noGamma = gainmapInfo.fGainmapGamma.fR == 1.f &&
+ gainmapInfo.fGainmapGamma.fG == 1.f &&
+ gainmapInfo.fGainmapGamma.fB == 1.f;
+ const uint32_t colorTypeFlags = SkColorTypeChannelFlags(gainmapImage->colorType());
+ const int gainmapIsAlpha = colorTypeFlags == kAlpha_SkColorChannelFlag;
+ const int gainmapIsRed = colorTypeFlags == kRed_SkColorChannelFlag;
+ const int singleChannel = all_channels_equal(gainmapInfo.fGainmapGamma) &&
+ all_channels_equal(gainmapInfo.fGainmapRatioMin) &&
+ all_channels_equal(gainmapInfo.fGainmapRatioMax) &&
+ (colorTypeFlags == kGray_SkColorChannelFlag ||
+ colorTypeFlags == kAlpha_SkColorChannelFlag ||
+ colorTypeFlags == kRed_SkColorChannelFlag);
+ mBuilder.uniform("logRatioMin") = logRatioMin;
+ mBuilder.uniform("logRatioMax") = logRatioMax;
+ mBuilder.uniform("gainmapGamma") = gainmapInfo.fGainmapGamma;
+ mBuilder.uniform("epsilonSdr") = gainmapInfo.fEpsilonSdr;
+ mBuilder.uniform("epsilonHdr") = gainmapInfo.fEpsilonHdr;
+ mBuilder.uniform("noGamma") = noGamma;
+ mBuilder.uniform("singleChannel") = singleChannel;
+ mBuilder.uniform("gainmapIsAlpha") = gainmapIsAlpha;
+ mBuilder.uniform("gainmapIsRed") = gainmapIsRed;
+ }
+
+ sk_sp<const SkData> build(float targetHdrSdrRatio) {
+ sk_sp<const SkData> uniforms;
+ {
+ // If we are called concurrently from multiple threads, we need to guard the call
+ // to writableUniforms() which mutates mUniform. This is otherwise safe because
+ // writeableUniforms() will make a copy if it's not unique before mutating
+ // This can happen if a BitmapShader is used on multiple canvas', such as a
+ // software + hardware canvas, which is otherwise valid as SkShader is "immutable"
+ std::lock_guard _lock(mUniformGuard);
+ const float Wunclamped = (sk_float_log(targetHdrSdrRatio) -
+ sk_float_log(mGainmapInfo.fDisplayRatioSdr)) /
+ (sk_float_log(mGainmapInfo.fDisplayRatioHdr) -
+ sk_float_log(mGainmapInfo.fDisplayRatioSdr));
+ const float W = std::max(std::min(Wunclamped, 1.f), 0.f);
+ mBuilder.uniform("W") = W;
+ uniforms = mBuilder.uniforms();
+ }
+ return uniforms;
+ }
+
+public:
+ explicit DeferredGainmapShader(const sk_sp<const SkImage>& image,
+ const sk_sp<const SkImage>& gainmapImage,
+ const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
+ SkTileMode tileModeY, const SkSamplingOptions& sampling) {
+ mGainmapInfo = gainmapInfo;
+ setupChildren(image, gainmapImage, tileModeX, tileModeY, sampling);
+ setupGenericUniforms(gainmapImage, gainmapInfo);
+ }
+
+ static sk_sp<SkShader> Make(const sk_sp<const SkImage>& image,
+ const sk_sp<const SkImage>& gainmapImage,
+ const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
+ SkTileMode tileModeY, const SkSamplingOptions& sampling) {
+ auto deferredHandler = std::make_shared<DeferredGainmapShader>(
+ image, gainmapImage, gainmapInfo, tileModeX, tileModeY, sampling);
+ auto callback =
+ [deferredHandler](const SkRuntimeEffectPriv::UniformsCallbackContext& renderContext)
+ -> sk_sp<const SkData> {
+ return deferredHandler->build(getTargetHdrSdrRatio(renderContext.fDstColorSpace));
+ };
+ return SkRuntimeEffectPriv::MakeDeferredShader(deferredHandler->mShader.get(), callback,
+ deferredHandler->mBuilder.children());
+ }
+};
+
+sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image,
+ const sk_sp<const SkImage>& gainmapImage,
+ const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
+ SkTileMode tileModeY, const SkSamplingOptions& sampling) {
+ return DeferredGainmapShader::Make(image, gainmapImage, gainmapInfo, tileModeX, tileModeY,
+ sampling);
+}
+
+#else // __ANDROID__
+
+sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image,
+ const sk_sp<const SkImage>& gainmapImage,
+ const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
+ SkTileMode tileModeY, const SkSamplingOptions& sampling) {
+ return nullptr;
+}
+
+#endif // __ANDROID__
+
} // namespace android::uirenderer
\ No newline at end of file
diff --git a/libs/hwui/effects/GainmapRenderer.h b/libs/hwui/effects/GainmapRenderer.h
index 7c56d94..4ed2445 100644
--- a/libs/hwui/effects/GainmapRenderer.h
+++ b/libs/hwui/effects/GainmapRenderer.h
@@ -30,4 +30,9 @@
SkCanvas::SrcRectConstraint constraint,
const sk_sp<const SkImage>& gainmapImage, const SkGainmapInfo& gainmapInfo);
+sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image,
+ const sk_sp<const SkImage>& gainmapImage,
+ const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
+ SkTileMode tileModeY, const SkSamplingOptions& sampling);
+
} // namespace android::uirenderer
diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp
index 75d45e5..7eb79be 100644
--- a/libs/hwui/jni/Shader.cpp
+++ b/libs/hwui/jni/Shader.cpp
@@ -1,6 +1,9 @@
#undef LOG_TAG
#define LOG_TAG "ShaderJNI"
+#include <vector>
+
+#include "Gainmap.h"
#include "GraphicsJNI.h"
#include "SkBitmap.h"
#include "SkBlendMode.h"
@@ -17,10 +20,9 @@
#include "SkShader.h"
#include "SkString.h"
#include "SkTileMode.h"
+#include "effects/GainmapRenderer.h"
#include "include/effects/SkRuntimeEffect.h"
-#include <vector>
-
using namespace android::uirenderer;
/**
@@ -74,7 +76,20 @@
if (bitmapHandle) {
// Only pass a valid SkBitmap object to the constructor if the Bitmap exists. Otherwise,
// we'll pass an empty SkBitmap to avoid crashing/excepting for compatibility.
- image = android::bitmap::toBitmap(bitmapHandle).makeImage();
+ auto& bitmap = android::bitmap::toBitmap(bitmapHandle);
+ image = bitmap.makeImage();
+
+ if (!isDirectSampled && bitmap.hasGainmap()) {
+ sk_sp<SkShader> gainmapShader = MakeGainmapShader(
+ image, bitmap.gainmap()->bitmap->makeImage(), bitmap.gainmap()->info,
+ (SkTileMode)tileModeX, (SkTileMode)tileModeY, sampling);
+ if (gainmapShader) {
+ if (matrix) {
+ gainmapShader = gainmapShader->makeWithLocalMatrix(*matrix);
+ }
+ return reinterpret_cast<jlong>(gainmapShader.release());
+ }
+ }
}
if (!image.get()) {
diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp
index 7a7f1ab..0afd949 100644
--- a/libs/hwui/renderthread/RenderThread.cpp
+++ b/libs/hwui/renderthread/RenderThread.cpp
@@ -135,7 +135,9 @@
!mFrameCallbackTaskPending) {
ATRACE_NAME("queue mFrameCallbackTask");
mFrameCallbackTaskPending = true;
- nsecs_t runAt = (frameTimeNanos + mDispatchFrameDelay);
+
+ nsecs_t timeUntilDeadline = frameDeadline - frameTimeNanos;
+ nsecs_t runAt = (frameTimeNanos + (timeUntilDeadline * 0.25f));
queue().postAt(runAt, [=]() { dispatchFrameCallbacks(); });
}
}
@@ -257,7 +259,6 @@
void RenderThread::setupFrameInterval() {
nsecs_t frameIntervalNanos = DeviceInfo::getVsyncPeriod();
mTimeLord.setFrameInterval(frameIntervalNanos);
- mDispatchFrameDelay = static_cast<nsecs_t>(frameIntervalNanos * .25f);
}
void RenderThread::requireGlContext() {
diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h
index 0a89e5e..c77cd41 100644
--- a/libs/hwui/renderthread/RenderThread.h
+++ b/libs/hwui/renderthread/RenderThread.h
@@ -235,7 +235,6 @@
bool mFrameCallbackTaskPending;
TimeLord mTimeLord;
- nsecs_t mDispatchFrameDelay = 4_ms;
RenderState* mRenderState;
EglManager* mEglManager;
WebViewFunctorManager& mFunctorManager;
diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp
index 24cfc9d..c398405 100644
--- a/libs/input/MouseCursorController.cpp
+++ b/libs/input/MouseCursorController.cpp
@@ -63,24 +63,23 @@
mLocked.pointerSprite.clear();
}
-bool MouseCursorController::getBounds(float* outMinX, float* outMinY, float* outMaxX,
- float* outMaxY) const {
+std::optional<FloatRect> MouseCursorController::getBounds() const {
std::scoped_lock lock(mLock);
- return getBoundsLocked(outMinX, outMinY, outMaxX, outMaxY);
+ return getBoundsLocked();
}
-bool MouseCursorController::getBoundsLocked(float* outMinX, float* outMinY, float* outMaxX,
- float* outMaxY) const REQUIRES(mLock) {
+std::optional<FloatRect> MouseCursorController::getBoundsLocked() const REQUIRES(mLock) {
if (!mLocked.viewport.isValid()) {
- return false;
+ return {};
}
- *outMinX = mLocked.viewport.logicalLeft;
- *outMinY = mLocked.viewport.logicalTop;
- *outMaxX = mLocked.viewport.logicalRight - 1;
- *outMaxY = mLocked.viewport.logicalBottom - 1;
- return true;
+ return FloatRect{
+ static_cast<float>(mLocked.viewport.logicalLeft),
+ static_cast<float>(mLocked.viewport.logicalTop),
+ static_cast<float>(mLocked.viewport.logicalRight - 1),
+ static_cast<float>(mLocked.viewport.logicalBottom - 1),
+ };
}
void MouseCursorController::move(float deltaX, float deltaY) {
@@ -121,31 +120,19 @@
}
void MouseCursorController::setPositionLocked(float x, float y) REQUIRES(mLock) {
- float minX, minY, maxX, maxY;
- if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) {
- if (x <= minX) {
- mLocked.pointerX = minX;
- } else if (x >= maxX) {
- mLocked.pointerX = maxX;
- } else {
- mLocked.pointerX = x;
- }
- if (y <= minY) {
- mLocked.pointerY = minY;
- } else if (y >= maxY) {
- mLocked.pointerY = maxY;
- } else {
- mLocked.pointerY = y;
- }
- updatePointerLocked();
- }
+ const auto bounds = getBoundsLocked();
+ if (!bounds) return;
+
+ mLocked.pointerX = std::max(bounds->left, std::min(bounds->right, x));
+ mLocked.pointerY = std::max(bounds->top, std::min(bounds->bottom, y));
+
+ updatePointerLocked();
}
-void MouseCursorController::getPosition(float* outX, float* outY) const {
+FloatPoint MouseCursorController::getPosition() const {
std::scoped_lock lock(mLock);
- *outX = mLocked.pointerX;
- *outY = mLocked.pointerY;
+ return {mLocked.pointerX, mLocked.pointerY};
}
int32_t MouseCursorController::getDisplayId() const {
@@ -235,10 +222,9 @@
// Reset cursor position to center if size or display changed.
if (oldViewport.displayId != viewport.displayId || oldDisplayWidth != newDisplayWidth ||
oldDisplayHeight != newDisplayHeight) {
- float minX, minY, maxX, maxY;
- if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) {
- mLocked.pointerX = (minX + maxX) * 0.5f;
- mLocked.pointerY = (minY + maxY) * 0.5f;
+ if (const auto bounds = getBoundsLocked(); bounds) {
+ mLocked.pointerX = (bounds->left + bounds->right) * 0.5f;
+ mLocked.pointerY = (bounds->top + bounds->bottom) * 0.5f;
// Reload icon resources for density may be changed.
loadResourcesLocked(getAdditionalMouseResources);
} else {
diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h
index db0ab56..26be2a8 100644
--- a/libs/input/MouseCursorController.h
+++ b/libs/input/MouseCursorController.h
@@ -43,12 +43,12 @@
MouseCursorController(PointerControllerContext& context);
~MouseCursorController();
- bool getBounds(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const;
+ std::optional<FloatRect> getBounds() const;
void move(float deltaX, float deltaY);
void setButtonState(int32_t buttonState);
int32_t getButtonState() const;
void setPosition(float x, float y);
- void getPosition(float* outX, float* outY) const;
+ FloatPoint getPosition() const;
int32_t getDisplayId() const;
void fade(PointerControllerInterface::Transition transition);
void unfade(PointerControllerInterface::Transition transition);
@@ -102,7 +102,7 @@
} mLocked GUARDED_BY(mLock);
- bool getBoundsLocked(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const;
+ std::optional<FloatRect> getBoundsLocked() const;
void setPositionLocked(float x, float y);
void updatePointerLocked();
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index fedf58d..544edc2 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -114,16 +114,15 @@
PointerController::~PointerController() {
mDisplayInfoListener->onPointerControllerDestroyed();
mUnregisterWindowInfosListener(mDisplayInfoListener);
- mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, 0, 0);
+ mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, FloatPoint{0, 0});
}
std::mutex& PointerController::getLock() const {
return mDisplayInfoListener->mLock;
}
-bool PointerController::getBounds(float* outMinX, float* outMinY, float* outMaxX,
- float* outMaxY) const {
- return mCursorController.getBounds(outMinX, outMinY, outMaxX, outMaxY);
+std::optional<FloatRect> PointerController::getBounds() const {
+ return mCursorController.getBounds();
}
void PointerController::move(float deltaX, float deltaY) {
@@ -156,15 +155,13 @@
mCursorController.setPosition(transformed.x, transformed.y);
}
-void PointerController::getPosition(float* outX, float* outY) const {
+FloatPoint PointerController::getPosition() const {
const int32_t displayId = mCursorController.getDisplayId();
- mCursorController.getPosition(outX, outY);
+ const auto p = mCursorController.getPosition();
{
std::scoped_lock lock(getLock());
const auto& transform = getTransformForDisplayLocked(displayId);
- const auto xy = transform.inverse().transform(*outX, *outY);
- *outX = xy.x;
- *outY = xy.y;
+ return FloatPoint{transform.inverse().transform(p.x, p.y)};
}
}
@@ -262,19 +259,31 @@
}
void PointerController::setDisplayViewport(const DisplayViewport& viewport) {
- std::scoped_lock lock(getLock());
+ struct PointerDisplayChangeArgs {
+ int32_t displayId;
+ FloatPoint cursorPosition;
+ };
+ std::optional<PointerDisplayChangeArgs> pointerDisplayChanged;
- bool getAdditionalMouseResources = false;
- if (mLocked.presentation == PointerController::Presentation::POINTER ||
- mLocked.presentation == PointerController::Presentation::STYLUS_HOVER) {
- getAdditionalMouseResources = true;
- }
- mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources);
- if (viewport.displayId != mLocked.pointerDisplayId) {
- float xPos, yPos;
- mCursorController.getPosition(&xPos, &yPos);
- mContext.getPolicy()->onPointerDisplayIdChanged(viewport.displayId, xPos, yPos);
- mLocked.pointerDisplayId = viewport.displayId;
+ { // acquire lock
+ std::scoped_lock lock(getLock());
+
+ bool getAdditionalMouseResources = false;
+ if (mLocked.presentation == PointerController::Presentation::POINTER ||
+ mLocked.presentation == PointerController::Presentation::STYLUS_HOVER) {
+ getAdditionalMouseResources = true;
+ }
+ mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources);
+ if (viewport.displayId != mLocked.pointerDisplayId) {
+ mLocked.pointerDisplayId = viewport.displayId;
+ pointerDisplayChanged = {viewport.displayId, mCursorController.getPosition()};
+ }
+ } // release lock
+
+ if (pointerDisplayChanged) {
+ // Notify the policy without holding the pointer controller lock.
+ mContext.getPolicy()->onPointerDisplayIdChanged(pointerDisplayChanged->displayId,
+ pointerDisplayChanged->cursorPosition);
}
}
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index 48d5a57..6d3557c 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -50,12 +50,12 @@
~PointerController() override;
- virtual bool getBounds(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const;
+ virtual std::optional<FloatRect> getBounds() const;
virtual void move(float deltaX, float deltaY);
virtual void setButtonState(int32_t buttonState);
virtual int32_t getButtonState() const;
virtual void setPosition(float x, float y);
- virtual void getPosition(float* outX, float* outY) const;
+ virtual FloatPoint getPosition() const;
virtual int32_t getDisplayId() const;
virtual void fade(Transition transition);
virtual void unfade(Transition transition);
diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h
index 96d83a5..f6f5d3b 100644
--- a/libs/input/PointerControllerContext.h
+++ b/libs/input/PointerControllerContext.h
@@ -81,7 +81,7 @@
virtual PointerIconStyle getDefaultPointerIconId() = 0;
virtual PointerIconStyle getDefaultStylusIconId() = 0;
virtual PointerIconStyle getCustomPointerIconId() = 0;
- virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) = 0;
+ virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) = 0;
};
/*
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index c820d00..2378d42 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -60,7 +60,7 @@
virtual PointerIconStyle getDefaultPointerIconId() override;
virtual PointerIconStyle getDefaultStylusIconId() override;
virtual PointerIconStyle getCustomPointerIconId() override;
- virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) override;
+ virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override;
bool allResourcesAreLoaded();
bool noResourcesAreLoaded();
@@ -143,8 +143,8 @@
}
void MockPointerControllerPolicyInterface::onPointerDisplayIdChanged(int32_t displayId,
- float /*xPos*/,
- float /*yPos*/) {
+ const FloatPoint& /*position*/
+) {
latestPointerDisplayId = displayId;
}
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 23f87ab..f86b9af 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -1566,7 +1566,7 @@
FileInputStream in = null;
try {
in = new FileInputStream(fileDescriptor);
- loadAttributes(in);
+ loadAttributes(in, fileDescriptor);
} finally {
closeQuietly(in);
if (isFdDuped) {
@@ -1637,7 +1637,7 @@
mSeekableFileDescriptor = null;
}
}
- loadAttributes(inputStream);
+ loadAttributes(inputStream, null);
}
/**
@@ -1963,7 +1963,7 @@
* This function decides which parser to read the image data according to the given input stream
* type and the content of the input stream.
*/
- private void loadAttributes(@NonNull InputStream in) {
+ private void loadAttributes(@NonNull InputStream in, @Nullable FileDescriptor fd) {
if (in == null) {
throw new NullPointerException("inputstream shouldn't be null");
}
@@ -1993,7 +1993,7 @@
break;
}
case IMAGE_TYPE_HEIF: {
- getHeifAttributes(inputStream);
+ getHeifAttributes(inputStream, fd);
break;
}
case IMAGE_TYPE_ORF: {
@@ -2580,7 +2580,7 @@
} else if (isSeekableFD(in.getFD())) {
mSeekableFileDescriptor = in.getFD();
}
- loadAttributes(in);
+ loadAttributes(in, null);
} finally {
closeQuietly(in);
if (modernFd != null) {
@@ -3068,59 +3068,66 @@
}
}
- private void getHeifAttributes(ByteOrderedDataInputStream in) throws IOException {
+ private void getHeifAttributes(ByteOrderedDataInputStream in, @Nullable FileDescriptor fd)
+ throws IOException {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
- retriever.setDataSource(new MediaDataSource() {
- long mPosition;
+ if (fd != null) {
+ retriever.setDataSource(fd);
+ } else {
+ retriever.setDataSource(new MediaDataSource() {
+ long mPosition;
- @Override
- public void close() throws IOException {}
+ @Override
+ public void close() throws IOException {}
- @Override
- public int readAt(long position, byte[] buffer, int offset, int size)
- throws IOException {
- if (size == 0) {
- return 0;
- }
- if (position < 0) {
+ @Override
+ public int readAt(long position, byte[] buffer, int offset, int size)
+ throws IOException {
+ if (size == 0) {
+ return 0;
+ }
+ if (position < 0) {
+ return -1;
+ }
+ try {
+ if (mPosition != position) {
+ // We don't allow seek to positions after the available bytes,
+ // the input stream won't be able to seek back then.
+ // However, if we hit an exception before (mPosition set to -1),
+ // let it try the seek in hope it might recover.
+ if (mPosition >= 0 && position >= mPosition + in.available()) {
+ return -1;
+ }
+ in.seek(position);
+ mPosition = position;
+ }
+
+ // If the read will cause us to go over the available bytes,
+ // reduce the size so that we stay in the available range.
+ // Otherwise the input stream may not be able to seek back.
+ if (size > in.available()) {
+ size = in.available();
+ }
+
+ int bytesRead = in.read(buffer, offset, size);
+ if (bytesRead >= 0) {
+ mPosition += bytesRead;
+ return bytesRead;
+ }
+ } catch (IOException e) {
+ // absorb the exception and fall through to the 'failed read' path below
+ }
+ mPosition = -1; // need to seek on next read
return -1;
}
- try {
- if (mPosition != position) {
- // We don't allow seek to positions after the available bytes,
- // the input stream won't be able to seek back then.
- // However, if we hit an exception before (mPosition set to -1),
- // let it try the seek in hope it might recover.
- if (mPosition >= 0 && position >= mPosition + in.available()) {
- return -1;
- }
- in.seek(position);
- mPosition = position;
- }
- // If the read will cause us to go over the available bytes,
- // reduce the size so that we stay in the available range.
- // Otherwise the input stream may not be able to seek back.
- if (size > in.available()) {
- size = in.available();
- }
-
- int bytesRead = in.read(buffer, offset, size);
- if (bytesRead >= 0) {
- mPosition += bytesRead;
- return bytesRead;
- }
- } catch (IOException e) {}
- mPosition = -1; // need to seek on next read
- return -1;
- }
-
- @Override
- public long getSize() throws IOException {
- return -1;
- }
- });
+ @Override
+ public long getSize() throws IOException {
+ return -1;
+ }
+ });
+ }
String exifOffsetStr = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET);
diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java
index 5bc8c04..0a0a626 100644
--- a/media/java/android/media/MediaCas.java
+++ b/media/java/android/media/MediaCas.java
@@ -436,7 +436,7 @@
if (mEventHandler != null) {
mEventHandler.sendMessage(
mEventHandler.obtainMessage(
- EventHandler.MSG_CAS_EVENT, event, arg, data));
+ EventHandler.MSG_CAS_EVENT, event, arg, toBytes(data)));
}
}
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index 9c629bb..b1b7d40 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -1210,7 +1210,7 @@
/**
* A key describing the desired bitrate mode to be used by an encoder.
- * Constants are declared in {@link MediaCodecInfo.CodecCapabilities}.
+ * Constants are declared in {@link MediaCodecInfo.EncoderCapabilities}.
*
* @see MediaCodecInfo.EncoderCapabilities#isBitrateModeSupported(int)
*/
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index 7d6a8b4..ef2b5a5 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -149,10 +149,45 @@
/**
* This constant is used as a {@link Bundle} key for TV messages. The value of the key
* identifies the stream on the TV input source for which the watermark event is relevant to.
+ *
+ * <p> Type: String
*/
public static final String TV_MESSAGE_KEY_STREAM_ID =
"android.media.tv.TvInputManager.stream_id";
+ /**
+ * This constant is used as a {@link Bundle} key for TV messages. The value of the key
+ * identifies the subtype of the data, such as the format of the CC data. The format
+ * found at this key can then be used to identify how to parse the data at
+ * {@link #TV_MESSAGE_KEY_RAW_DATA}.
+ *
+ * To parse the raw data bsed on the subtype, please refer to the official documentation of the
+ * concerning subtype. For example, for the subtype "ATSC A/335" for watermarking, the
+ * document for A/335 from the ATSC standard details how this data is formatted.
+ *
+ * Some other examples of common formats include:
+ * <ul>
+ * <li>Watermarking - ATSC A/336</li>
+ * <li>Closed Captioning - CTA 608-E</li>
+ * </ul>
+ *
+ * <p> Type: String
+ */
+ public static final String TV_MESSAGE_KEY_SUBTYPE =
+ "android.media.tv.TvInputManager.subtype";
+
+ /**
+ * This constant is used as a {@link Bundle} key for TV messages. The value of the key
+ * stores the raw data contained in this TV Message. The format of this data is determined
+ * by the format defined by the subtype, found using the key at
+ * {@link #TV_MESSAGE_KEY_SUBTYPE}. See {@link #TV_MESSAGE_KEY_SUBTYPE} for more
+ * information on how to parse this data.
+ *
+ * <p> Type: byte[]
+ */
+ public static final String TV_MESSAGE_KEY_RAW_DATA =
+ "android.media.tv.TvInputManager.raw_data";
+
static final int VIDEO_UNAVAILABLE_REASON_START = 0;
static final int VIDEO_UNAVAILABLE_REASON_END = 18;
@@ -802,7 +837,13 @@
*
* @param session A {@link TvInputManager.Session} associated with this callback.
* @param type The type of message received, such as {@link #TV_MESSAGE_TYPE_WATERMARK}
- * @param data The raw data of the message
+ * @param data The raw data of the message. The bundle keys are:
+ * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID},
+ * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE},
+ * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
+ * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
+ * how to parse this data.
+ *
*/
public void onTvMessage(Session session, @TvInputManager.TvMessageType int type,
Bundle data) {
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index 3c6ed91..4e380c4 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -1030,7 +1030,12 @@
*
* @param type The of message that was sent, such as
* {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK}
- * @param data The data sent with the message.
+ * @param data The raw data of the message. The bundle keys are:
+ * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID},
+ * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE},
+ * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
+ * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
+ * how to parse this data.
*/
public void notifyTvMessage(@TvInputManager.TvMessageType int type,
@NonNull Bundle data) {
@@ -1500,7 +1505,12 @@
*
* @param type The type of message received, such as
* {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK}
- * @param data The raw data of the message
+ * @param data The raw data of the message. The bundle keys are:
+ * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID},
+ * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE},
+ * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
+ * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
+ * how to parse this data.
*/
public void onTvMessage(@NonNull @TvInputManager.TvMessageType String type,
@NonNull Bundle data) {
diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java
index 19a2e5d..8a886832 100644
--- a/media/java/android/media/tv/TvView.java
+++ b/media/java/android/media/tv/TvView.java
@@ -1254,7 +1254,12 @@
* @param inputId The ID of the TV input bound to this view.
* @param type The type of message received, such as
* {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK}
- * @param data The raw data of the message
+ * @param data The raw data of the message. The bundle keys are:
+ * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID},
+ * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE},
+ * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
+ * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
+ * how to parse this data.
*/
public void onTvMessage(@NonNull String inputId,
@TvInputManager.TvMessageType int type, @NonNull Bundle data) {
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 7dfe16a..69ff9c6 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -919,7 +919,12 @@
*
* @param type The type of message received, such as
* {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK}
- * @param data The raw data of the message
+ * @param data The raw data of the message. The bundle keys are:
+ * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID},
+ * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE},
+ * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
+ * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
+ * how to parse this data.
*/
public void onTvMessage(@TvInputManager.TvMessageType int type,
@NonNull Bundle data) {
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 1a0319b..80a1435 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -944,7 +944,12 @@
*
* @param type The type of message received, such as
* {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK}
- * @param data The raw data of the message
+ * @param data The raw data of the message. The bundle keys are:
+ * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID},
+ * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE},
+ * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
+ * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
+ * how to parse this data.
*/
public void notifyTvMessage(@NonNull @TvInputManager.TvMessageType int type,
@NonNull Bundle data) {
diff --git a/packages/CarrierDefaultApp/res/values/strings.xml b/packages/CarrierDefaultApp/res/values/strings.xml
index e91d35b..f4e89a14 100644
--- a/packages/CarrierDefaultApp/res/values/strings.xml
+++ b/packages/CarrierDefaultApp/res/values/strings.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <string name="app_name">CarrierDefaultApp</string>
+ <string name="app_name">Carrier Communications</string>
<string name="android_system_label">Mobile Carrier</string>
<string name="portal_notification_id">Mobile data has run out</string>
<string name="no_data_notification_id">Your mobile data has been deactivated</string>
@@ -17,9 +17,9 @@
<!-- Telephony notification channel name for performance boost notifications. -->
<string name="performance_boost_notification_channel">Performance boost</string>
<!-- Notification title text for the performance boost notification. -->
- <string name="performance_boost_notification_title">Improve your app experience</string>
+ <string name="performance_boost_notification_title">5G options from your carrier</string>
<!-- Notification detail text for the performance boost notification. -->
- <string name="performance_boost_notification_detail">Tap to visit %s\'s website and learn more</string>
+ <string name="performance_boost_notification_detail">Visit %s\'s website to see options for your app experience</string>
<!-- Notification button text to cancel the performance boost notification. -->
<string name="performance_boost_notification_button_not_now">Not now</string>
<!-- Notification button text to manage the performance boost notification. -->
diff --git a/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm b/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm
index 1614188..9c2064c 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm
@@ -22,89 +22,89 @@
key GRAVE {
label: '`'
- base, capslock: '\u0630'
+ base: '\u0630'
shift: '\u0651'
}
key 1 {
label: '1'
base: '\u0661'
- shift: '!'
capslock: '1'
+ shift: '!'
}
key 2 {
label: '2'
base: '\u0662'
- shift: '@'
capslock: '2'
+ shift: '@'
}
key 3 {
label: '3'
base: '\u0663'
- shift: '#'
capslock: '3'
+ shift: '#'
}
key 4 {
label: '4'
base: '\u0664'
- shift: '$'
capslock: '4'
+ shift: '$'
}
key 5 {
label: '5'
base: '\u0665'
- shift: '%'
capslock: '5'
+ shift: '%'
}
key 6 {
label: '6'
base: '\u0666'
- shift: '^'
capslock: '6'
+ shift: '^'
}
key 7 {
label: '7'
base: '\u0667'
- shift: '&'
capslock: '7'
+ shift: '&'
}
key 8 {
label: '8'
base: '\u0668'
- shift: '*'
capslock: '8'
+ shift: '*'
}
key 9 {
label: '9'
base: '\u0669'
- shift: ')'
capslock: '9'
+ shift: ')'
}
key 0 {
label: '0'
base: '\u0660'
- shift: '('
capslock: '0'
+ shift: '('
}
key MINUS {
label: '-'
- base, capslock: '-'
+ base: '-'
shift: '_'
}
key EQUALS {
label: '='
- base, capslock: '='
+ base: '='
shift: '+'
}
@@ -112,79 +112,79 @@
key Q {
label: 'Q'
- base, capslock: '\u0636'
+ base: '\u0636'
shift: '\u064e'
}
key W {
label: 'W'
- base, capslock: '\u0635'
+ base: '\u0635'
shift: '\u064b'
}
key E {
label: 'E'
- base, capslock: '\u062b'
+ base: '\u062b'
shift: '\u064f'
}
key R {
label: 'R'
- base, capslock: '\u0642'
+ base: '\u0642'
shift: '\u064c'
}
key T {
label: 'T'
- base, capslock: '\u0641'
+ base: '\u0641'
shift: '\ufef9'
}
key Y {
label: 'Y'
- base, capslock: '\u063a'
+ base: '\u063a'
shift: '\u0625'
}
key U {
label: 'U'
- base, capslock: '\u0639'
+ base: '\u0639'
shift: '\u2018'
}
key I {
label: 'I'
- base, capslock: '\u0647'
+ base: '\u0647'
shift: '\u00f7'
}
key O {
label: 'O'
- base, capslock: '\u062e'
+ base: '\u062e'
shift: '\u00d7'
}
key P {
label: 'P'
- base, capslock: '\u062d'
+ base: '\u062d'
shift: '\u061b'
}
key LEFT_BRACKET {
label: ']'
- base, capslock: '\u062c'
+ base: '\u062c'
shift: '>'
}
key RIGHT_BRACKET {
label: '['
- base, capslock: '\u062f'
+ base: '\u062f'
shift: '<'
}
key BACKSLASH {
label: '\\'
- base, capslock: '\\'
+ base: '\\'
shift: '|'
}
@@ -192,67 +192,67 @@
key A {
label: 'A'
- base, capslock: '\u0634'
+ base: '\u0634'
shift: '\u0650'
}
key S {
label: 'S'
- base, capslock: '\u0633'
+ base: '\u0633'
shift: '\u064d'
}
key D {
label: 'D'
- base, capslock: '\u064a'
+ base: '\u064a'
shift: ']'
}
key F {
label: 'F'
- base, capslock: '\u0628'
+ base: '\u0628'
shift: '['
}
key G {
label: 'G'
- base, capslock: '\u0644'
+ base: '\u0644'
shift: '\ufef7'
}
key H {
label: 'H'
- base, capslock: '\u0627'
+ base: '\u0627'
shift: '\u0623'
}
key J {
label: 'J'
- base, capslock: '\u062a'
+ base: '\u062a'
shift: '\u0640'
}
key K {
label: 'K'
- base, capslock: '\u0646'
+ base: '\u0646'
shift: '\u060c'
}
key L {
label: 'L'
- base, capslock: '\u0645'
+ base: '\u0645'
shift: '/'
}
key SEMICOLON {
label: ';'
- base, capslock: '\u0643'
+ base: '\u0643'
shift: ':'
}
key APOSTROPHE {
label: '\''
- base, capslock: '\u0637'
+ base: '\u0637'
shift: '"'
}
@@ -260,60 +260,60 @@
key Z {
label: 'Z'
- base, capslock: '\u0626'
+ base: '\u0626'
shift: '~'
}
key X {
label: 'X'
- base, capslock: '\u0621'
+ base: '\u0621'
shift: '\u0652'
}
key C {
label: 'C'
- base, capslock: '\u0624'
+ base: '\u0624'
shift: '}'
}
key V {
label: 'V'
- base, capslock: '\u0631'
+ base: '\u0631'
shift: '{'
}
key B {
label: 'B'
- base, capslock: '\ufefb'
+ base: '\ufefb'
shift: '\ufef5'
}
key N {
label: 'N'
- base, capslock: '\u0649'
+ base: '\u0649'
shift: '\u0622'
}
key M {
label: 'M'
- base, capslock: '\u0629'
+ base: '\u0629'
shift: '\u2019'
}
key COMMA {
label: ','
- base, capslock: '\u0648'
+ base: '\u0648'
shift: ','
}
key PERIOD {
label: '.'
- base, capslock: '\u0632'
+ base: '\u0632'
shift: '.'
}
key SLASH {
label: '/'
- base, capslock: '\u0638'
+ base: '\u0638'
shift: '\u061f'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm b/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm
index 69490cc..3f5e894 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm
@@ -107,72 +107,84 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: '\u00dc'
base: '\u00fc'
shift, capslock: '\u00dc'
+ shift+capslock: '\u00fc'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: '\u0130'
base: 'i'
shift, capslock: '\u0130'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: '\u00d6'
base: '\u00f6'
shift: '\u00d6'
+ shift+capslock: '\u00f6'
}
key RIGHT_BRACKET {
label: '\u011e'
base: '\u011f'
shift: '\u011e'
+ shift+capslock: '\u011f'
}
key BACKSLASH {
@@ -187,66 +199,77 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: 'I'
base: '\u0131'
shift: 'I'
+ shift+capslock: '\u0131'
}
key APOSTROPHE {
label: '\u018f'
base: '\u0259'
shift: '\u018f'
+ shift+capslock: '\u0259'
}
### ROW 4
@@ -255,54 +278,63 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
label: '\u00c7'
base: '\u00e7'
shift: '\u00c7'
+ shift+capslock: '\u00e7'
}
key PERIOD {
label: '\u015e'
base: '\u015f'
shift: '\u015e'
+ shift+capslock: '\u015f'
}
key SLASH {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_belarusian.kcm b/packages/InputDevices/res/raw/keyboard_layout_belarusian.kcm
index 3deb9dd..6751e1d 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_belarusian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_belarusian.kcm
@@ -24,6 +24,7 @@
label: '\u0401'
base: '\u0451'
shift, capslock: '\u0401'
+ shift+capslock: '\u0451'
ralt: '`'
ralt+shift: '~'
}
@@ -106,163 +107,203 @@
label: '\u0419'
base: '\u0439'
shift, capslock: '\u0419'
+ shift+capslock: '\u0439'
ralt: 'q'
- ralt+shift, ralt+capslock: 'Q'
+ shift+ralt, capslock+ralt: 'Q'
+ shift+capslock+ralt: 'q'
}
key W {
label: '\u0426'
base: '\u0446'
shift, capslock: '\u0426'
+ shift+capslock: '\u0446'
ralt: 'w'
- ralt+shift, ralt+capslock: 'W'
+ shift+ralt, capslock+ralt: 'W'
+ shift+capslock+ralt: 'w'
}
key E {
label: '\u0423'
base: '\u0443'
shift, capslock: '\u0423'
+ shift+capslock: '\u0443'
ralt: 'e'
- ralt+shift, ralt+capslock: 'E'
+ shift+ralt, capslock+ralt: 'E'
+ shift+capslock+ralt: 'e'
}
key R {
label: '\u041a'
base: '\u043a'
shift, capslock: '\u041a'
+ shift+capslock: '\u043a'
ralt: 'r'
- ralt+shift, ralt+capslock: 'R'
+ shift+ralt, capslock+ralt: 'R'
+ shift+capslock+ralt: 'r'
}
key T {
label: '\u0415'
base: '\u0435'
shift, capslock: '\u0415'
+ shift+capslock: '\u0435'
ralt: 't'
- ralt+shift, ralt+capslock: 'T'
+ shift+ralt, capslock+ralt: 'T'
+ shift+capslock+ralt: 't'
}
key Y {
label: '\u041d'
base: '\u043d'
shift, capslock: '\u041d'
+ shift+capslock: '\u043d'
ralt: 'y'
- ralt+shift, ralt+capslock: 'Y'
+ shift+ralt, capslock+ralt: 'Y'
+ shift+capslock+ralt: 'y'
}
key U {
label: '\u0413'
base: '\u0433'
shift, capslock: '\u0413'
+ shift+capslock: '\u0433'
ralt: 'u'
- ralt+shift, ralt+capslock: 'U'
+ shift+ralt, capslock+ralt: 'U'
+ shift+capslock+ralt: 'u'
}
key I {
label: '\u0428'
base: '\u0448'
shift, capslock: '\u0428'
+ shift+capslock: '\u0448'
ralt: 'i'
- ralt+shift, ralt+capslock: 'I'
+ shift+ralt, capslock+ralt: 'I'
+ shift+capslock+ralt: 'i'
}
key O {
label: '\u040E'
base: '\u045E'
shift, capslock: '\u040E'
+ shift+capslock: '\u045E'
ralt: 'o'
- ralt+shift, ralt+capslock: 'O'
+ shift+ralt, capslock+ralt: 'O'
+ shift+capslock+ralt: 'o'
}
key P {
label: '\u0417'
base: '\u0437'
shift, capslock: '\u0417'
+ shift+capslock: '\u0437'
ralt: 'p'
- ralt+shift, ralt+capslock: 'P'
+ shift+ralt, capslock+ralt: 'P'
+ shift+capslock+ralt: 'p'
}
key LEFT_BRACKET {
label: '\u0425'
base: '\u0445'
shift, capslock: '\u0425'
+ shift+capslock: '\u0445'
ralt: '['
- ralt+shift: '{'
+ shift+ralt: '{'
}
key RIGHT_BRACKET {
label: '\u0027'
base: '\u0027'
- shift, capslock: '\u0027'
ralt: ']'
- ralt+shift: '}'
+ shift+ralt: '}'
}
### ROW 3
key A {
label: '\u0424'
base: '\u0444'
shift, capslock: '\u0424'
+ shift+capslock: '\u0444'
ralt: 'a'
- ralt+shift, ralt+capslock: 'A'
+ shift+ralt, capslock+ralt: 'A'
+ shift+capslock+ralt: 'a'
}
key S {
label: '\u042b'
base: '\u044b'
shift, capslock: '\u042b'
+ shift+capslock: '\u044b'
ralt: 's'
- ralt+shift, ralt+capslock: 'S'
+ shift+ralt, capslock+ralt: 'S'
+ shift+capslock+ralt: 's'
}
key D {
label: '\u0412'
base: '\u0432'
shift, capslock: '\u0412'
+ shift+capslock: '\u0432'
ralt: 'd'
- ralt+shift, ralt+capslock: 'D'
+ shift+ralt, capslock+ralt: 'D'
+ shift+capslock+ralt: 'd'
}
key F {
label: '\u0410'
base: '\u0430'
shift, capslock: '\u0410'
+ shift+capslock: '\u0430'
ralt: 'f'
- ralt+shift, ralt+capslock: 'F'
+ shift+ralt, capslock+ralt: 'F'
+ shift+capslock+ralt: 'f'
}
key G {
label: '\u041f'
base: '\u043f'
shift, capslock: '\u041f'
+ shift+capslock: '\u043f'
ralt: 'g'
- ralt+shift, ralt+capslock: 'G'
+ shift+ralt, capslock+ralt: 'G'
+ shift+capslock+ralt: 'g'
}
key H {
label: '\u0420'
base: '\u0440'
shift, capslock: '\u0420'
+ shift+capslock: '\u0440'
ralt: 'h'
- ralt+shift, ralt+capslock: 'H'
+ shift+ralt, capslock+ralt: 'H'
+ shift+capslock+ralt: 'h'
}
key J {
label: '\u041e'
base: '\u043e'
shift, capslock: '\u041e'
+ shift+capslock: '\u043e'
ralt: 'j'
- ralt+shift, ralt+capslock: 'J'
+ shift+ralt, capslock+ralt: 'J'
+ shift+capslock+ralt: 'j'
}
key K {
label: '\u041b'
base: '\u043b'
shift, capslock: '\u041b'
+ shift+capslock: '\u043b'
ralt: 'k'
- ralt+shift, ralt+capslock: 'K'
+ shift+ralt, capslock+ralt: 'K'
+ shift+capslock+ralt: 'k'
}
key L {
label: '\u0414'
base: '\u0434'
shift, capslock: '\u0414'
+ shift+capslock: '\u0434'
ralt: 'l'
- ralt+shift, ralt+capslock: 'L'
+ shift+ralt, capslock+ralt: 'L'
+ shift+capslock+ralt: 'l'
}
key SEMICOLON {
label: '\u0416'
base: '\u0436'
shift, capslock: '\u0416'
+ shift+capslock: '\u0436'
ralt: ';'
- ralt+shift: ':'
+ shift+ralt: ':'
}
key APOSTROPHE {
label: '\u042d'
base: '\u044d'
shift, capslock: '\u042d'
+ shift+capslock: '\u044d'
ralt: '\''
- ralt+shift: '"'
+ shift+ralt: '"'
}
key BACKSLASH {
label: '\\'
@@ -275,69 +316,85 @@
label: '\u042f'
base: '\u044f'
shift, capslock: '\u042f'
+ shift+capslock: '\u044f'
ralt: 'z'
- ralt+shift, ralt+capslock: 'Z'
+ shift+ralt, capslock+ralt: 'Z'
+ shift+capslock+ralt: 'z'
}
key X {
label: '\u0427'
base: '\u0447'
shift, capslock: '\u0427'
+ shift+capslock: '\u0447'
ralt: 'x'
- ralt+shift, ralt+capslock: 'X'
+ shift+ralt, capslock+ralt: 'X'
+ shift+capslock+ralt: 'x'
}
key C {
label: '\u0421'
base: '\u0441'
shift, capslock: '\u0421'
+ shift+capslock: '\u0441'
ralt: 'c'
- ralt+shift, ralt+capslock: 'C'
+ shift+ralt, capslock+ralt: 'C'
+ shift+capslock+ralt: 'c'
}
key V {
label: '\u041c'
base: '\u043c'
shift, capslock: '\u041c'
+ shift+capslock: '\u043c'
ralt: 'v'
- ralt+shift, ralt+capslock: 'V'
+ shift+ralt, capslock+ralt: 'V'
+ shift+capslock+ralt: 'v'
}
key B {
label: '\u0406'
base: '\u0456'
shift, capslock: '\u0406'
+ shift+capslock: '\u0456'
ralt: 'b'
- ralt+shift, ralt+capslock: 'B'
+ shift+ralt, capslock+ralt: 'B'
+ shift+capslock+ralt: 'b'
}
key N {
label: '\u0422'
base: '\u0442'
shift, capslock: '\u0422'
+ shift+capslock: '\u0442'
ralt: 'n'
- ralt+shift, ralt+capslock: 'N'
+ shift+ralt, capslock+ralt: 'N'
+ shift+capslock+ralt: 'n'
}
key M {
label: '\u042c'
base: '\u044c'
shift, capslock: '\u042c'
+ shift+capslock: '\u044c'
ralt: 'm'
- ralt+shift, ralt+capslock: 'M'
+ shift+ralt, capslock+ralt: 'M'
+ shift+capslock+ralt: 'm'
}
key COMMA {
label: '\u0411'
base: '\u0431'
shift, capslock: '\u0411'
+ shift+capslock: '\u0431'
ralt: ','
- ralt+shift: '<'
+ shift+ralt: '<'
}
key PERIOD {
label: '\u042e'
base: '\u044e'
shift, capslock: '\u042e'
+ shift+capslock: '\u044e'
ralt: '.'
- ralt+shift: '>'
+ shift+ralt: '>'
}
key SLASH {
label: '.'
base: '.'
shift: ','
ralt: '/'
- ralt+shift: '?'
+ shift+ralt: '?'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_belgian.kcm b/packages/InputDevices/res/raw/keyboard_layout_belgian.kcm
index f2c39ce..d529311 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_belgian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_belgian.kcm
@@ -122,18 +122,21 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key Z {
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -141,42 +144,49 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -199,60 +209,70 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key APOSTROPHE {
@@ -282,36 +302,42 @@
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_brazilian.kcm b/packages/InputDevices/res/raw/keyboard_layout_brazilian.kcm
index 140c7ac..ad3199f 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_brazilian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_brazilian.kcm
@@ -115,6 +115,7 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '/'
}
@@ -122,6 +123,7 @@
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
ralt: '?'
}
@@ -129,6 +131,7 @@
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -136,42 +139,49 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -193,60 +203,70 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: '\u00c7'
base: '\u00e7'
shift, capslock: '\u00c7'
+ shift+capslock: '\u00e7'
}
key APOSTROPHE {
@@ -258,7 +278,7 @@
key BACKSLASH {
label: ']'
base: ']'
- shift, capslock: '}'
+ shift: '}'
ralt: '\u00ba'
}
@@ -274,18 +294,21 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
ralt: '\u20a2'
}
@@ -293,24 +316,28 @@
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'n'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_bulgarian.kcm b/packages/InputDevices/res/raw/keyboard_layout_bulgarian.kcm
index c56367e..94ffbd0 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_bulgarian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_bulgarian.kcm
@@ -27,7 +27,7 @@
key GRAVE {
label: '`'
base: '`'
- shift, capslock: '~'
+ shift: '~'
ralt: '`'
ralt+shift: '~'
}
@@ -123,89 +123,109 @@
label: ','
base: ','
shift: '\u044b'
- capslock: '\u042b'
+ shift+capslock: '\u042b'
ralt: 'q'
- ralt+shift, ralt+capslock: 'Q'
+ shift+ralt, capslock+ralt: 'Q'
+ shift+capslock+ralt: 'q'
}
key W {
label: '\u0423'
base: '\u0443'
shift, capslock: '\u0423'
+ shift+capslock: '\u0443'
ralt: 'w'
- ralt+shift, ralt+capslock: 'W'
+ shift+ralt, capslock+ralt: 'W'
+ shift+capslock+ralt: 'w'
}
key E {
label: '\u0415'
base: '\u0435'
shift, capslock: '\u0415'
+ shift+capslock: '\u0435'
ralt: 'e'
- ralt+shift, ralt+capslock: 'E'
+ shift+ralt, capslock+ralt: 'E'
+ shift+capslock+ralt: 'e'
}
key R {
label: '\u0418'
base: '\u0438'
shift, capslock: '\u0418'
+ shift+capslock: '\u0438'
ralt: 'r'
- ralt+shift, ralt+capslock: 'R'
+ shift+ralt, capslock+ralt: 'R'
+ shift+capslock+ralt: 'r'
}
key T {
label: '\u0428'
base: '\u0448'
shift, capslock: '\u0428'
+ shift+capslock: '\u0448'
ralt: 't'
- ralt+shift, ralt+capslock: 'T'
+ shift+ralt, capslock+ralt: 'T'
+ shift+capslock+ralt: 't'
}
key Y {
label: '\u0429'
base: '\u0449'
shift, capslock: '\u0429'
+ shift+capslock: '\u0449'
ralt: 'y'
- ralt+shift, ralt+capslock: 'Y'
+ shift+ralt, capslock+ralt: 'Y'
+ shift+capslock+ralt: 'y'
}
key U {
label: '\u041a'
base: '\u043a'
shift, capslock: '\u041a'
+ shift+capslock: '\u043a'
ralt: 'u'
- ralt+shift, ralt+capslock: 'U'
+ shift+ralt, capslock+ralt: 'U'
+ shift+capslock+ralt: 'u'
}
key I {
label: '\u0421'
base: '\u0441'
shift, capslock: '\u0421'
+ shift+capslock: '\u0441'
ralt: 'i'
- ralt+shift, ralt+capslock: 'I'
+ shift+ralt, capslock+ralt: 'I'
+ shift+capslock+ralt: 'i'
}
key O {
label: '\u0414'
base: '\u0434'
shift, capslock: '\u0414'
+ shift+capslock: '\u0434'
ralt: 'o'
- ralt+shift, ralt+capslock: 'O'
+ shift+ralt, capslock+ralt: 'O'
+ shift+capslock+ralt: 'o'
}
key P {
label: '\u0417'
base: '\u0437'
shift, capslock: '\u0417'
+ shift+capslock: '\u0437'
ralt: 'p'
- ralt+shift, ralt+capslock: 'P'
+ shift+ralt, capslock+ralt: 'P'
+ shift+capslock+ralt: 'p'
}
key LEFT_BRACKET {
label: '\u0426'
base: '\u0446'
shift, capslock: '\u0426'
+ shift+capslock: '\u0446'
ralt: '['
- ralt+shift: '{'
+ shift+ralt: '{'
}
key RIGHT_BRACKET {
@@ -213,7 +233,7 @@
base: ';'
shift: '\u00a7'
ralt: ']'
- ralt+shift: '}'
+ shift+ralt: '}'
}
### ROW 3
@@ -222,78 +242,97 @@
label: '\u042c'
base: '\u044c'
shift, capslock: '\u042c'
+ shift+capslock: '\u044c'
ralt: 'a'
- ralt+shift, ralt+capslock: 'A'
+ shift+ralt, capslock+ralt: 'A'
+ shift+capslock+ralt: 'a'
}
key S {
label: '\u042f'
base: '\u044f'
shift, capslock: '\u042f'
+ shift+capslock: '\u044f'
ralt: 's'
- ralt+shift, ralt+capslock: 'S'
+ shift+ralt, capslock+ralt: 'S'
+ shift+capslock+ralt: 's'
}
key D {
label: '\u0410'
base: '\u0430'
shift, capslock: '\u0410'
+ shift+capslock: '\u0430'
ralt: 'd'
- ralt+shift, ralt+capslock: 'D'
+ shift+ralt, capslock+ralt: 'D'
+ shift+capslock+ralt: 'd'
}
key F {
label: '\u041e'
base: '\u043e'
shift, capslock: '\u041e'
+ shift+capslock: '\u043e'
ralt: 'f'
- ralt+shift, ralt+capslock: 'F'
+ shift+ralt, capslock+ralt: 'F'
+ shift+capslock+ralt: 'f'
}
key G {
label: '\u0416'
base: '\u0436'
shift, capslock: '\u0416'
+ shift+capslock: '\u0436'
ralt: 'g'
- ralt+shift, ralt+capslock: 'G'
+ shift+ralt, capslock+ralt: 'G'
+ shift+capslock+ralt: 'g'
}
key H {
label: '\u0413'
base: '\u0433'
shift, capslock: '\u0413'
+ shift+capslock: '\u0433'
ralt: 'h'
- ralt+shift, ralt+capslock: 'H'
+ shift+ralt, capslock+ralt: 'H'
+ shift+capslock+ralt: 'h'
}
key J {
label: '\u0422'
base: '\u0442'
shift, capslock: '\u0422'
+ shift+capslock: '\u0442'
ralt: 'j'
- ralt+shift, ralt+capslock: 'J'
+ shift+ralt, capslock+ralt: 'J'
+ shift+capslock+ralt: 'j'
}
key K {
label: '\u041d'
base: '\u043d'
shift, capslock: '\u041d'
+ shift+capslock: '\u043d'
ralt: 'k'
- ralt+shift, ralt+capslock: 'K'
+ shift+ralt, capslock+ralt: 'K'
+ shift+capslock+ralt: 'k'
}
key L {
label: '\u0412'
base: '\u0432'
shift, capslock: '\u0412'
+ shift+capslock: '\u0432'
ralt: 'l'
- ralt+shift, ralt+capslock: 'L'
+ shift+ralt, capslock+ralt: 'L'
+ shift+capslock+ralt: 'l'
}
key SEMICOLON {
label: '\u041c'
base: '\u043c'
shift, capslock: '\u041c'
+ shift+capslock: '\u043c'
ralt: ';'
ralt+shift: ':'
}
@@ -302,6 +341,7 @@
label: '\u0427'
base: '\u0447'
shift, capslock: '\u0427'
+ shift+capslock: '\u0447'
ralt: '\''
ralt+shift: '"'
}
@@ -328,62 +368,77 @@
label: '\u042e'
base: '\u044e'
shift, capslock: '\u042e'
+ shift+capslock: '\u044e'
ralt: 'z'
- ralt+shift, ralt+capslock: 'Z'
+ shift+ralt, capslock+ralt: 'Z'
+ shift+capslock+ralt: 'z'
}
key X {
label: '\u0419'
base: '\u0439'
shift, capslock: '\u0419'
+ shift+capslock: '\u0439'
ralt: 'x'
- ralt+shift, ralt+capslock: 'X'
+ shift+ralt, capslock+ralt: 'X'
+ shift+capslock+ralt: 'x'
}
key C {
label: '\u042a'
base: '\u044a'
shift, capslock: '\u042a'
+ shift+capslock: '\u044a'
ralt: 'c'
- ralt+shift, ralt+capslock: 'C'
+ shift+ralt, capslock+ralt: 'C'
+ shift+capslock+ralt: 'c'
}
key V {
label: '\u042d'
base: '\u044d'
shift, capslock: '\u042d'
+ shift+capslock: '\u044d'
ralt: 'v'
- ralt+shift, ralt+capslock: 'V'
+ shift+ralt, capslock+ralt: 'V'
+ shift+capslock+ralt: 'v'
}
key B {
label: '\u0424'
base: '\u0444'
shift, capslock: '\u0424'
+ shift+capslock: '\u0444'
ralt: 'b'
- ralt+shift, ralt+capslock: 'B'
+ shift+ralt, capslock+ralt: 'B'
+ shift+capslock+ralt: 'b'
}
key N {
label: '\u0425'
base: '\u0445'
shift, capslock: '\u0425'
+ shift+capslock: '\u0445'
ralt: 'n'
- ralt+shift, ralt+capslock: 'N'
+ shift+ralt, capslock+ralt: 'N'
+ shift+capslock+ralt: 'n'
}
key M {
label: '\u041f'
base: '\u043f'
shift, capslock: '\u041f'
+ shift+capslock: '\u043f'
ralt: 'm'
- ralt+shift, ralt+capslock: 'M'
+ shift+ralt, capslock+ralt: 'M'
+ shift+capslock+ralt: 'm'
}
key COMMA {
label: '\u0420'
base: '\u0440'
shift, capslock: '\u0420'
+ shift+capslock: '\u0440'
ralt: ','
ralt+shift: '<'
}
@@ -392,6 +447,7 @@
label: '\u041b'
base: '\u043b'
shift, capslock: '\u041b'
+ shift+capslock: '\u043b'
ralt: '.'
ralt+shift: '>'
}
@@ -400,6 +456,7 @@
label: '\u0411'
base: '\u0431'
shift, capslock: '\u0411'
+ shift+capslock: '\u0431'
ralt: '/'
ralt+shift: '?'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_bulgarian_phonetic.kcm b/packages/InputDevices/res/raw/keyboard_layout_bulgarian_phonetic.kcm
index 8878807..6314158 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_bulgarian_phonetic.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_bulgarian_phonetic.kcm
@@ -28,6 +28,7 @@
label: '`'
base: '\u044e'
shift, capslock: '\u042e'
+ shift+capslock: '\u044e'
ralt: '`'
ralt+shift: '~'
}
@@ -122,88 +123,108 @@
key Q {
label: '\u0447'
base: '\u0447'
- shift: '\u0427'
- capslock: '\u0427'
+ shift, capslock: '\u0427'
+ shift+capslock: '\u0447'
ralt: 'q'
- ralt+shift, ralt+capslock: 'Q'
+ shift+ralt, capslock+ralt: 'Q'
+ shift+capslock+ralt: 'q'
}
key W {
label: '\u0448'
base: '\u0448'
shift, capslock: '\u0428'
+ shift+capslock: '\u0448'
ralt: 'w'
- ralt+shift, ralt+capslock: 'W'
+ shift+ralt, capslock+ralt: 'W'
+ shift+capslock+ralt: 'w'
}
key E {
label: '\u0435'
base: '\u0435'
shift, capslock: '\u0415'
+ shift+capslock: '\u0435'
ralt: 'e'
- ralt+shift, ralt+capslock: 'E'
+ shift+ralt, capslock+ralt: 'E'
+ shift+capslock+ralt: 'e'
}
key R {
label: '\u0440'
base: '\u0440'
shift, capslock: '\u0420'
+ shift+capslock: '\u0440'
ralt: 'r'
- ralt+shift, ralt+capslock: 'R'
+ shift+ralt, capslock+ralt: 'R'
+ shift+capslock+ralt: 'r'
}
key T {
label: '\u0442'
base: '\u0442'
shift, capslock: '\u0422'
+ shift+capslock: '\u0442'
ralt: 't'
- ralt+shift, ralt+capslock: 'T'
+ shift+ralt, capslock+ralt: 'T'
+ shift+capslock+ralt: 't'
}
key Y {
label: '\u044a'
base: '\u044a'
shift, capslock: '\u042a'
+ shift+capslock: '\u044a'
ralt: 'y'
- ralt+shift, ralt+capslock: 'Y'
+ shift+ralt, capslock+ralt: 'Y'
+ shift+capslock+ralt: 'y'
}
key U {
label: '\u0443'
base: '\u0443'
shift, capslock: '\u0423'
+ shift+capslock: '\u0443'
ralt: 'u'
- ralt+shift, ralt+capslock: 'U'
+ shift+ralt, capslock+ralt: 'U'
+ shift+capslock+ralt: 'u'
}
key I {
label: '\u0438'
base: '\u0438'
shift, capslock: '\u0418'
+ shift+capslock: '\u0438'
ralt: 'i'
- ralt+shift, ralt+capslock: 'I'
+ shift+ralt, capslock+ralt: 'I'
+ shift+capslock+ralt: 'i'
}
key O {
label: '\u043e'
base: '\u043e'
shift, capslock: '\u041e'
+ shift+capslock: '\u043e'
ralt: 'o'
- ralt+shift, ralt+capslock: 'O'
+ shift+ralt, capslock+ralt: 'O'
+ shift+capslock+ralt: 'o'
}
key P {
label: '\u043f'
base: '\u043f'
shift, capslock: '\u041f'
+ shift+capslock: '\u043f'
ralt: 'p'
- ralt+shift, ralt+capslock: 'P'
+ shift+ralt, capslock+ralt: 'P'
+ shift+capslock+ralt: 'p'
}
key LEFT_BRACKET {
label: '\u044f'
base: '\u044f'
shift, capslock: '\u042f'
+ shift+capslock: '\u044f'
ralt: '['
ralt+shift: '{'
}
@@ -211,7 +232,8 @@
key RIGHT_BRACKET {
label: '\u0449'
base: '\u0449'
- shift: '\u0429'
+ shift, capslock: '\u0429'
+ shift+capslock: '\u0449'
ralt: ']'
ralt+shift: '}'
}
@@ -219,9 +241,8 @@
key BACKSLASH {
label: '\u044c'
base: '\u044c'
- shift: '\u042c'
- capslock: '\u042c'
- shift+capslock: '\u040d'
+ shift, capslock: '\u042c'
+ shift+capslock: '\u044c'
ralt: '\\'
ralt+shift: '|'
}
@@ -232,78 +253,96 @@
label: '\u0430'
base: '\u0430'
shift, capslock: '\u0410'
+ shift+capslock: '\u0430'
ralt: 'a'
- ralt+shift, ralt+capslock: 'A'
+ shift+ralt, capslock+ralt: 'A'
+ shift+capslock+ralt: 'a'
}
key S {
label: '\u0441'
base: '\u0441'
shift, capslock: '\u0421'
+ shift+capslock: '\u0441'
ralt: 's'
- ralt+shift, ralt+capslock: 'S'
+ shift+ralt, capslock+ralt: 'S'
+ shift+capslock+ralt: 's'
}
key D {
label: '\u0434'
base: '\u0434'
shift, capslock: '\u0414'
+ shift+capslock: '\u0434'
ralt: 'd'
- ralt+shift, ralt+capslock: 'D'
+ shift+ralt, capslock+ralt: 'D'
+ shift+capslock+ralt: 'd'
}
key F {
label: '\u0444'
base: '\u0444'
shift, capslock: '\u0424'
+ shift+capslock: '\u0444'
ralt: 'f'
- ralt+shift, ralt+capslock: 'F'
+ shift+ralt, capslock+ralt: 'F'
+ shift+capslock+ralt: 'f'
}
key G {
label: '\u0433'
base: '\u0433'
shift, capslock: '\u0413'
+ shift+capslock: '\u0433'
ralt: 'g'
- ralt+shift, ralt+capslock: 'G'
+ shift+ralt, capslock+ralt: 'G'
+ shift+capslock+ralt: 'g'
}
key H {
label: '\u0445'
base: '\u0445'
shift, capslock: '\u0425'
+ shift+capslock: '\u0445'
ralt: 'h'
- ralt+shift, ralt+capslock: 'H'
+ shift+ralt, capslock+ralt: 'H'
+ shift+capslock+ralt: 'h'
}
key J {
label: '\u0439'
base: '\u0439'
shift, capslock: '\u0419'
+ shift+capslock: '\u0439'
ralt: 'j'
- ralt+shift, ralt+capslock: 'J'
+ shift+ralt, capslock+ralt: 'J'
+ shift+capslock+ralt: 'j'
}
key K {
label: '\u043a'
base: '\u043a'
shift, capslock: '\u041a'
+ shift+capslock: '\u043a'
ralt: 'k'
- ralt+shift, ralt+capslock: 'K'
+ shift+ralt, capslock+ralt: 'K'
+ shift+capslock+ralt: 'k'
}
key L {
label: '\u043b'
base: '\u043b'
shift, capslock: '\u041b'
+ shift+capslock: '\u043b'
ralt: 'l'
- ralt+shift, ralt+capslock: 'L'
+ shift+ralt, capslock+ralt: 'L'
+ shift+capslock+ralt: 'l'
}
key SEMICOLON {
label: ';'
base: ';'
- shift, capslock: ':'
+ shift: ':'
ralt: ';'
ralt+shift: ':'
}
@@ -311,7 +350,7 @@
key APOSTROPHE {
label: '\''
base: '\''
- shift, capslock: '"'
+ shift: '"'
ralt: '\''
ralt+shift: '"'
}
@@ -322,6 +361,7 @@
label: '\u045d'
base: '\u045d'
shift, capslock: '\u040d'
+ shift+capslock: '\u045d'
ralt: '\\'
ralt+shift: '|'
}
@@ -330,62 +370,76 @@
label: '\u0437'
base: '\u0437'
shift, capslock: '\u0417'
+ shift+capslock: '\u0437'
ralt: 'z'
- ralt+shift, ralt+capslock: 'Z'
+ shift+ralt, capslock+ralt: 'Z'
+ shift+capslock+ralt: 'z'
}
key X {
label: '\u0436'
base: '\u0436'
shift, capslock: '\u0416'
+ shift+capslock: '\u0436'
ralt: 'x'
- ralt+shift, ralt+capslock: 'X'
+ shift+ralt, capslock+ralt: 'X'
+ shift+capslock+ralt: 'x'
}
key C {
label: '\u0446'
base: '\u0446'
shift, capslock: '\u0426'
+ shift+capslock: '\u0446'
ralt: 'c'
- ralt+shift, ralt+capslock: 'C'
+ shift+ralt, capslock+ralt: 'C'
+ shift+capslock+ralt: 'c'
}
key V {
label: '\u0432'
base: '\u0432'
shift, capslock: '\u0412'
+ shift+capslock: '\u0432'
ralt: 'v'
- ralt+shift, ralt+capslock: 'V'
+ shift+ralt, capslock+ralt: 'V'
+ shift+capslock+ralt: 'v'
}
key B {
label: '\u0431'
base: '\u0431'
shift, capslock: '\u0411'
+ shift+capslock: '\u0431'
ralt: 'b'
- ralt+shift, ralt+capslock: 'B'
+ shift+ralt, capslock+ralt: 'B'
+ shift+capslock+ralt: 'b'
}
key N {
label: '\u043d'
base: '\u043d'
shift, capslock: '\u041d'
+ shift+capslock: '\u043d'
ralt: 'n'
- ralt+shift, ralt+capslock: 'N'
+ shift+ralt, capslock+ralt: 'N'
+ shift+capslock+ralt: 'n'
}
key M {
label: '\u043c'
base: '\u043c'
shift, capslock: '\u041c'
+ shift+capslock: '\u043c'
ralt: 'm'
- ralt+shift, ralt+capslock: 'M'
+ shift+ralt, capslock+ralt: 'M'
+ shift+capslock+ralt: 'm'
}
key COMMA {
label: ','
base: ','
- shift, capslock: '\u201e'
+ shift: '\u201e'
ralt: ','
ralt+shift: '<'
}
@@ -393,7 +447,7 @@
key PERIOD {
label: '.'
base: '.'
- shift, capslock: '\u201c'
+ shift: '\u201c'
ralt: '.'
ralt+shift: '>'
}
@@ -401,7 +455,7 @@
key SLASH {
label: '/'
base: '/'
- shift, capslock: '?'
+ shift: '?'
ralt: '/'
ralt+shift: '?'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_croatian_and_slovenian.kcm b/packages/InputDevices/res/raw/keyboard_layout_croatian_and_slovenian.kcm
index 96445a4..1c774cc 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_croatian_and_slovenian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_croatian_and_slovenian.kcm
@@ -122,6 +122,7 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '\\'
}
@@ -129,6 +130,7 @@
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
ralt: '|'
}
@@ -136,6 +138,7 @@
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -143,48 +146,56 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Z {
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: '\u0160'
base: '\u0161'
shift, capslock: '\u0160'
+ shift+capslock: '\u0161'
ralt: '\u00f7'
}
@@ -192,6 +203,7 @@
label: '\u0110'
base: '\u0111'
shift, capslock: '\u0110'
+ shift+capslock: '\u0111'
ralt: '\u00d7'
}
@@ -201,24 +213,28 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
ralt: '['
}
@@ -226,6 +242,7 @@
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
ralt: ']'
}
@@ -233,40 +250,48 @@
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
ralt: '\u0268'
- ralt+shift, ralt+capslock: '\u0197'
+ shift+ralt, capslock+ralt: '\u0197'
+ shift+capslock+ralt: '\u0268'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
ralt: '\u0142'
- ralt+shift, ralt+capslock: '\u0141'
+ shift+ralt, capslock+ralt: '\u0141'
+ shift+capslock+ralt: '\u0142'
}
key SEMICOLON {
label: '\u010c'
base: '\u010d'
shift, capslock: '\u010c'
+ shift+capslock: '\u010d'
}
key APOSTROPHE {
label: '\u0106'
base: '\u0107'
shift, capslock: '\u0106'
+ shift+capslock: '\u0107'
ralt: '\u00df'
}
@@ -274,6 +299,7 @@
label: '\u017d'
base: '\u017e'
shift, capslock: '\u017d'
+ shift+capslock: '\u017e'
ralt: '\u00a4'
}
@@ -289,24 +315,28 @@
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
ralt: '@'
}
@@ -314,6 +344,7 @@
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
ralt: '{'
}
@@ -321,6 +352,7 @@
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
ralt: '}'
}
@@ -328,6 +360,7 @@
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
ralt: '\u00a7'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_czech.kcm b/packages/InputDevices/res/raw/keyboard_layout_czech.kcm
index 32750e0..08b012e 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_czech.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_czech.kcm
@@ -131,18 +131,21 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -150,42 +153,49 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -211,54 +221,63 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
@@ -300,24 +319,28 @@
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
ralt: '@'
}
@@ -325,18 +348,21 @@
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
ralt: '\u00b5'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_czech_qwerty.kcm b/packages/InputDevices/res/raw/keyboard_layout_czech_qwerty.kcm
index 457d4da..cad262b 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_czech_qwerty.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_czech_qwerty.kcm
@@ -131,18 +131,21 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -150,42 +153,49 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -211,54 +221,63 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
@@ -300,24 +319,28 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
ralt: '@'
}
@@ -325,18 +348,21 @@
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
ralt: '\u00b5'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_danish.kcm b/packages/InputDevices/res/raw/keyboard_layout_danish.kcm
index 9168d12..83ee8c3 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_danish.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_danish.kcm
@@ -115,76 +115,90 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '\u00e2'
- ralt+capslock, shift+ralt: '\u00c2'
+ shift+ralt, capslock+ralt: '\u00c2'
+ shift+capslock+ralt: '\u00e2'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
- ralt+capslock: '\u20ac'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
ralt: '\u0167'
- ralt+capslock, shift+ralt: '\u0166'
+ shift+ralt, capslock+ralt: '\u0166'
+ shift+capslock+ralt: '\u0167'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
ralt: '\u00ef'
- ralt+capslock, shift+ralt: '\u00cf'
+ shift+ralt, capslock+ralt: '\u00cf'
+ shift+capslock+ralt: '\u00ef'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
ralt: '\u00f5'
- ralt+capslock, shift+ralt: '\u00d5'
+ shift+ralt, capslock+ralt: '\u00d5'
+ shift+capslock+ralt: '\u00f5'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: '\u00c5'
base: '\u00e5'
shift, capslock: '\u00c5'
+ shift+capslock: '\u00e5'
}
key RIGHT_BRACKET {
@@ -200,84 +214,104 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
ralt: '\u00e1'
- ralt+capslock, shift+ralt: '\u00c1'
+ shift+ralt, capslock+ralt: '\u00c1'
+ shift+capslock+ralt: '\u00e1'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u0161'
- ralt+capslock, shift+ralt: '\u0160'
+ shift+ralt, capslock+ralt: '\u0160'
+ shift+capslock+ralt: '\u0161'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
ralt: '\u0111'
- ralt+capslock, shift+ralt: '\u0110'
+ shift+ralt, capslock+ralt: '\u0110'
+ shift+capslock+ralt: '\u0111'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
ralt: '\u01e5'
- ralt+capslock, shift+ralt: '\u01e4'
+ shift+ralt, capslock+ralt: '\u01e4'
+ shift+capslock+ralt: '\u01e5'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
ralt: '\u01e7'
- ralt+capslock, shift+ralt: '\u01e6'
+ shift+ralt, capslock+ralt: '\u01e6'
+ shift+capslock+ralt: '\u01e7'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
ralt: '\u021f'
- ralt+capslock, shift+ralt: '\u021e'
+ shift+ralt, capslock+ralt: '\u021e'
+ shift+capslock+ralt: '\u021f'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
ralt: '\u01e9'
- ralt+capslock, shift+ralt: '\u01e8'
+ shift+ralt, capslock+ralt: '\u01e8'
+ shift+capslock+ralt: '\u01e9'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: '\u00c6'
base: '\u00e6'
shift, capslock: '\u00c6'
+ shift+capslock: '\u00e6'
ralt: '\u00e4'
- ralt+capslock, shift+ralt: '\u00c4'
+ shift+ralt, capslock+ralt: '\u00c4'
+ shift+capslock+ralt: '\u00e4'
}
key APOSTROPHE {
label: '\u00d8'
base: '\u00f8'
shift, capslock: '\u00d8'
+ shift+capslock: '\u00f8'
ralt: '\u00f6'
- ralt+capslock, shift+ralt: '\u00d6'
+ shift+ralt, capslock+ralt: '\u00d6'
+ shift+capslock+ralt: '\u00f6'
}
key BACKSLASH {
@@ -299,53 +333,65 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
ralt: '\u017e'
- ralt+capslock, shift+ralt: '\u017d'
+ shift+ralt, capslock+ralt: '\u017d'
+ shift+capslock+ralt: '\u017e'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
ralt: '\u010d'
- ralt+capslock, shift+ralt: '\u010c'
+ shift+ralt, capslock+ralt: '\u010c'
+ shift+capslock+ralt: '\u010d'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
ralt: '\u01ef'
- ralt+capslock, shift+ralt: '\u01ee'
+ shift+ralt, capslock+ralt: '\u01ee'
+ shift+capslock+ralt: '\u01ef'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
ralt: '\u0292'
- ralt+capslock, shift+ralt: '\u01b7'
+ shift+ralt, capslock+ralt: '\u01b7'
+ shift+capslock+ralt: '\u0292'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
ralt: '\u014b'
- ralt+capslock, shift+ralt: '\u014a'
+ shift+ralt, capslock+ralt: '\u014a'
+ shift+capslock+ralt: '\u014b'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
- ralt, ralt+capslock: '\u00b5'
+ shift+capslock: 'm'
+ ralt: '\u00b5'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
index 6d9c2e5..93a5082 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
@@ -108,68 +108,82 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u00e9'
- shift+ralt: '\u00c9'
+ shift+ralt, capslock+ralt: '\u00c9'
+ shift+capslock+ralt: '\u00e9'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
ralt: '\u00fa'
- shift+ralt: '\u00da'
+ shift+ralt, capslock+ralt: '\u00da'
+ shift+capslock+ralt: '\u00fa'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
ralt: '\u00ed'
- shift+ralt: '\u00cd'
+ shift+ralt, capslock+ralt: '\u00cd'
+ shift+capslock+ralt: '\u00ed'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
ralt: '\u00f3'
- shift+ralt: '\u00d3'
+ shift+ralt, capslock+ralt: '\u00d3'
+ shift+capslock+ralt: '\u00f3'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -190,56 +204,66 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
ralt: '\u00e1'
- shift+ralt: '\u00c1'
+ shift+ralt, capslock+ralt: '\u00c1'
+ shift+capslock+ralt: '\u00e1'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
@@ -274,42 +298,49 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_us.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_us.kcm
index 050b149..da76448 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_english_us.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_english_us.kcm
@@ -106,60 +106,70 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -186,54 +196,63 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
@@ -254,42 +273,49 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_us_colemak.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_us_colemak.kcm
index 72e6d04..e52ccf0 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_english_us_colemak.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_english_us_colemak.kcm
@@ -125,60 +125,70 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key SEMICOLON {
label: ';'
base: ';'
shift, capslock: ':'
+ shift+capslock: ':'
}
key LEFT_BRACKET {
@@ -205,54 +215,63 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
@@ -273,42 +292,49 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_us_dvorak.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_us_dvorak.kcm
index df6a3fd..6ff627b 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_english_us_dvorak.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_english_us_dvorak.kcm
@@ -160,42 +160,49 @@
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SLASH {
@@ -222,60 +229,70 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key MINUS {
@@ -296,52 +313,61 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key Z {
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_us_intl.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_us_intl.kcm
index aa31493..dff17b3 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_english_us_intl.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_english_us_intl.kcm
@@ -121,30 +121,37 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '\u00e4'
shift+ralt, capslock+ralt: '\u00c4'
+ shift+capslock+ralt: '\u00e4'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
ralt: '\u00e5'
shift+ralt, capslock+ralt: '\u00c5'
+ shift+capslock+ralt: '\u00e5'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u00e9'
shift+ralt, capslock+ralt: '\u00c9'
+ shift+capslock+ralt: '\u00e9'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
ralt: '\u00ae'
}
@@ -152,48 +159,60 @@
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
ralt: '\u00fe'
shift+ralt, capslock+ralt: '\u00de'
+ shift+capslock+ralt: '\u00fe'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
ralt: '\u00fc'
shift+ralt, capslock+ralt: '\u00dc'
+ shift+capslock+ralt: '\u00fc'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
ralt: '\u00fa'
shift+ralt, capslock+ralt: '\u00da'
+ shift+capslock+ralt: '\u00fa'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
ralt: '\u00ed'
shift+ralt, capslock+ralt: '\u00cd'
+ shift+capslock+ralt: '\u00ed'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
ralt: '\u00f3'
shift+ralt, capslock+ralt: '\u00d3'
+ shift+capslock+ralt: '\u00f3'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
ralt: '\u00f6'
shift+ralt, capslock+ralt: '\u00d6'
+ shift+capslock+ralt: '\u00f6'
}
key LEFT_BRACKET {
@@ -224,14 +243,17 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
ralt: '\u00e1'
- shift+ralt, ralt+capslock: '\u00c1'
+ shift+ralt, capslock+ralt: '\u00c1'
+ shift+capslock+ralt: '\u00e1'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u00df'
shift+ralt: '\u00a7'
}
@@ -240,46 +262,55 @@
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
ralt: '\u00f0'
shift+ralt, capslock+ralt: '\u00d0'
+ shift+capslock+ralt: '\u00f0'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
ralt: '\u00f8'
shift+ralt, capslock+ralt: '\u00d8'
+ shift+capslock+ralt: '\u00f8'
}
key SEMICOLON {
@@ -312,20 +343,24 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
ralt: '\u00e6'
shift+ralt, capslock+ralt: '\u00c6'
+ shift+capslock+ralt: '\u00e6'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
ralt: '\u00a9'
shift+ralt: '\u00a2'
}
@@ -334,26 +369,31 @@
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
ralt: '\u00f1'
shift+ralt, capslock+ralt: '\u00d1'
+ shift+capslock+ralt: '\u00f1'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
ralt: '\u00b5'
}
@@ -363,6 +403,7 @@
shift: '<'
ralt: '\u00e7'
shift+ralt, capslock+ralt: '\u00c7'
+ shift+capslock+ralt: '\u00e7'
}
key PERIOD {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_us_workman.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_us_workman.kcm
index fe82c8d..713afba 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_english_us_workman.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_english_us_workman.kcm
@@ -129,60 +129,70 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key SEMICOLON {
label: ';'
base: ';'
shift, capslock: ':'
+ shift+capslock: ':'
}
key LEFT_BRACKET {
@@ -209,48 +219,56 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
}
key O {
@@ -263,6 +281,7 @@
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key APOSTROPHE {
@@ -277,42 +296,49 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_estonian.kcm b/packages/InputDevices/res/raw/keyboard_layout_estonian.kcm
index ef545b8..27a03da 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_estonian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_estonian.kcm
@@ -116,18 +116,21 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -135,54 +138,63 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: '\u00dc'
base: '\u00fc'
shift, capslock: '\u00dc'
+ shift+capslock: '\u00fc'
}
key RIGHT_BRACKET {
label: '\u00d5'
base: '\u00f5'
shift, capslock: '\u00d5'
+ shift+capslock: '\u00f5'
ralt: '\u00a7'
}
@@ -192,68 +204,80 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u0161'
- ralt+shift, ralt+capslock: '\u0160'
+ shift+ralt, capslock+ralt: '\u0160'
+ shift+capslock+ralt: '\u0161'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: '\u00d6'
base: '\u00f6'
shift, capslock: '\u00d6'
+ shift+capslock: '\u00f6'
}
key APOSTROPHE {
label: '\u00c4'
base: '\u00e4'
shift, capslock: '\u00c4'
+ shift+capslock: '\u00e4'
ralt: '\u0302'
}
@@ -277,44 +301,52 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
ralt: '\u017e'
- ralt+shift, ralt+capslock: '\u017d'
+ shift+ralt, capslock+ralt: '\u017d'
+ shift+capslock+ralt: '\u017e'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_finnish.kcm b/packages/InputDevices/res/raw/keyboard_layout_finnish.kcm
index b4deed4..79096ad 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_finnish.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_finnish.kcm
@@ -115,76 +115,90 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '\u00e2'
- ralt+capslock, shift+ralt: '\u00c2'
+ shift+ralt, capslock+ralt: '\u00c2'
+ shift+capslock+ralt: '\u00e2'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
- ralt+capslock: '\u20ac'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
ralt: '\u0167'
- ralt+capslock, shift+ralt: '\u0166'
+ shift+ralt, capslock+ralt: '\u0166'
+ shift+capslock+ralt: '\u0167'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
ralt: '\u00ef'
- ralt+capslock, shift+ralt: '\u00cf'
+ shift+ralt, capslock+ralt: '\u00cf'
+ shift+capslock+ralt: '\u00ef'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
ralt: '\u00f5'
- ralt+capslock, shift+ralt: '\u00d5'
+ shift+ralt, capslock+ralt: '\u00d5'
+ shift+capslock+ralt: '\u00f5'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: '\u00c5'
base: '\u00e5'
shift, capslock: '\u00c5'
+ shift+capslock: '\u00e5'
}
key RIGHT_BRACKET {
@@ -200,84 +214,104 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
ralt: '\u00e1'
- ralt+capslock, shift+ralt: '\u00c1'
+ shift+ralt, capslock+ralt: '\u00c1'
+ shift+capslock+ralt: '\u00e1'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u0161'
- ralt+capslock, shift+ralt: '\u0160'
+ shift+ralt, capslock+ralt: '\u0160'
+ shift+capslock+ralt: '\u0161'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
ralt: '\u0111'
- ralt+capslock, shift+ralt: '\u0110'
+ shift+ralt, capslock+ralt: '\u0110'
+ shift+capslock+ralt: '\u0111'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
ralt: '\u01e5'
- ralt+capslock, shift+ralt: '\u01e4'
+ shift+ralt, capslock+ralt: '\u01e4'
+ shift+capslock+ralt: '\u01e5'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
ralt: '\u01e7'
- ralt+capslock, shift+ralt: '\u01e6'
+ shift+ralt, capslock+ralt: '\u01e6'
+ shift+capslock+ralt: '\u01e7'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
ralt: '\u021f'
- ralt+capslock, shift+ralt: '\u021e'
+ shift+ralt, capslock+ralt: '\u021e'
+ shift+capslock+ralt: '\u021f'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
ralt: '\u01e9'
- ralt+capslock, shift+ralt: '\u01e8'
+ shift+ralt, capslock+ralt: '\u01e8'
+ shift+capslock+ralt: '\u01e9'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: '\u00d6'
base: '\u00f6'
shift, capslock: '\u00d6'
+ shift+capslock: '\u00f6'
ralt: '\u00f8'
- ralt+capslock, shift+ralt: '\u00d8'
+ shift+ralt, capslock+ralt: '\u00d8'
+ shift+capslock+ralt: '\u00f8'
}
key APOSTROPHE {
label: '\u00c4'
base: '\u00e4'
shift, capslock: '\u00c4'
+ shift+capslock: '\u00e4'
ralt: '\u00e6'
- ralt+capslock, shift+ralt: '\u00c6'
+ shift+ralt, capslock+ralt: '\u00c6'
+ shift+capslock+ralt: '\u00e6'
}
key BACKSLASH {
@@ -299,53 +333,65 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
ralt: '\u017e'
- ralt+capslock, shift+ralt: '\u017d'
+ shift+ralt, capslock+ralt: '\u017d'
+ shift+capslock+ralt: '\u017e'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
ralt: '\u010d'
- ralt+capslock, shift+ralt: '\u010c'
+ shift+ralt, capslock+ralt: '\u010c'
+ shift+capslock+ralt: '\u010d'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
ralt: '\u01ef'
- ralt+capslock, shift+ralt: '\u01ee'
+ shift+ralt, capslock+ralt: '\u01ee'
+ shift+capslock+ralt: '\u01ef'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
ralt: '\u0292'
- ralt+capslock, shift+ralt: '\u01b7'
+ shift+ralt, capslock+ralt: '\u01b7'
+ shift+capslock+ralt: '\u0292'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
ralt: '\u014b'
- ralt+capslock, shift+ralt: '\u014a'
+ shift+ralt, capslock+ralt: '\u014a'
+ shift+capslock+ralt: '\u014b'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
- ralt, ralt+capslock: '\u00b5'
+ shift+capslock: 'm'
+ ralt: '\u00b5'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_french.kcm b/packages/InputDevices/res/raw/keyboard_layout_french.kcm
index 89e83da..4906304 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_french.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_french.kcm
@@ -123,18 +123,21 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key Z {
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -142,42 +145,49 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -199,60 +209,70 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key APOSTROPHE {
@@ -279,36 +299,42 @@
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_french_ca.kcm b/packages/InputDevices/res/raw/keyboard_layout_french_ca.kcm
index 55ddd60..03b5c19 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_french_ca.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_french_ca.kcm
@@ -119,54 +119,63 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
ralt: '\u00a7'
}
@@ -174,6 +183,7 @@
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
ralt: '\u00b6'
}
@@ -196,54 +206,63 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
@@ -279,42 +298,49 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
ralt: '\u00b5'
}
@@ -335,5 +361,6 @@
label: '\u00c9'
base: '\u00e9'
shift, capslock: '\u00c9'
+ shift+capslock: '\u00e9'
ralt: '\u0301'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_georgian.kcm b/packages/InputDevices/res/raw/keyboard_layout_georgian.kcm
index 35b66a3..a8f229f 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_georgian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_georgian.kcm
@@ -28,6 +28,7 @@
label: '\u201e'
base: '\u201e'
shift, capslock: '\u201c'
+ shift+capslock: '\u201e'
ralt: '`'
ralt+shift: '~'
}
@@ -128,79 +129,92 @@
label: '\u10e5'
base: '\u10e5'
ralt: 'q'
- ralt+shift, ralt+capslock: 'Q'
+ shift+ralt, capslock+ralt: 'Q'
+ shift+capslock+ralt: 'q'
}
key W {
label: '\u10ec'
base: '\u10ec'
shift, capslock: '\u10ed'
+ shift+capslock: '\u10ec'
ralt: 'w'
- ralt+shift, ralt+capslock: 'W'
+ shift+ralt, capslock+ralt: 'W'
+ shift+capslock+ralt: 'w'
}
key E {
label: '\u10d4'
base: '\u10d4'
ralt: 'e'
- ralt+shift, ralt+capslock: 'E'
+ shift+ralt, capslock+ralt: 'E'
+ shift+capslock+ralt: 'e'
}
key R {
label: '\u10e0'
base: '\u10e0'
shift, capslock: '\u10e6'
+ shift+capslock: '\u10e0'
ralt: 'r'
- ralt+shift, ralt+capslock: 'R'
+ shift+ralt, capslock+ralt: 'R'
+ shift+capslock+ralt: 'r'
}
key T {
label: '\u10e2'
base: '\u10e2'
shift, capslock: '\u10d7'
+ shift+capslock: '\u10e2'
ralt: 't'
- ralt+shift, ralt+capslock: 'T'
+ shift+ralt, capslock+ralt: 'T'
+ shift+capslock+ralt: 't'
}
key Y {
label: '\u10e7'
base: '\u10e7'
ralt: 'y'
- ralt+shift, ralt+capslock: 'Y'
+ shift+ralt, capslock+ralt: 'Y'
+ shift+capslock+ralt: 'y'
}
key U {
label: '\u10e3'
base: '\u10e3'
ralt: 'u'
- ralt+shift, ralt+capslock: 'U'
+ shift+ralt, capslock+ralt: 'U'
+ shift+capslock+ralt: 'u'
}
key I {
label: '\u10d8'
base: '\u10d8'
ralt: 'i'
- ralt+shift, ralt+capslock: 'I'
+ shift+ralt, capslock+ralt: 'I'
+ shift+capslock+ralt: 'i'
}
key O {
label: '\u10dd'
base: '\u10dd'
ralt: 'o'
- ralt+shift, ralt+capslock: 'O'
+ shift+ralt, capslock+ralt: 'O'
+ shift+capslock+ralt: 'o'
}
key P {
label: '\u10de'
base: '\u10de'
ralt: 'p'
- ralt+shift, ralt+capslock: 'P'
+ shift+ralt, capslock+ralt: 'P'
+ shift+capslock+ralt: 'p'
}
key LEFT_BRACKET {
label: '['
base: '['
- shift, capslock: '{'
+ shift: '{'
ralt: '['
ralt+shift: '{'
}
@@ -208,7 +222,7 @@
key RIGHT_BRACKET {
label: ']'
base: ']'
- shift, capslock: '}'
+ shift: '}'
ralt: ']'
ralt+shift: '}'
}
@@ -227,72 +241,84 @@
label: '\u10d0'
base: '\u10d0'
ralt: 'a'
- ralt+shift, ralt+capslock: 'A'
+ shift+ralt, capslock+ralt: 'A'
+ shift+capslock+ralt: 'a'
}
key S {
label: '\u10e1'
base: '\u10e1'
shift, capslock: '\u10e8'
+ shift+capslock: '\u10e1'
ralt: 's'
- ralt+shift, ralt+capslock: 'S'
+ shift+ralt, capslock+ralt: 'S'
+ shift+capslock+ralt: 's'
}
key D {
label: '\u10d3'
base: '\u10d3'
ralt: 'd'
- ralt+shift, ralt+capslock: 'D'
+ shift+ralt, capslock+ralt: 'D'
+ shift+capslock+ralt: 'd'
}
key F {
label: '\u10e4'
base: '\u10e4'
ralt: 'f'
- ralt+shift, ralt+capslock: 'F'
+ shift+ralt, capslock+ralt: 'F'
+ shift+capslock+ralt: 'f'
}
key G {
label: '\u10d2'
base: '\u10d2'
ralt: 'g'
- ralt+shift, ralt+capslock: 'G'
+ shift+ralt, capslock+ralt: 'G'
+ shift+capslock+ralt: 'g'
}
key H {
label: '\u10f0'
base: '\u10f0'
ralt: 'h'
- ralt+shift, ralt+capslock: 'H'
+ shift+ralt, capslock+ralt: 'H'
+ shift+capslock+ralt: 'h'
}
key J {
label: '\u10ef'
base: '\u10ef'
shift, capslock: '\u10df'
+ shift+capslock: '\u10ef'
ralt: 'j'
- ralt+shift, ralt+capslock: 'J'
+ shift+ralt, capslock+ralt: 'J'
+ shift+capslock+ralt: 'j'
}
key K {
label: '\u10d9'
base: '\u10d9'
ralt: 'k'
- ralt+shift, ralt+capslock: 'K'
+ shift+ralt, capslock+ralt: 'K'
+ shift+capslock+ralt: 'k'
}
key L {
label: '\u10da'
base: '\u10da'
shift, capslock: '\u20be'
+ shift+capslock: '\u10da'
ralt: 'l'
- ralt+shift, ralt+capslock: 'L'
+ shift+ralt, capslock+ralt: 'L'
+ shift+capslock+ralt: 'l'
}
key SEMICOLON {
label: ';'
base: ';'
- shift, capslock: ':'
+ shift: ':'
ralt: ';'
ralt+shift: ':'
}
@@ -300,7 +326,7 @@
key APOSTROPHE {
label: '\''
base: '\''
- shift, capslock: '"'
+ shift: '"'
ralt: '\''
ralt+shift: '"'
}
@@ -311,57 +337,66 @@
label: '\u10d6'
base: '\u10d6'
shift, capslock: '\u10eb'
+ shift+capslock: '\u10d6'
ralt: 'z'
- ralt+shift, ralt+capslock: 'Z'
+ shift+ralt, capslock+ralt: 'Z'
+ shift+capslock+ralt: 'z'
}
key X {
label: '\u10ee'
base: '\u10ee'
ralt: 'x'
- ralt+shift, ralt+capslock: 'X'
+ shift+ralt, capslock+ralt: 'X'
+ shift+capslock+ralt: 'x'
}
key C {
label: '\u10ea'
base: '\u10ea'
shift, capslock: '\u10e9'
+ shift+capslock: '\u10ea'
ralt: 'c'
- ralt+shift, ralt+capslock: 'C'
+ shift+ralt, capslock+ralt: 'C'
+ shift+capslock+ralt: 'c'
}
key V {
label: '\u10d5'
base: '\u10d5'
ralt: 'v'
- ralt+shift, ralt+capslock: 'V'
+ shift+ralt, capslock+ralt: 'V'
+ shift+capslock+ralt: 'v'
}
key B {
label: '\u10d1'
base: '\u10d1'
ralt: 'b'
- ralt+shift, ralt+capslock: 'B'
+ shift+ralt, capslock+ralt: 'B'
+ shift+capslock+ralt: 'b'
}
key N {
label: '\u10dc'
base: '\u10dc'
ralt: 'n'
- ralt+shift, ralt+capslock: 'N'
+ shift+ralt, capslock+ralt: 'N'
+ shift+capslock+ralt: 'n'
}
key M {
label: '\u10db'
base: '\u10db'
ralt: 'm'
- ralt+shift, ralt+capslock: 'M'
+ shift+ralt, capslock+ralt: 'M'
+ shift+capslock+ralt: 'm'
}
key COMMA {
label: ','
base: ','
- shift, capslock: '<'
+ shift: '<'
ralt: ','
ralt+shift: '<'
}
@@ -369,7 +404,7 @@
key PERIOD {
label: '.'
base: '.'
- shift, capslock: '>'
+ shift: '>'
ralt: '.'
ralt+shift: '>'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_german.kcm b/packages/InputDevices/res/raw/keyboard_layout_german.kcm
index d9caa32..23ccc9a 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_german.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_german.kcm
@@ -18,7 +18,7 @@
type OVERLAY
-map key 12 SLASH # § ? \
+map key 12 SLASH # § ? \
map key 21 Z
map key 44 Y
map key 53 MINUS # - _
@@ -117,6 +117,7 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '@'
}
@@ -124,12 +125,14 @@
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -137,48 +140,56 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Z {
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: '\u00dc'
base: '\u00fc'
shift, capslock: '\u00dc'
+ shift+capslock: '\u00fc'
}
key RIGHT_BRACKET {
@@ -194,66 +205,77 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: '\u00d6'
base: '\u00f6'
shift, capslock: '\u00d6'
+ shift+capslock: '\u00f6'
}
key APOSTROPHE {
label: '\u00c4'
base: '\u00e4'
shift, capslock: '\u00c4'
+ shift+capslock: '\u00e4'
}
key BACKSLASH {
@@ -275,42 +297,49 @@
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
ralt: '\u00b5'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_greek.kcm b/packages/InputDevices/res/raw/keyboard_layout_greek.kcm
index a7684e1..6eff114 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_greek.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_greek.kcm
@@ -24,88 +24,88 @@
key GRAVE {
label: '`'
- base, capslock: '`'
+ base: '`'
shift: '~'
}
key 1 {
label: '1'
- base, capslock: '1'
+ base: '1'
shift: '!'
}
key 2 {
label: '2'
- base, capslock: '2'
+ base: '2'
shift: '@'
ralt: '\u00b2'
}
key 3 {
label: '3'
- base, capslock: '3'
+ base: '3'
shift: '#'
ralt: '\u00b3'
}
key 4 {
label: '4'
- base, capslock: '4'
+ base: '4'
shift: '$'
ralt: '\u00a3'
}
key 5 {
label: '5'
- base, capslock: '5'
+ base: '5'
shift: '%'
ralt: '\u00a7'
}
key 6 {
label: '6'
- base, capslock: '6'
+ base: '6'
shift: '^'
ralt: '\u00b6'
}
key 7 {
label: '7'
- base, capslock: '7'
+ base: '7'
shift: '&'
}
key 8 {
label: '8'
- base, capslock: '8'
+ base: '8'
shift: '*'
ralt: '\u00a4'
}
key 9 {
label: '9'
- base, capslock: '9'
+ base: '9'
shift: '('
ralt: '\u00a6'
}
key 0 {
label: '0'
- base, capslock: '0'
+ base: '0'
shift: ')'
ralt: '\u00b0'
}
key MINUS {
label: '-'
- base, capslock: '-'
+ base: '-'
shift: '_'
ralt: '\u00b1'
}
key EQUALS {
label: '='
- base, capslock: '='
+ base: '='
shift: '+'
ralt: '\u00bd'
}
@@ -114,13 +114,13 @@
key Q {
label: 'Q'
- base, capslock: ';'
+ base: ';'
shift: ':'
}
key W {
label: 'W'
- base, capslock: '\u03c2'
+ base: '\u03c2'
shift: '\u0385'
}
@@ -128,6 +128,7 @@
label: 'E'
base: '\u03b5'
shift, capslock: '\u0395'
+ shift+capslock: '\u03b5'
ralt: '\u20ac'
}
@@ -135,6 +136,7 @@
label: 'R'
base: '\u03c1'
shift, capslock: '\u03a1'
+ shift+capslock: '\u03c1'
ralt: '\u00ae'
}
@@ -142,12 +144,14 @@
label: 'T'
base: '\u03c4'
shift, capslock: '\u03a4'
+ shift+capslock: '\u03c4'
}
key Y {
label: 'Y'
base: '\u03c5'
shift, capslock: '\u03a5'
+ shift+capslock: '\u03c5'
ralt: '\u00a5'
}
@@ -155,36 +159,40 @@
label: 'U'
base: '\u03b8'
shift, capslock: '\u0398'
+ shift+capslock: '\u03b8'
}
key I {
label: 'I'
base: '\u03b9'
shift, capslock: '\u0399'
+ shift+capslock: '\u03b9'
}
key O {
label: 'O'
base: '\u03bf'
shift, capslock: '\u039f'
+ shift+capslock: '\u03bf'
}
key P {
label: 'P'
base: '\u03c0'
shift, capslock: '\u03a0'
+ shift+capslock: '\u03c0'
}
key LEFT_BRACKET {
label: '['
- base, capslock: '['
+ base: '['
shift: '{'
ralt: '\u00ab'
}
key RIGHT_BRACKET {
label: ']'
- base, capslock: ']'
+ base: ']'
shift: '}'
ralt: '\u00bb'
}
@@ -195,59 +203,68 @@
label: 'A'
base: '\u03b1'
shift, capslock: '\u0391'
+ shift+capslock: '\u03b1'
}
key S {
label: 'S'
base: '\u03c3'
shift, capslock: '\u03a3'
+ shift+capslock: '\u03c3'
}
key D {
label: 'D'
base: '\u03b4'
shift, capslock: '\u0394'
+ shift+capslock: '\u03b4'
}
key F {
label: 'F'
base: '\u03c6'
shift, capslock: '\u03a6'
+ shift+capslock: '\u03c6'
}
key G {
label: 'G'
base: '\u03b3'
shift, capslock: '\u0393'
+ shift+capslock: '\u03b3'
}
key H {
label: 'H'
base: '\u03b7'
shift, capslock: '\u0397'
+ shift+capslock: '\u03b7'
}
key J {
label: 'J'
base: '\u03be'
shift, capslock: '\u039e'
+ shift+capslock: '\u03be'
}
key K {
label: 'K'
base: '\u03ba'
shift, capslock: '\u039a'
+ shift+capslock: '\u03ba'
}
key L {
label: 'L'
base: '\u03bb'
shift, capslock: '\u039b'
+ shift+capslock: '\u03bb'
}
key SEMICOLON {
label: ';'
- base, capslock: '\u0301'
+ base: '\u0301'
#should be \u0384 (greek tonos)
shift: '\u0308'
ralt: '\u0385'
@@ -255,13 +272,13 @@
key APOSTROPHE {
label: '\''
- base, capslock: '\''
+ base: '\''
shift: '"'
}
key BACKSLASH {
label: '\\'
- base, capslock: '\\'
+ base: '\\'
shift: '|'
ralt: '\u00ac'
}
@@ -270,7 +287,7 @@
key PLUS {
label: '<'
- base, capslock: '<'
+ base: '<'
shift: '>'
ralt: '\\'
shift+ralt: '|'
@@ -280,18 +297,21 @@
label: 'Z'
base: '\u03b6'
shift, capslock: '\u0396'
+ shift+capslock: '\u03b6'
}
key X {
label: 'X'
base: '\u03c7'
shift, capslock: '\u03a7'
+ shift+capslock: '\u03c7'
}
key C {
label: 'C'
base: '\u03c8'
shift, capslock: '\u03a8'
+ shift+capslock: '\u03c8'
ralt: '\u00a9'
}
@@ -299,40 +319,44 @@
label: 'V'
base: '\u03c9'
shift, capslock: '\u03a9'
+ shift+capslock: '\u03c9'
}
key B {
label: 'B'
base: '\u03b2'
shift, capslock: '\u0392'
+ shift+capslock: '\u03b2'
}
key N {
label: 'N'
base: '\u03bd'
shift, capslock: '\u039d'
+ shift+capslock: '\u03bd'
}
key M {
label: 'M'
base: '\u03bc'
shift, capslock: '\u039c'
+ shift+capslock: '\u03bc'
}
key COMMA {
label: ','
- base, capslock: ','
+ base: ','
shift: '<'
}
key PERIOD {
label: '.'
- base, capslock: '.'
+ base: '.'
shift: '>'
}
key SLASH {
label: '/'
- base, capslock: '/'
+ base: '/'
shift: '?'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_hebrew.kcm b/packages/InputDevices/res/raw/keyboard_layout_hebrew.kcm
index 283cb4e..11ade42 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_hebrew.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_hebrew.kcm
@@ -121,18 +121,21 @@
label: 'Q'
base: '/'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: '\u0027'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: '\u05e7'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -140,24 +143,28 @@
label: 'R'
base: '\u05e8'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: '\u05d0'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: '\u05d8'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: '\u05d5'
shift, capslock: 'U'
+ shift+capslock: 'u'
ralt: '\u05f0'
}
@@ -165,29 +172,32 @@
label: 'I'
base: '\u05df'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: '\u05dd'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: '\u05e4'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: ']'
- base, capslock: ']'
+ base: ']'
shift: '}'
}
key RIGHT_BRACKET {
label: '['
- base, capslock: '['
+ base: '['
shift: '{'
}
@@ -197,36 +207,42 @@
label: 'A'
base: '\u05e9'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: '\u05d3'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: '\u05d2'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: '\u05db'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: '\u05e2'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: '\u05d9'
shift, capslock: 'H'
+ shift+capslock: 'h'
ralt: '\u05f2'
}
@@ -234,6 +250,7 @@
label: 'J'
base: '\u05d7'
shift, capslock: 'J'
+ shift+capslock: 'j'
ralt: '\u05f1'
}
@@ -241,12 +258,14 @@
label: 'K'
base: '\u05dc'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: '\u05da'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
@@ -254,6 +273,7 @@
base: '\u05e3'
shift: ':'
capslock: ';'
+ shift+capslock: ':'
}
key APOSTROPHE {
@@ -261,6 +281,7 @@
base: ','
shift: '"'
capslock: '\''
+ shift+capslock: '"'
}
key BACKSLASH {
@@ -273,7 +294,7 @@
key PLUS {
label: '\\'
- base, capslock: '\\'
+ base: '\\'
shift: '|'
}
@@ -281,42 +302,49 @@
label: 'Z'
base: '\u05d6'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: '\u05e1'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: '\u05d1'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: '\u05d4'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: '\u05e0'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: '\u05de'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: '\u05e6'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
@@ -324,6 +352,7 @@
base: '\u05ea'
shift: '>'
capslock: ','
+ shift+capslock: '>'
}
key PERIOD {
@@ -331,6 +360,7 @@
base: '\u05e5'
shift: '<'
capslock: '.'
+ shift+capslock: '<'
}
key SLASH {
@@ -338,4 +368,5 @@
base: '.'
shift: '?'
capslock: '/'
+ shift+capslock: '?'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_hungarian.kcm b/packages/InputDevices/res/raw/keyboard_layout_hungarian.kcm
index dafb50b..6c947c7 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_hungarian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_hungarian.kcm
@@ -101,6 +101,7 @@
label: '\u00d6'
base: '\u00f6'
shift, capslock: '\u00d6'
+ shift+capslock: '\u00f6'
ralt: '\u030b'
}
@@ -108,6 +109,7 @@
label: '\u00dc'
base: '\u00fc'
shift, capslock: '\u00dc'
+ shift+capslock: '\u00fc'
ralt: '\u0308'
}
@@ -115,6 +117,7 @@
label: '\u00d3'
base: '\u00f3'
shift, capslock: '\u00d3'
+ shift+capslock: '\u00f3'
ralt: '\u0327'
}
@@ -124,6 +127,7 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '\\'
}
@@ -131,6 +135,7 @@
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
ralt: '|'
}
@@ -138,6 +143,7 @@
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u00c4'
}
@@ -145,24 +151,28 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Z {
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
ralt: '\u20ac'
}
@@ -170,6 +180,7 @@
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
ralt: '\u00cd'
}
@@ -177,18 +188,21 @@
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: '\u0150'
base: '\u0151'
shift, capslock: '\u0150'
+ shift+capslock: '\u0151'
ralt: '\u00f7'
}
@@ -196,6 +210,7 @@
label: '\u00da'
base: '\u00fa'
shift, capslock: '\u00da'
+ shift+capslock: '\u00fa'
ralt: '\u00d7'
}
@@ -205,6 +220,7 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
ralt: '\u00e4'
}
@@ -212,6 +228,7 @@
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u0111'
}
@@ -219,6 +236,7 @@
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
ralt: '\u0110'
}
@@ -226,6 +244,7 @@
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
ralt: '['
}
@@ -233,6 +252,7 @@
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
ralt: ']'
}
@@ -240,12 +260,14 @@
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
ralt: '\u00ed'
}
@@ -253,6 +275,7 @@
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
ralt: '\u0197'
}
@@ -260,6 +283,7 @@
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
ralt: '\u0141'
}
@@ -267,6 +291,7 @@
label: '\u00c9'
base: '\u00e9'
shift, capslock: '\u00c9'
+ shift+capslock: '\u00e9'
ralt: '$'
}
@@ -274,6 +299,7 @@
label: '\u00c1'
base: '\u00e1'
shift, capslock: '\u00c1'
+ shift+capslock: '\u00e1'
ralt: '\u00df'
}
@@ -281,6 +307,7 @@
label: '\u0170'
base: '\u0171'
shift, capslock: '\u0170'
+ shift+capslock: '\u0171'
ralt: '\u00a4'
}
@@ -290,6 +317,7 @@
label: '\u00cd'
base: '\u00ed'
shift, capslock: '\u00cd'
+ shift+capslock: '\u00ed'
ralt: '<'
}
@@ -297,6 +325,7 @@
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
ralt: '>'
}
@@ -304,6 +333,7 @@
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
ralt: '#'
}
@@ -311,6 +341,7 @@
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
ralt: '&'
}
@@ -318,6 +349,7 @@
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
ralt: '@'
}
@@ -325,6 +357,7 @@
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
ralt: '{'
}
@@ -332,6 +365,7 @@
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
ralt: '}'
}
@@ -339,6 +373,7 @@
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_icelandic.kcm b/packages/InputDevices/res/raw/keyboard_layout_icelandic.kcm
index 117f58b..5131b4f 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_icelandic.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_icelandic.kcm
@@ -99,6 +99,7 @@
label: '\u00d6'
base: '\u00f6'
shift, capslock: '\u00d6'
+ shift+capslock: '\u00f6'
ralt: '\\'
}
@@ -114,6 +115,7 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '@'
}
@@ -121,12 +123,14 @@
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -134,48 +138,56 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: '\u0110'
base: '\u0111'
shift, capslock: '\u0110'
+ shift+capslock: '\u0111'
}
key RIGHT_BRACKET {
@@ -191,60 +203,70 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: '\u00c6'
base: '\u00e6'
shift, capslock: '\u00c6'
+ shift+capslock: '\u00e6'
}
key APOSTROPHE {
@@ -274,42 +296,49 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
ralt: '\u00b5'
}
@@ -329,4 +358,5 @@
label: '\u00de'
base: '\u00fe'
shift, capslock: '\u00de'
+ shift+capslock: '\u00fe'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_italian.kcm b/packages/InputDevices/res/raw/keyboard_layout_italian.kcm
index bd2d25a..309d8b2 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_italian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_italian.kcm
@@ -109,18 +109,21 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -128,42 +131,49 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -188,54 +198,63 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
@@ -270,42 +289,49 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_latvian_qwerty.kcm b/packages/InputDevices/res/raw/keyboard_layout_latvian_qwerty.kcm
index d4bc0c0..3b77cb1 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_latvian_qwerty.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_latvian_qwerty.kcm
@@ -119,70 +119,85 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u0113'
- shift+ralt, ralt+capslock: '\u0112'
+ shift+ralt, capslock+ralt: '\u0112'
+ shift+capslock+ralt: '\u0113'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
ralt: '\u0157'
- shift+ralt, ralt+capslock: '\u0156'
+ shift+ralt, capslock+ralt: '\u0156'
+ shift+capslock+ralt: '\u0157'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
ralt: '\u016b'
- shift+ralt, ralt+capslock: '\u016a'
+ shift+ralt, capslock+ralt: '\u016a'
+ shift+capslock+ralt: '\u016b'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
ralt: '\u012b'
- shift+ralt, ralt+capslock: '\u012a'
+ shift+ralt, capslock+ralt: '\u012a'
+ shift+capslock+ralt: '\u012b'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
ralt: '\u00f5'
- shift+ralt, ralt+capslock: '\u00d5'
+ shift+ralt, capslock+ralt: '\u00d5'
+ shift+capslock+ralt: '\u00f5'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -204,64 +219,78 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
ralt: '\u0101'
- shift+ralt, ralt+capslock: '\u0100'
+ shift+ralt, capslock+ralt: '\u0100'
+ shift+capslock+ralt: '\u0101'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u0161'
- shift+ralt, ralt+capslock: '\u0160'
+ shift+ralt, capslock+ralt: '\u0160'
+ shift+capslock+ralt: '\u0161'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
ralt: '\u0123'
- shift+ralt, ralt+capslock: '\u0122'
+ shift+ralt, capslock+ralt: '\u0122'
+ shift+capslock+ralt: '\u0123'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
ralt: '\u0137'
- shift+ralt, ralt+capslock: '\u0136'
+ shift+ralt, capslock+ralt: '\u0136'
+ shift+capslock+ralt: '\u0137'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
ralt: '\u013c'
- shift+ralt, ralt+capslock: '\u013b'
+ shift+ralt, capslock+ralt: '\u013b'
+ shift+capslock+ralt: '\u013c'
}
key SEMICOLON {
@@ -298,48 +327,58 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
ralt: '\u017e'
- shift+ralt, ralt+capslock: '\u017d'
+ shift+ralt, capslock+ralt: '\u017d'
+ shift+capslock+ralt: '\u017e'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
ralt: '\u010d'
- shift+ralt, ralt+capslock: '\u010c'
+ shift+ralt, capslock+ralt: '\u010c'
+ shift+capslock+ralt: '\u010d'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
ralt: '\u0146'
- shift+ralt, ralt+capslock: '\u0145'
+ shift+ralt, capslock+ralt: '\u0145'
+ shift+capslock+ralt: '\u0146'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_lithuanian.kcm b/packages/InputDevices/res/raw/keyboard_layout_lithuanian.kcm
index 72ca333..bcfdb12 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_lithuanian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_lithuanian.kcm
@@ -32,6 +32,7 @@
label: '1'
base: '\u0105'
shift, capslock: '\u0104'
+ shift+capslock: '\u0105'
ralt: '1'
shift+ralt: '!'
}
@@ -40,6 +41,7 @@
label: '2'
base: '\u010d'
shift, capslock: '\u010c'
+ shift+capslock: '\u010d'
ralt: '2'
shift+ralt: '@'
}
@@ -48,6 +50,7 @@
label: '3'
base: '\u0119'
shift, capslock: '\u0118'
+ shift+capslock: '\u0119'
ralt: '3'
shift+ralt: '#'
}
@@ -56,6 +59,7 @@
label: '4'
base: '\u0117'
shift, capslock: '\u0116'
+ shift+capslock: '\u0117'
ralt: '4'
shift+ralt: '$'
}
@@ -64,6 +68,7 @@
label: '5'
base: '\u012f'
shift, capslock: '\u012e'
+ shift+capslock: '\u012f'
ralt: '5'
shift+ralt: '%'
}
@@ -72,6 +77,7 @@
label: '6'
base: '\u0161'
shift, capslock: '\u0160'
+ shift+capslock: '\u0161'
ralt: '6'
shift+ralt: '\u0302'
}
@@ -80,6 +86,7 @@
label: '7'
base: '\u0173'
shift, capslock: '\u0172'
+ shift+capslock: '\u0173'
ralt: '7'
shift+ralt: '&'
}
@@ -88,6 +95,7 @@
label: '8'
base: '\u016b'
shift, capslock: '\u016a'
+ shift+capslock: '\u016b'
ralt: '8'
shift+ralt: '*'
}
@@ -116,6 +124,7 @@
label: '='
base: '\u017e'
shift, capslock: '\u017d'
+ shift+capslock: '\u017e'
ralt: '='
shift+ralt: '+'
}
@@ -126,18 +135,21 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -145,42 +157,49 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -201,54 +220,63 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
@@ -281,42 +309,49 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_mongolian.kcm b/packages/InputDevices/res/raw/keyboard_layout_mongolian.kcm
index 3d4a8c6..77cc672 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_mongolian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_mongolian.kcm
@@ -28,6 +28,7 @@
label: '='
base: '='
shift, capslock: '+'
+ shift+capslock: '+'
ralt: '`'
ralt+shift: '~'
}
@@ -122,86 +123,107 @@
label: '\u0444'
base: '\u0444'
shift, capslock: '\u0424'
+ shift+capslock: '\u0444'
ralt: 'q'
- ralt+shift, ralt+capslock: 'Q'
+ shift+ralt, capslock+ralt: 'Q'
+ shift+capslock+ralt: 'q'
}
key W {
label: '\u0446'
base: '\u0446'
shift, capslock: '\u0426'
+ shift+capslock: '\u0446'
ralt: 'w'
- ralt+shift, ralt+capslock: 'W'
+ shift+ralt, capslock+ralt: 'W'
+ shift+capslock+ralt: 'w'
}
key E {
label: '\u0443'
base: '\u0443'
shift, capslock: '\u0423'
+ shift+capslock: '\u0443'
ralt: 'e'
- ralt+shift, ralt+capslock: 'E'
+ shift+ralt, capslock+ralt: 'E'
+ shift+capslock+ralt: 'e'
}
key R {
label: '\u0436'
base: '\u0436'
shift, capslock: '\u0416'
+ shift+capslock: '\u0436'
ralt: 'r'
- ralt+shift, ralt+capslock: 'R'
+ shift+ralt, capslock+ralt: 'R'
+ shift+capslock+ralt: 'r'
}
key T {
label: '\u044d'
base: '\u044d'
shift, capslock: '\u042d'
+ shift+capslock: '\u044d'
ralt: 't'
- ralt+shift, ralt+capslock: 'T'
+ shift+ralt, capslock+ralt: 'T'
+ shift+capslock+ralt: 't'
}
key Y {
label: '\u043d'
base: '\u043d'
shift, capslock: '\u041d'
+ shift+capslock: '\u043d'
ralt: 'y'
- ralt+shift, ralt+capslock: 'Y'
+ shift+ralt, capslock+ralt: 'Y'
+ shift+capslock+ralt: 'y'
}
key U {
label: '\u0433'
base: '\u0433'
shift, capslock: '\u0413'
+ shift+capslock: '\u0433'
ralt: 'u'
- ralt+shift, ralt+capslock: 'U'
+ shift+ralt, capslock+ralt: 'U'
+ shift+capslock+ralt: 'u'
}
key I {
label: '\u0448'
base: '\u0448'
shift, capslock: '\u0428'
+ shift+capslock: '\u0448'
ralt: 'i'
- ralt+shift, ralt+capslock: 'I'
+ shift+ralt, capslock+ralt: 'I'
+ shift+capslock+ralt: 'i'
}
key O {
label: '\u04af'
base: '\u04af'
shift, capslock: '\u04ae'
+ shift+capslock: '\u04af'
ralt: 'o'
- ralt+shift, ralt+capslock: 'O'
+ shift+ralt, capslock+ralt: 'O'
+ shift+capslock+ralt: 'o'
}
key P {
label: '\u0437'
base: '\u0437'
shift, capslock: '\u0417'
+ shift+capslock: '\u0437'
ralt: 'p'
- ralt+shift, ralt+capslock: 'P'
+ shift+ralt, capslock+ralt: 'P'
+ shift+capslock+ralt: 'p'
}
key LEFT_BRACKET {
label: '\u043a'
base: '\u043a'
shift, capslock: '\u041a'
+ shift+capslock: '\u043a'
ralt: '['
ralt+shift: '{'
}
@@ -210,6 +232,7 @@
label: '\u044a'
base: '\u044a'
shift, capslock: '\u042a'
+ shift+capslock: '\u044a'
ralt: ']'
ralt+shift: '}'
}
@@ -220,78 +243,97 @@
label: '\u0439'
base: '\u0439'
shift, capslock: '\u0419'
+ shift+capslock: '\u0439'
ralt: 'a'
- ralt+shift, ralt+capslock: 'A'
+ shift+ralt, capslock+ralt: 'A'
+ shift+capslock+ralt: 'a'
}
key S {
label: '\u044b'
base: '\u044b'
shift, capslock: '\u042b'
+ shift+capslock: '\u044b'
ralt: 's'
- ralt+shift, ralt+capslock: 'S'
+ shift+ralt, capslock+ralt: 'S'
+ shift+capslock+ralt: 's'
}
key D {
label: '\u0431'
base: '\u0431'
shift, capslock: '\u0411'
+ shift+capslock: '\u0431'
ralt: 'd'
- ralt+shift, ralt+capslock: 'D'
+ shift+ralt, capslock+ralt: 'D'
+ shift+capslock+ralt: 'd'
}
key F {
label: '\u04e9'
base: '\u04e9'
shift, capslock: '\u04e8'
+ shift+capslock: '\u04e9'
ralt: 'f'
- ralt+shift, ralt+capslock: 'F'
+ shift+ralt, capslock+ralt: 'F'
+ shift+capslock+ralt: 'f'
}
key G {
label: '\u0430'
base: '\u0430'
shift, capslock: '\u0410'
+ shift+capslock: '\u0430'
ralt: 'g'
- ralt+shift, ralt+capslock: 'G'
+ shift+ralt, capslock+ralt: 'G'
+ shift+capslock+ralt: 'g'
}
key H {
label: '\u0445'
base: '\u0445'
shift, capslock: '\u0425'
+ shift+capslock: '\u0445'
ralt: 'h'
- ralt+shift, ralt+capslock: 'H'
+ shift+ralt, capslock+ralt: 'H'
+ shift+capslock+ralt: 'h'
}
key J {
label: '\u0440'
base: '\u0440'
shift, capslock: '\u0420'
+ shift+capslock: '\u0440'
ralt: 'j'
- ralt+shift, ralt+capslock: 'J'
+ shift+ralt, capslock+ralt: 'J'
+ shift+capslock+ralt: 'j'
}
key K {
label: '\u043e'
base: '\u043e'
shift, capslock: '\u041e'
+ shift+capslock: '\u043e'
ralt: 'k'
- ralt+shift, ralt+capslock: 'K'
+ shift+ralt, capslock+ralt: 'K'
+ shift+capslock+ralt: 'k'
}
key L {
label: '\u043b'
base: '\u043b'
shift, capslock: '\u041b'
+ shift+capslock: '\u043b'
ralt: 'l'
- ralt+shift, ralt+capslock: 'L'
+ shift+ralt, capslock+ralt: 'L'
+ shift+capslock+ralt: 'l'
}
key SEMICOLON {
label: '\u0434'
base: '\u0434'
shift, capslock: '\u0414'
+ shift+capslock: '\u0434'
ralt: ';'
ralt+shift: ':'
}
@@ -300,6 +342,7 @@
label: '\u043f'
base: '\u043f'
shift, capslock: '\u041f'
+ shift+capslock: '\u043f'
ralt: '\''
ralt+shift: '"'
}
@@ -318,62 +361,77 @@
label: '\u044f'
base: '\u044f'
shift, capslock: '\u042f'
+ shift+capslock: '\u044f'
ralt: 'z'
- ralt+shift, ralt+capslock: 'Z'
+ shift+ralt, capslock+ralt: 'Z'
+ shift+capslock+ralt: 'z'
}
key X {
label: '\u0447'
base: '\u0447'
shift, capslock: '\u0427'
+ shift+capslock: '\u0447'
ralt: 'x'
- ralt+shift, ralt+capslock: 'X'
+ shift+ralt, capslock+ralt: 'X'
+ shift+capslock+ralt: 'x'
}
key C {
label: '\u0451'
base: '\u0451'
shift, capslock: '\u0401'
+ shift+capslock: '\u0451'
ralt: 'c'
- ralt+shift, ralt+capslock: 'C'
+ shift+ralt, capslock+ralt: 'C'
+ shift+capslock+ralt: 'c'
}
key V {
label: '\u0441'
base: '\u0441'
shift, capslock: '\u0421'
+ shift+capslock: '\u0441'
ralt: 'v'
- ralt+shift, ralt+capslock: 'V'
+ shift+ralt, capslock+ralt: 'V'
+ shift+capslock+ralt: 'v'
}
key B {
label: '\u043c'
base: '\u043c'
shift, capslock: '\u041c'
+ shift+capslock: '\u043c'
ralt: 'b'
- ralt+shift, ralt+capslock: 'B'
+ shift+ralt, capslock+ralt: 'B'
+ shift+capslock+ralt: 'b'
}
key N {
label: '\u0438'
base: '\u0438'
shift, capslock: '\u0418'
+ shift+capslock: '\u0438'
ralt: 'n'
- ralt+shift, ralt+capslock: 'N'
+ shift+ralt, capslock+ralt: 'N'
+ shift+capslock+ralt: 'n'
}
key M {
label: '\u0442'
base: '\u0442'
shift, capslock: '\u0422'
+ shift+capslock: '\u0442'
ralt: 'm'
- ralt+shift, ralt+capslock: 'M'
+ shift+ralt, capslock+ralt: 'M'
+ shift+capslock+ralt: 'm'
}
key COMMA {
label: '\u044c'
base: '\u044c'
shift, capslock: '\u042c'
+ shift+capslock: '\u044c'
ralt: ','
ralt+shift: '<'
}
@@ -382,6 +440,7 @@
label: '\u0432'
base: '\u0432'
shift, capslock: '\u0412'
+ shift+capslock: '\u0432'
ralt: '.'
ralt+shift: '>'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_norwegian.kcm b/packages/InputDevices/res/raw/keyboard_layout_norwegian.kcm
index 560dd16..cae1c94 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_norwegian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_norwegian.kcm
@@ -115,76 +115,90 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '\u00e2'
- ralt+capslock, shift+ralt: '\u00c2'
+ shift+ralt, capslock+ralt: '\u00c2'
+ shift+capslock+ralt: '\u00e2'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
- ralt+capslock: '\u20ac'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
ralt: '\u0167'
- ralt+capslock, shift+ralt: '\u0166'
+ shift+ralt, capslock+ralt: '\u0166'
+ shift+capslock+ralt: '\u0167'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
ralt: '\u00ef'
- ralt+capslock, shift+ralt: '\u00cf'
+ shift+ralt, capslock+ralt: '\u00cf'
+ shift+capslock+ralt: '\u00ef'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
ralt: '\u00f5'
- ralt+capslock, shift+ralt: '\u00d5'
+ shift+ralt, capslock+ralt: '\u00d5'
+ shift+capslock+ralt: '\u00f5'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: '\u00c5'
base: '\u00e5'
shift, capslock: '\u00c5'
+ shift+capslock: '\u00e5'
}
key RIGHT_BRACKET {
@@ -200,84 +214,104 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
ralt: '\u00e1'
- ralt+capslock, shift+ralt: '\u00c1'
+ shift+ralt, capslock+ralt: '\u00c1'
+ shift+capslock+ralt: '\u00e1'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u0161'
- ralt+capslock, shift+ralt: '\u0160'
+ shift+ralt, capslock+ralt: '\u0160'
+ shift+capslock+ralt: '\u0161'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
ralt: '\u0111'
- ralt+capslock, shift+ralt: '\u0110'
+ shift+ralt, capslock+ralt: '\u0110'
+ shift+capslock+ralt: '\u0111'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
ralt: '\u01e5'
- ralt+capslock, shift+ralt: '\u01e4'
+ shift+ralt, capslock+ralt: '\u01e4'
+ shift+capslock+ralt: '\u01e5'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
ralt: '\u01e7'
- ralt+capslock, shift+ralt: '\u01e6'
+ shift+ralt, capslock+ralt: '\u01e6'
+ shift+capslock+ralt: '\u01e7'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
ralt: '\u021f'
- ralt+capslock, shift+ralt: '\u021e'
+ shift+ralt, capslock+ralt: '\u021e'
+ shift+capslock+ralt: '\u021f'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
ralt: '\u01e9'
- ralt+capslock, shift+ralt: '\u01e8'
+ shift+ralt, capslock+ralt: '\u01e8'
+ shift+capslock+ralt: '\u01e9'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: '\u00d8'
base: '\u00f8'
shift, capslock: '\u00d8'
+ shift+capslock: '\u00f8'
ralt: '\u00f6'
- ralt+capslock, shift+ralt: '\u00d6'
+ shift+ralt, capslock+ralt: '\u00d6'
+ shift+capslock+ralt: '\u00f6'
}
key APOSTROPHE {
label: '\u00c6'
base: '\u00e6'
shift, capslock: '\u00c6'
+ shift+capslock: '\u00e6'
ralt: '\u00e4'
- ralt+capslock, shift+ralt: '\u00c4'
+ shift+ralt, capslock+ralt: '\u00c4'
+ shift+capslock+ralt: '\u00e4'
}
key BACKSLASH {
@@ -298,53 +332,65 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
ralt: '\u017e'
- ralt+capslock, shift+ralt: '\u017d'
+ shift+ralt, capslock+ralt: '\u017d'
+ shift+capslock+ralt: '\u017e'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
ralt: '\u010d'
- ralt+capslock, shift+ralt: '\u010c'
+ shift+ralt, capslock+ralt: '\u010c'
+ shift+capslock+ralt: '\u010d'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
ralt: '\u01ef'
- ralt+capslock, shift+ralt: '\u01ee'
+ shift+ralt, capslock+ralt: '\u01ee'
+ shift+capslock+ralt: '\u01ef'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
ralt: '\u0292'
- ralt+capslock, shift+ralt: '\u01b7'
+ shift+ralt, capslock+ralt: '\u01b7'
+ shift+capslock+ralt: '\u0292'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
ralt: '\u014b'
- ralt+capslock, shift+ralt: '\u014a'
+ shift+ralt, capslock+ralt: '\u014a'
+ shift+capslock+ralt: '\u014b'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
- ralt, ralt+capslock: '\u00b5'
+ shift+capslock: 'm'
+ ralt: '\u00b5'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_persian.kcm b/packages/InputDevices/res/raw/keyboard_layout_persian.kcm
index bfe7821..6744922 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_persian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_persian.kcm
@@ -22,231 +22,222 @@
label: '\u0634'
base: '\u0634'
shift, capslock: '\u0624'
- ctrl, alt, meta: none
+ shift+capslock: '\u0634'
}
key B {
label: '\u0630'
base: '\u0630'
shift, capslock: '\u200C'
- ctrl, alt, meta: none
+ shift+capslock: '\u0630'
}
key C {
label: '\u0632'
base: '\u0632'
shift, capslock: '\u0698'
- ctrl, alt, meta: none
+ shift+capslock: '\u0632'
}
key D {
label: '\u06CC'
base: '\u06CC'
shift, capslock: '\u064A'
- ctrl, alt, meta: none
+ shift+capslock: '\u06CC'
}
key E {
label: '\u062B'
base: '\u062B'
shift, capslock: '\u064D'
- ctrl, alt, meta: none
+ shift+capslock: '\u062B'
}
key F {
label: '\u0628'
base: '\u0628'
shift, capslock: '\u0625'
- ctrl, alt, meta: none
+ shift+capslock: '\u0628'
}
key G {
label: '\u0644'
base: '\u0644'
shift, capslock: '\u0623'
- ctrl, alt, meta: none
+ shift+capslock: '\u0644'
}
key H {
label: '\u0627'
base: '\u0627'
shift, capslock: '\u0622'
- ctrl, alt, meta: none
+ shift+capslock: '\u0627'
}
key I {
label: '\u0647'
base: '\u0647'
shift, capslock: '\u0651'
- ctrl, alt, meta: none
+ shift+capslock: '\u0647'
}
key J {
label: '\u062A'
base: '\u062A'
shift, capslock: '\u0629'
- ctrl, alt, meta: none
+ shift+capslock: '\u062A'
}
key K {
label: '\u0646'
base: '\u0646'
shift, capslock: '\u00AB'
- ctrl, alt, meta: none
+ shift+capslock: '\u0646'
}
key L {
label: '\u0645'
base: '\u0645'
shift, capslock: '\u00BB'
- ctrl, alt, meta: none
+ shift+capslock: '\u0645'
}
key M {
label: '\u067E'
base: '\u067E'
shift, capslock: '\u0621'
- ctrl, alt, meta: none
+ shift+capslock: '\u067E'
}
key N {
label: '\u062F'
base: '\u062F'
shift, capslock: '\u0654'
- ctrl, alt, meta: none
+ shift+capslock: '\u062F'
}
key O {
label: '\u062E'
base: '\u062E'
- shift, capslock: ']'
- ctrl, alt, meta: none
+ shift: ']'
}
key P {
label: '\u062D'
base: '\u062D'
- shift, capslock: '['
- ctrl, alt, meta: none
+ shift: '['
}
key Q {
label: '\u0636'
base: '\u0636'
shift, capslock: '\u0652'
- ctrl, alt, meta: none
+ shift+capslock: '\u0636'
}
key R {
label: '\u0642'
base: '\u0642'
shift, capslock: '\u064B'
- ctrl, alt, meta: none
+ shift+capslock: '\u0642'
}
key S {
label: '\u0633'
base: '\u0633'
shift, capslock: '\u0626'
- ctrl, alt, meta: none
+ shift+capslock: '\u0633'
}
key T {
label: '\u0641'
base: '\u0641'
shift, capslock: '\u064F'
- ctrl, alt, meta: none
+ shift+capslock: '\u0641'
}
key U {
label: '\u0639'
base: '\u0639'
shift, capslock: '\u064E'
- ctrl, alt, meta: none
+ shift+capslock: '\u0639'
}
key V {
label: '\u0631'
base: '\u0631'
shift, capslock: '\u0670'
- ctrl, alt, meta: none
+ shift+capslock: '\u0631'
}
key W {
label: '\u0635'
base: '\u0635'
shift, capslock: '\u064C'
- ctrl, alt, meta: none
+ shift+capslock: '\u0635'
}
key X {
label: '\u0637'
base: '\u0637'
shift, capslock: '\u0653'
- ctrl, alt, meta: none
+ shift+capslock: '\u0637'
}
key Y {
label: '\u063A'
base: '\u063A'
shift, capslock: '\u0650'
- ctrl, alt, meta: none
+ shift+capslock: '\u063A'
}
key Z {
label: '\u0638'
base: '\u0638'
shift, capslock: '\u0643'
- ctrl, alt, meta: none
+ shift+capslock: '\u0638'
}
key 0 {
label, number: '\u06F0'
base: '\u06F0'
shift: '('
- ctrl, alt, meta: none
}
key 1 {
label, number: '\u06F1'
base: '\u06F1'
shift: '!'
- ctrl, alt, meta: none
}
key 2 {
label, number: '\u06F2'
base: '\u06F2'
shift: '\u066C'
- ctrl, alt, meta: none
}
key 3 {
label, number: '\u06F3'
base: '\u06F3'
shift: '\u066B'
- ctrl, alt, meta: none
}
key 4 {
label, number: '\u06F4'
base: '\u06F4'
shift: '\uFDFC'
- ctrl, alt, meta: none
}
key 5 {
label, number: '\u06F5'
base: '\u06F5'
shift: '\u066A'
- ctrl, alt, meta: none
}
key 6 {
label, number: '\u06F6'
base: '\u06F6'
shift: '\u00D7'
- ctrl, alt, meta: none
}
@@ -254,248 +245,82 @@
label, number: '\u06F7'
base: '\u06F7'
shift: '\u060C'
- ctrl, alt, meta: none
}
key 8 {
label, number: '\u06F8'
base: '\u06F8'
shift: '*'
- ctrl, alt, meta: none
}
key 9 {
label, number: '\u06F9'
base: '\u06F9'
shift: ')'
- ctrl, alt, meta: none
-}
-
-key SPACE {
- label: ' '
- base: ' '
- ctrl, alt, meta: none
-}
-
-key ENTER {
- label: '\n'
- base: '\n'
- ctrl, alt, meta: none
-}
-
-key TAB {
- label: '\t'
- base: '\t'
- ctrl, alt, meta: none
}
key COMMA {
label, number: '\u0648'
base: '\u0648'
shift: '>'
- ctrl, alt, meta: none
}
key PERIOD {
label, number: '.'
base: '.'
shift: '<'
- ctrl, alt, meta: none
}
key SLASH {
label, number: '/'
base: '/'
shift: '\u061F'
- ctrl, alt, meta: none
}
key GRAVE {
label, number: '`'
base: '`'
shift: '\u00F7'
- ctrl, alt, meta: none
}
-
key MINUS {
label, number: '-'
base: '-'
shift: '_'
- ctrl, alt, meta: none
}
key EQUALS {
label, number: '='
base: '='
shift: '+'
- ctrl, alt, meta: none
}
key LEFT_BRACKET {
label, number: '\u062C'
base: '\u062C'
shift: '}'
- ctrl, alt, meta: none
}
key RIGHT_BRACKET {
label, number: '\u0686'
base: '\u0686'
shift: '{'
- ctrl, alt, meta: none
}
key BACKSLASH {
label, number: '\\'
base: '\\'
shift: '|'
- ctrl, alt, meta: none
}
key SEMICOLON {
label, number: '\u06A9'
base: '\u06A9'
shift: ':'
- ctrl, alt, meta: none
}
key APOSTROPHE {
label, number: '\''
base: '\''
shift: '\"'
- ctrl, alt, meta: none
-}
-
-### Numeric keypad ###
-
-key NUMPAD_0 {
- label, number: '0'
- base: fallback INSERT
- numlock: '0'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_1 {
- label, number: '1'
- base: fallback MOVE_END
- numlock: '1'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_2 {
- label, number: '2'
- base: fallback DPAD_DOWN
- numlock: '2'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_3 {
- label, number: '3'
- base: fallback PAGE_DOWN
- numlock: '3'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_4 {
- label, number: '4'
- base: fallback DPAD_LEFT
- numlock: '4'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_5 {
- label, number: '5'
- base: fallback DPAD_CENTER
- numlock: '5'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_6 {
- label, number: '6'
- base: fallback DPAD_RIGHT
- numlock: '6'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_7 {
- label, number: '7'
- base: fallback MOVE_HOME
- numlock: '7'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_8 {
- label, number: '8'
- base: fallback DPAD_UP
- numlock: '8'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_9 {
- label, number: '9'
- base: fallback PAGE_UP
- numlock: '9'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_LEFT_PAREN {
- label, number: ')'
- base: ')'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_RIGHT_PAREN {
- label, number: '('
- base: '('
- ctrl, alt, meta: none
-}
-
-key NUMPAD_DIVIDE {
- label, number: '/'
- base: '/'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_MULTIPLY {
- label, number: '*'
- base: '*'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_SUBTRACT {
- label, number: '-'
- base: '-'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_ADD {
- label, number: '+'
- base: '+'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_DOT {
- label, number: '.'
- base: fallback FORWARD_DEL
- numlock: '.'
- ctrl, alt, meta: none
-}
-
-key NUMPAD_COMMA {
- label, number: ','
- base: ','
- ctrl, alt, meta: none
-}
-
-key NUMPAD_EQUALS {
- label, number: '='
- base: '='
- ctrl, alt, meta: none
-}
-
-key NUMPAD_ENTER {
- label: '\n'
- base: '\n' fallback ENTER
- ctrl, alt, meta: none fallback ENTER
-}
+}
\ No newline at end of file
diff --git a/packages/InputDevices/res/raw/keyboard_layout_polish.kcm b/packages/InputDevices/res/raw/keyboard_layout_polish.kcm
index 559ec07..66fbefc1 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_polish.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_polish.kcm
@@ -104,64 +104,76 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u0119'
- ralt+shift, ralt+capslock: '\u0118'
+ shift+ralt, capslock+ralt: '\u0118'
+ shift+capslock+ralt: '\u0119'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
ralt: '\u00F3'
- ralt+shift, ralt+capslock: '\u00D3'
+ shift+ralt, capslock+ralt: '\u00D3'
+ shift+capslock+ralt: '\u00F3'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -188,60 +200,72 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
ralt: '\u0105'
- ralt+shift, ralt+capslock: '\u0104'
+ shift+ralt, capslock+ralt: '\u0104'
+ shift+capslock+ralt: '\u0105'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u015b'
- ralt+shift, ralt+capslock: '\u015a'
+ shift+ralt, capslock+ralt: '\u015a'
+ shift+capslock+ralt: '\u015b'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
ralt: '\u0142'
- ralt+shift, ralt+capslock: '\u0141'
+ shift+ralt, capslock+ralt: '\u0141'
+ shift+capslock+ralt: '\u0142'
}
key SEMICOLON {
@@ -262,50 +286,61 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
ralt: '\u017c'
- ralt+shift, ralt+capslock: '\u017b'
+ shift+ralt, capslock+ralt: '\u017b'
+ shift+capslock+ralt: '\u017c'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
ralt: '\u017a'
- ralt+shift, ralt+capslock: '\u0179'
+ shift+ralt, capslock+ralt: '\u0179'
+ shift+capslock+ralt: '\u017a'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
ralt: '\u0107'
- ralt+shift, ralt+capslock: '\u0106'
+ shift+ralt, capslock+ralt: '\u0106'
+ shift+capslock+ralt: '\u0107'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
ralt: '\u0144'
- ralt+shift, ralt+capslock: '\u0143'
+ shift+ralt, capslock+ralt: '\u0143'
+ shift+capslock+ralt: '\u0144'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_portuguese.kcm b/packages/InputDevices/res/raw/keyboard_layout_portuguese.kcm
index 47ee867..6fe0e47 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_portuguese.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_portuguese.kcm
@@ -115,18 +115,21 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -134,42 +137,49 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -191,60 +201,70 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: '\u00c7'
base: '\u00e7'
shift, capslock: '\u00c7'
+ shift+capslock: '\u00e7'
}
key APOSTROPHE {
@@ -272,42 +292,49 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_russian.kcm b/packages/InputDevices/res/raw/keyboard_layout_russian.kcm
index 41c6bb3..ecada49 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_russian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_russian.kcm
@@ -28,6 +28,7 @@
label: '\u0401'
base: '\u0451'
shift, capslock: '\u0401'
+ shift+capslock: '\u0451'
ralt: '`'
ralt+shift: '~'
}
@@ -124,86 +125,107 @@
label: '\u0419'
base: '\u0439'
shift, capslock: '\u0419'
+ shift+capslock: '\u0439'
ralt: 'q'
- ralt+shift, ralt+capslock: 'Q'
+ shift+ralt, capslock+ralt: 'Q'
+ shift+capslock+ralt: 'q'
}
key W {
label: '\u0426'
base: '\u0446'
shift, capslock: '\u0426'
+ shift+capslock: '\u0446'
ralt: 'w'
- ralt+shift, ralt+capslock: 'W'
+ shift+ralt, capslock+ralt: 'W'
+ shift+capslock+ralt: 'w'
}
key E {
label: '\u0423'
base: '\u0443'
shift, capslock: '\u0423'
+ shift+capslock: '\u0443'
ralt: 'e'
- ralt+shift, ralt+capslock: 'E'
+ shift+ralt, capslock+ralt: 'E'
+ shift+capslock+ralt: 'e'
}
key R {
label: '\u041a'
base: '\u043a'
shift, capslock: '\u041a'
+ shift+capslock: '\u043a'
ralt: 'r'
- ralt+shift, ralt+capslock: 'R'
+ shift+ralt, capslock+ralt: 'R'
+ shift+capslock+ralt: 'r'
}
key T {
label: '\u0415'
base: '\u0435'
shift, capslock: '\u0415'
+ shift+capslock: '\u0435'
ralt: 't'
- ralt+shift, ralt+capslock: 'T'
+ shift+ralt, capslock+ralt: 'T'
+ shift+capslock+ralt: 't'
}
key Y {
label: '\u041d'
base: '\u043d'
shift, capslock: '\u041d'
+ shift+capslock: '\u043d'
ralt: 'y'
- ralt+shift, ralt+capslock: 'Y'
+ shift+ralt, capslock+ralt: 'Y'
+ shift+capslock+ralt: 'y'
}
key U {
label: '\u0413'
base: '\u0433'
shift, capslock: '\u0413'
+ shift+capslock: '\u0433'
ralt: 'u'
- ralt+shift, ralt+capslock: 'U'
+ shift+ralt, capslock+ralt: 'U'
+ shift+capslock+ralt: 'u'
}
key I {
label: '\u0428'
base: '\u0448'
shift, capslock: '\u0428'
+ shift+capslock: '\u0448'
ralt: 'i'
- ralt+shift, ralt+capslock: 'I'
+ shift+ralt, capslock+ralt: 'I'
+ shift+capslock+ralt: 'i'
}
key O {
label: '\u0429'
base: '\u0449'
shift, capslock: '\u0429'
+ shift+capslock: '\u0449'
ralt: 'o'
- ralt+shift, ralt+capslock: 'O'
+ shift+ralt, capslock+ralt: 'O'
+ shift+capslock+ralt: 'o'
}
key P {
label: '\u0417'
base: '\u0437'
shift, capslock: '\u0417'
+ shift+capslock: '\u0437'
ralt: 'p'
- ralt+shift, ralt+capslock: 'P'
+ shift+ralt, capslock+ralt: 'P'
+ shift+capslock+ralt: 'p'
}
key LEFT_BRACKET {
label: '\u0425'
base: '\u0445'
shift, capslock: '\u0425'
+ shift+capslock: '\u0445'
ralt: '['
ralt+shift: '{'
}
@@ -212,6 +234,7 @@
label: '\u042a'
base: '\u044a'
shift, capslock: '\u042a'
+ shift+capslock: '\u044a'
ralt: ']'
ralt+shift: '}'
}
@@ -222,78 +245,97 @@
label: '\u0424'
base: '\u0444'
shift, capslock: '\u0424'
+ shift+capslock: '\u0444'
ralt: 'a'
- ralt+shift, ralt+capslock: 'A'
+ shift+ralt, capslock+ralt: 'A'
+ shift+capslock+ralt: 'a'
}
key S {
label: '\u042b'
base: '\u044b'
shift, capslock: '\u042b'
+ shift+capslock: '\u044b'
ralt: 's'
- ralt+shift, ralt+capslock: 'S'
+ shift+ralt, capslock+ralt: 'S'
+ shift+capslock+ralt: 's'
}
key D {
label: '\u0412'
base: '\u0432'
shift, capslock: '\u0412'
+ shift+capslock: '\u0432'
ralt: 'd'
- ralt+shift, ralt+capslock: 'D'
+ shift+ralt, capslock+ralt: 'D'
+ shift+capslock+ralt: 'd'
}
key F {
label: '\u0410'
base: '\u0430'
shift, capslock: '\u0410'
+ shift+capslock: '\u0430'
ralt: 'f'
- ralt+shift, ralt+capslock: 'F'
+ shift+ralt, capslock+ralt: 'F'
+ shift+capslock+ralt: 'f'
}
key G {
label: '\u041f'
base: '\u043f'
shift, capslock: '\u041f'
+ shift+capslock: '\u043f'
ralt: 'g'
- ralt+shift, ralt+capslock: 'G'
+ shift+ralt, capslock+ralt: 'G'
+ shift+capslock+ralt: 'g'
}
key H {
label: '\u0420'
base: '\u0440'
shift, capslock: '\u0420'
+ shift+capslock: '\u0440'
ralt: 'h'
- ralt+shift, ralt+capslock: 'H'
+ shift+ralt, capslock+ralt: 'H'
+ shift+capslock+ralt: 'h'
}
key J {
label: '\u041e'
base: '\u043e'
shift, capslock: '\u041e'
+ shift+capslock: '\u043e'
ralt: 'j'
- ralt+shift, ralt+capslock: 'J'
+ shift+ralt, capslock+ralt: 'J'
+ shift+capslock+ralt: 'j'
}
key K {
label: '\u041b'
base: '\u043b'
shift, capslock: '\u041b'
+ shift+capslock: '\u043b'
ralt: 'k'
- ralt+shift, ralt+capslock: 'K'
+ shift+ralt, capslock+ralt: 'K'
+ shift+capslock+ralt: 'k'
}
key L {
label: '\u0414'
base: '\u0434'
shift, capslock: '\u0414'
+ shift+capslock: '\u0434'
ralt: 'l'
- ralt+shift, ralt+capslock: 'L'
+ shift+ralt, capslock+ralt: 'L'
+ shift+capslock+ralt: 'l'
}
key SEMICOLON {
label: '\u0416'
base: '\u0436'
shift, capslock: '\u0416'
+ shift+capslock: '\u0436'
ralt: ';'
ralt+shift: ':'
}
@@ -302,6 +344,7 @@
label: '\u042d'
base: '\u044d'
shift, capslock: '\u042d'
+ shift+capslock: '\u044d'
ralt: '\''
ralt+shift: '"'
}
@@ -319,62 +362,77 @@
label: '\u042f'
base: '\u044f'
shift, capslock: '\u042f'
+ shift+capslock: '\u044f'
ralt: 'z'
- ralt+shift, ralt+capslock: 'Z'
+ shift+ralt, capslock+ralt: 'Z'
+ shift+capslock+ralt: 'z'
}
key X {
label: '\u0427'
base: '\u0447'
shift, capslock: '\u0427'
+ shift+capslock: '\u0447'
ralt: 'x'
- ralt+shift, ralt+capslock: 'X'
+ shift+ralt, capslock+ralt: 'X'
+ shift+capslock+ralt: 'x'
}
key C {
label: '\u0421'
base: '\u0441'
shift, capslock: '\u0421'
+ shift+capslock: '\u0441'
ralt: 'c'
- ralt+shift, ralt+capslock: 'C'
+ shift+ralt, capslock+ralt: 'C'
+ shift+capslock+ralt: 'c'
}
key V {
label: '\u041c'
base: '\u043c'
shift, capslock: '\u041c'
+ shift+capslock: '\u043c'
ralt: 'v'
- ralt+shift, ralt+capslock: 'V'
+ shift+ralt, capslock+ralt: 'V'
+ shift+capslock+ralt: 'v'
}
key B {
label: '\u0418'
base: '\u0438'
shift, capslock: '\u0418'
+ shift+capslock: '\u0438'
ralt: 'b'
- ralt+shift, ralt+capslock: 'B'
+ shift+ralt, capslock+ralt: 'B'
+ shift+capslock+ralt: 'b'
}
key N {
label: '\u0422'
base: '\u0442'
shift, capslock: '\u0422'
+ shift+capslock: '\u0442'
ralt: 'n'
- ralt+shift, ralt+capslock: 'N'
+ shift+ralt, capslock+ralt: 'N'
+ shift+capslock+ralt: 'n'
}
key M {
label: '\u042c'
base: '\u044c'
shift, capslock: '\u042c'
+ shift+capslock: '\u044c'
ralt: 'm'
- ralt+shift, ralt+capslock: 'M'
+ shift+ralt, capslock+ralt: 'M'
+ shift+capslock+ralt: 'm'
}
key COMMA {
label: '\u0411'
base: '\u0431'
shift, capslock: '\u0411'
+ shift+capslock: '\u0431'
ralt: ','
ralt+shift: '<'
}
@@ -383,6 +441,7 @@
label: '\u042e'
base: '\u044e'
shift, capslock: '\u042e'
+ shift+capslock: '\u044e'
ralt: '.'
ralt+shift: '>'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_russian_mac.kcm b/packages/InputDevices/res/raw/keyboard_layout_russian_mac.kcm
index 11c2ad4..5417bc3 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_russian_mac.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_russian_mac.kcm
@@ -126,86 +126,107 @@
label: '\u0419'
base: '\u0439'
shift, capslock: '\u0419'
+ shift+capslock: '\u0439'
ralt: 'q'
- ralt+shift, ralt+capslock: 'Q'
+ shift+ralt, capslock+ralt: 'Q'
+ shift+capslock+ralt: 'q'
}
key W {
label: '\u0426'
base: '\u0446'
shift, capslock: '\u0426'
+ shift+capslock: '\u0446'
ralt: 'w'
- ralt+shift, ralt+capslock: 'W'
+ shift+ralt, capslock+ralt: 'W'
+ shift+capslock+ralt: 'w'
}
key E {
label: '\u0423'
base: '\u0443'
shift, capslock: '\u0423'
+ shift+capslock: '\u0443'
ralt: 'e'
- ralt+shift, ralt+capslock: 'E'
+ shift+ralt, capslock+ralt: 'E'
+ shift+capslock+ralt: 'e'
}
key R {
label: '\u041a'
base: '\u043a'
shift, capslock: '\u041a'
+ shift+capslock: '\u043a'
ralt: 'r'
- ralt+shift, ralt+capslock: 'R'
+ shift+ralt, capslock+ralt: 'R'
+ shift+capslock+ralt: 'r'
}
key T {
label: '\u0415'
base: '\u0435'
shift, capslock: '\u0415'
+ shift+capslock: '\u0435'
ralt: 't'
- ralt+shift, ralt+capslock: 'T'
+ shift+ralt, capslock+ralt: 'T'
+ shift+capslock+ralt: 't'
}
key Y {
label: '\u041d'
base: '\u043d'
shift, capslock: '\u041d'
+ shift+capslock: '\u043d'
ralt: 'y'
- ralt+shift, ralt+capslock: 'Y'
+ shift+ralt, capslock+ralt: 'Y'
+ shift+capslock+ralt: 'y'
}
key U {
label: '\u0413'
base: '\u0433'
shift, capslock: '\u0413'
+ shift+capslock: '\u0433'
ralt: 'u'
- ralt+shift, ralt+capslock: 'U'
+ shift+ralt, capslock+ralt: 'U'
+ shift+capslock+ralt: 'u'
}
key I {
label: '\u0428'
base: '\u0448'
shift, capslock: '\u0428'
+ shift+capslock: '\u0448'
ralt: 'i'
- ralt+shift, ralt+capslock: 'I'
+ shift+ralt, capslock+ralt: 'I'
+ shift+capslock+ralt: 'i'
}
key O {
label: '\u0429'
base: '\u0449'
shift, capslock: '\u0429'
+ shift+capslock: '\u0449'
ralt: 'o'
- ralt+shift, ralt+capslock: 'O'
+ shift+ralt, capslock+ralt: 'O'
+ shift+capslock+ralt: 'o'
}
key P {
label: '\u0417'
base: '\u0437'
shift, capslock: '\u0417'
+ shift+capslock: '\u0437'
ralt: 'p'
- ralt+shift, ralt+capslock: 'P'
+ shift+ralt, capslock+ralt: 'P'
+ shift+capslock+ralt: 'p'
}
key LEFT_BRACKET {
label: '\u0425'
base: '\u0445'
shift, capslock: '\u0425'
+ shift+capslock: '\u0445'
ralt: '['
ralt+shift: '{'
}
@@ -214,6 +235,7 @@
label: '\u042a'
base: '\u044a'
shift, capslock: '\u042a'
+ shift+capslock: '\u044a'
ralt: ']'
ralt+shift: '}'
}
@@ -224,78 +246,97 @@
label: '\u0424'
base: '\u0444'
shift, capslock: '\u0424'
+ shift+capslock: '\u0444'
ralt: 'a'
- ralt+shift, ralt+capslock: 'A'
+ shift+ralt, capslock+ralt: 'A'
+ shift+capslock+ralt: 'a'
}
key S {
label: '\u042b'
base: '\u044b'
shift, capslock: '\u042b'
+ shift+capslock: '\u044b'
ralt: 's'
- ralt+shift, ralt+capslock: 'S'
+ shift+ralt, capslock+ralt: 'S'
+ shift+capslock+ralt: 's'
}
key D {
label: '\u0412'
base: '\u0432'
shift, capslock: '\u0412'
+ shift+capslock: '\u0432'
ralt: 'd'
- ralt+shift, ralt+capslock: 'D'
+ shift+ralt, capslock+ralt: 'D'
+ shift+capslock+ralt: 'd'
}
key F {
label: '\u0410'
base: '\u0430'
shift, capslock: '\u0410'
+ shift+capslock: '\u0430'
ralt: 'f'
- ralt+shift, ralt+capslock: 'F'
+ shift+ralt, capslock+ralt: 'F'
+ shift+capslock+ralt: 'f'
}
key G {
label: '\u041f'
base: '\u043f'
shift, capslock: '\u041f'
+ shift+capslock: '\u043f'
ralt: 'g'
- ralt+shift, ralt+capslock: 'G'
+ shift+ralt, capslock+ralt: 'G'
+ shift+capslock+ralt: 'g'
}
key H {
label: '\u0420'
base: '\u0440'
shift, capslock: '\u0420'
+ shift+capslock: '\u0440'
ralt: 'h'
- ralt+shift, ralt+capslock: 'H'
+ shift+ralt, capslock+ralt: 'H'
+ shift+capslock+ralt: 'h'
}
key J {
label: '\u041e'
base: '\u043e'
shift, capslock: '\u041e'
+ shift+capslock: '\u043e'
ralt: 'j'
- ralt+shift, ralt+capslock: 'J'
+ shift+ralt, capslock+ralt: 'J'
+ shift+capslock+ralt: 'j'
}
key K {
label: '\u041b'
base: '\u043b'
shift, capslock: '\u041b'
+ shift+capslock: '\u043b'
ralt: 'k'
- ralt+shift, ralt+capslock: 'K'
+ shift+ralt, capslock+ralt: 'K'
+ shift+capslock+ralt: 'k'
}
key L {
label: '\u0414'
base: '\u0434'
shift, capslock: '\u0414'
+ shift+capslock: '\u0434'
ralt: 'l'
- ralt+shift, ralt+capslock: 'L'
+ shift+ralt, capslock+ralt: 'L'
+ shift+capslock+ralt: 'l'
}
key SEMICOLON {
label: '\u0416'
base: '\u0436'
shift, capslock: '\u0416'
+ shift+capslock: '\u0436'
ralt: ';'
ralt+shift: ':'
}
@@ -304,6 +345,7 @@
label: '\u042d'
base: '\u044d'
shift, capslock: '\u042d'
+ shift+capslock: '\u044d'
ralt: '\''
ralt+shift: '"'
}
@@ -312,6 +354,7 @@
label: '\u0401'
base: '\u0451'
shift, capslock: '\u0401'
+ shift+capslock: '\u0451'
ralt: '\\'
ralt+shift: '|'
}
@@ -330,62 +373,77 @@
label: '\u042f'
base: '\u044f'
shift, capslock: '\u042f'
+ shift+capslock: '\u044f'
ralt: 'z'
- ralt+shift, ralt+capslock: 'Z'
+ shift+ralt, capslock+ralt: 'Z'
+ shift+capslock+ralt: 'z'
}
key X {
label: '\u0427'
base: '\u0447'
shift, capslock: '\u0427'
+ shift+capslock: '\u0447'
ralt: 'x'
- ralt+shift, ralt+capslock: 'X'
+ shift+ralt, capslock+ralt: 'X'
+ shift+capslock+ralt: 'x'
}
key C {
label: '\u0421'
base: '\u0441'
shift, capslock: '\u0421'
+ shift+capslock: '\u0441'
ralt: 'c'
- ralt+shift, ralt+capslock: 'C'
+ shift+ralt, capslock+ralt: 'C'
+ shift+capslock+ralt: 'c'
}
key V {
label: '\u041c'
base: '\u043c'
shift, capslock: '\u041c'
+ shift+capslock: '\u043c'
ralt: 'v'
- ralt+shift, ralt+capslock: 'V'
+ shift+ralt, capslock+ralt: 'V'
+ shift+capslock+ralt: 'v'
}
key B {
label: '\u0418'
base: '\u0438'
shift, capslock: '\u0418'
+ shift+capslock: '\u0438'
ralt: 'b'
- ralt+shift, ralt+capslock: 'B'
+ shift+ralt, capslock+ralt: 'B'
+ shift+capslock+ralt: 'b'
}
key N {
label: '\u0422'
base: '\u0442'
shift, capslock: '\u0422'
+ shift+capslock: '\u0442'
ralt: 'n'
- ralt+shift, ralt+capslock: 'N'
+ shift+ralt, capslock+ralt: 'N'
+ shift+capslock+ralt: 'n'
}
key M {
label: '\u042c'
base: '\u044c'
shift, capslock: '\u042c'
+ shift+capslock: '\u044c'
ralt: 'm'
- ralt+shift, ralt+capslock: 'M'
+ shift+ralt, capslock+ralt: 'M'
+ shift+capslock+ralt: 'm'
}
key COMMA {
label: '\u0411'
base: '\u0431'
shift, capslock: '\u0411'
+ shift+capslock: '\u0431'
ralt: ','
ralt+shift: '<'
}
@@ -394,6 +452,7 @@
label: '\u042e'
base: '\u044e'
shift, capslock: '\u042e'
+ shift+capslock: '\u044e'
ralt: '.'
ralt+shift: '>'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_slovak.kcm b/packages/InputDevices/res/raw/keyboard_layout_slovak.kcm
index 2eb0f63..5065aa8 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_slovak.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_slovak.kcm
@@ -118,6 +118,7 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '\\'
}
@@ -125,6 +126,7 @@
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
ralt: '|'
}
@@ -132,6 +134,7 @@
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -139,42 +142,49 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
ralt: '\''
}
@@ -198,12 +208,14 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u0111'
}
@@ -211,6 +223,7 @@
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
ralt: '\u0110'
}
@@ -218,6 +231,7 @@
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
ralt: '['
}
@@ -225,6 +239,7 @@
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
ralt: ']'
}
@@ -232,18 +247,21 @@
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
ralt: '\u0142'
}
@@ -251,6 +269,7 @@
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
ralt: '\u0141'
}
@@ -288,6 +307,7 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
ralt: '>'
}
@@ -295,6 +315,7 @@
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
ralt: '#'
}
@@ -302,6 +323,7 @@
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
ralt: '&'
}
@@ -309,6 +331,7 @@
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
ralt: '@'
}
@@ -316,6 +339,7 @@
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
ralt: '{'
}
@@ -323,6 +347,7 @@
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
ralt: '}'
}
@@ -330,6 +355,7 @@
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_spanish.kcm b/packages/InputDevices/res/raw/keyboard_layout_spanish.kcm
index da9159b..6a63e70 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_spanish.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_spanish.kcm
@@ -113,18 +113,21 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -132,42 +135,49 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -190,60 +200,70 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: '\u00d1'
base: '\u00f1'
shift, capslock: '\u00d1'
+ shift+capslock: '\u00f1'
}
key APOSTROPHE {
@@ -257,6 +277,7 @@
label: '\u00c7'
base: '\u00e7'
shift, capslock: '\u00c7'
+ shift+capslock: '\u00e7'
ralt: '}'
}
@@ -272,42 +293,49 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_spanish_latin.kcm b/packages/InputDevices/res/raw/keyboard_layout_spanish_latin.kcm
index 16eb53f..29aab97 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_spanish_latin.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_spanish_latin.kcm
@@ -109,6 +109,7 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '@'
}
@@ -116,54 +117,63 @@
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -186,60 +196,70 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: '\u00d1'
base: '\u00f1'
shift, capslock: '\u00d1'
+ shift+capslock: '\u00f1'
}
key APOSTROPHE {
@@ -268,42 +288,49 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_swedish.kcm b/packages/InputDevices/res/raw/keyboard_layout_swedish.kcm
index 8a4e9a5..f12804f 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_swedish.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_swedish.kcm
@@ -115,76 +115,90 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '\u00e2'
- ralt+capslock, shift+ralt: '\u00c2'
+ shift+ralt, capslock+ralt: '\u00c2'
+ shift+capslock+ralt: '\u00e2'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
- ralt+capslock: '\u20ac'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
ralt: '\u0167'
- ralt+capslock, shift+ralt: '\u0166'
+ shift+ralt, capslock+ralt: '\u0166'
+ shift+capslock+ralt: '\u0167'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
ralt: '\u00ef'
- ralt+capslock, shift+ralt: '\u00cf'
+ shift+ralt, capslock+ralt: '\u00cf'
+ shift+capslock+ralt: '\u00ef'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
ralt: '\u00f5'
- ralt+capslock, shift+ralt: '\u00d5'
+ shift+ralt, capslock+ralt: '\u00d5'
+ shift+capslock+ralt: '\u00f5'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: '\u00c5'
base: '\u00e5'
shift, capslock: '\u00c5'
+ shift+capslock: '\u00e5'
}
key RIGHT_BRACKET {
@@ -200,84 +214,104 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
ralt: '\u00e1'
- ralt+capslock, shift+ralt: '\u00c1'
+ shift+ralt, capslock+ralt: '\u00c1'
+ shift+capslock+ralt: '\u00e1'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u0161'
- ralt+capslock, shift+ralt: '\u0160'
+ shift+ralt, capslock+ralt: '\u0160'
+ shift+capslock+ralt: '\u0161'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
ralt: '\u0111'
- ralt+capslock, shift+ralt: '\u0110'
+ shift+ralt, capslock+ralt: '\u0110'
+ shift+capslock+ralt: '\u0111'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
ralt: '\u01e5'
- ralt+capslock, shift+ralt: '\u01e4'
+ shift+ralt, capslock+ralt: '\u01e4'
+ shift+capslock+ralt: '\u01e5'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
ralt: '\u01e7'
- ralt+capslock, shift+ralt: '\u01e6'
+ shift+ralt, capslock+ralt: '\u01e6'
+ shift+capslock+ralt: '\u01e7'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
ralt: '\u021f'
- ralt+capslock, shift+ralt: '\u021e'
+ shift+ralt, capslock+ralt: '\u021e'
+ shift+capslock+ralt: '\u021f'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
ralt: '\u01e9'
- ralt+capslock, shift+ralt: '\u01e8'
+ shift+ralt, capslock+ralt: '\u01e8'
+ shift+capslock+ralt: '\u01e9'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: '\u00d6'
base: '\u00f6'
shift, capslock: '\u00d6'
+ shift+capslock: '\u00f6'
ralt: '\u00f8'
- ralt+capslock, shift+ralt: '\u00d8'
+ shift+ralt, capslock+ralt: '\u00d8'
+ shift+capslock+ralt: '\u00f8'
}
key APOSTROPHE {
label: '\u00c4'
base: '\u00e4'
shift, capslock: '\u00c4'
+ shift+capslock: '\u00e4'
ralt: '\u00e6'
- ralt+capslock, shift+ralt: '\u00c6'
+ shift+ralt, capslock+ralt: '\u00c6'
+ shift+capslock+ralt: '\u00e6'
}
key BACKSLASH {
@@ -299,53 +333,65 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
ralt: '\u017e'
- ralt+capslock, shift+ralt: '\u017d'
+ shift+ralt, capslock+ralt: '\u017d'
+ shift+capslock+ralt: '\u017e'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
ralt: '\u010d'
- ralt+capslock, shift+ralt: '\u010c'
+ shift+ralt, capslock+ralt: '\u010c'
+ shift+capslock+ralt: '\u010d'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
ralt: '\u01ef'
- ralt+capslock, shift+ralt: '\u01ee'
+ shift+ralt, capslock+ralt: '\u01ee'
+ shift+capslock+ralt: '\u01ef'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
ralt: '\u0292'
- ralt+capslock, shift+ralt: '\u01b7'
+ shift+ralt, capslock+ralt: '\u01b7'
+ shift+capslock+ralt: '\u0292'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
ralt: '\u014b'
- ralt+capslock, shift+ralt: '\u014a'
+ shift+ralt, capslock+ralt: '\u014a'
+ shift+capslock+ralt: '\u014b'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
- ralt, ralt+capslock: '\u00b5'
+ shift+capslock: 'm'
+ ralt: '\u00b5'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_swiss_french.kcm b/packages/InputDevices/res/raw/keyboard_layout_swiss_french.kcm
index 9e20462..6476793 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_swiss_french.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_swiss_french.kcm
@@ -119,18 +119,21 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -138,42 +141,49 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Z {
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -196,54 +206,63 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
@@ -279,42 +298,49 @@
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_swiss_german.kcm b/packages/InputDevices/res/raw/keyboard_layout_swiss_german.kcm
index 7fbd1a9..9d6f367 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_swiss_german.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_swiss_german.kcm
@@ -119,18 +119,21 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -138,42 +141,49 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Z {
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
@@ -198,54 +208,63 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
@@ -285,42 +304,49 @@
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_turkish.kcm b/packages/InputDevices/res/raw/keyboard_layout_turkish.kcm
index e193d34..2a8fcef 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_turkish.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_turkish.kcm
@@ -124,6 +124,7 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '@'
}
@@ -131,12 +132,14 @@
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -144,50 +147,59 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
base: '\u0131'
shift, capslock: 'I'
+ shift+capslock: 'i'
ralt: 'i'
- ralt+shift, ralt+capslock: '\u0130'
+ shift+ralt, capslock+ralt: '\u0130'
+ shift+capslock+ralt: 'i'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: '\u011e'
base: '\u011f'
shift, capslock: '\u011e'
+ shift+capslock: '\u011f'
ralt: '\u0308'
}
@@ -195,6 +207,7 @@
label: '\u00dc'
base: '\u00fc'
shift, capslock: '\u00dc'
+ shift+capslock: '\u00fc'
ralt: '\u0303'
}
@@ -204,14 +217,17 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
ralt: '\u00e6'
- ralt+shift, ralt+capslock: '\u00c6'
+ shift+ralt, capslock+ralt: '\u00c6'
+ shift+capslock+ralt: '\u00e6'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u00df'
}
@@ -219,48 +235,56 @@
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: '\u015e'
base: '\u015f'
shift, capslock: '\u015e'
+ shift+capslock: '\u015f'
ralt: '\u0301'
}
@@ -268,6 +292,7 @@
label: '\u0130'
base: 'i'
shift, capslock: '\u0130'
+ shift+capslock: 'i'
}
key COMMA {
@@ -290,54 +315,63 @@
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key EQUALS {
label: '\u00d6'
base: '\u00f6'
shift, capslock: '\u00d6'
+ shift+capslock: '\u00f6'
}
key BACKSLASH {
label: '\u00c7'
base: '\u00e7'
shift, capslock: '\u00c7'
+ shift+capslock: '\u00e7'
}
key PERIOD {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_turkish_f.kcm b/packages/InputDevices/res/raw/keyboard_layout_turkish_f.kcm
index 5b96da0..b27f6fa 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_turkish_f.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_turkish_f.kcm
@@ -125,6 +125,7 @@
label: 'F'
base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
ralt: '@'
}
@@ -132,32 +133,38 @@
label: 'G'
base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key E {
label: '\u011f'
base: '\u011f'
shift, capslock: '\u011e'
+ shift+capslock: '\u011f'
}
key R {
label: '\u0131'
base: '\u0131'
shift, capslock: 'I'
+ shift+capslock: 'i'
ralt: '\u00b6'
- ralt+shift, ralt+capslock: '\u00ae'
+ shift+ralt, capslock+ralt: '\u00ae'
+ shift+capslock+ralt: '\u00b6'
}
key T {
label: 'O'
base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key Y {
label: 'D'
base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
ralt: '\u00a5'
}
@@ -165,26 +172,31 @@
label: 'R'
base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key I {
label: 'N'
base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key O {
label: 'H'
base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
ralt: '\u00f8'
- ralt+shift, ralt+capslock: '\u00d8'
+ shift+ralt, capslock+ralt: '\u00d8'
+ shift+capslock+ralt: '\u00f8'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
ralt: '\u00a3'
}
@@ -192,6 +204,7 @@
label: 'Q'
base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '"'
}
@@ -199,6 +212,7 @@
label: 'W'
base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
ralt: '~'
}
@@ -208,22 +222,27 @@
label: '\u0075'
base: '\u0075'
shift, capslock: '\u0055'
+ shift+capslock: '\u0075'
ralt: '\u00e6'
- ralt+shift, ralt+capslock: '\u00c6'
+ shift+ralt, capslock+ralt: '\u00c6'
+ shift+capslock+ralt: '\u00e6'
}
key S {
label: 'i'
base: 'i'
shift, capslock: '\u0130'
+ shift+capslock: 'i'
ralt: '\u00df'
- ralt+shift, ralt+capslock: '\u00a7'
+ shift+ralt, capslock+ralt: '\u00a7'
+ shift+capslock+ralt: '\u00df'
}
key D {
label: 'E'
base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
@@ -231,6 +250,7 @@
label: 'A'
base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
ralt: '\u00aa'
}
@@ -238,12 +258,14 @@
label: '\u00fc'
base: '\u00fc'
shift, capslock: '\u00dc'
+ shift+capslock: '\u00fc'
}
key H {
label: 'T'
base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
ralt: '\u20ba'
}
@@ -251,24 +273,28 @@
label: 'K'
base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key K {
label: 'M'
base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
}
key SEMICOLON {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
ralt: '\u00b4'
}
@@ -276,6 +302,7 @@
label: '\u015f'
base: '\u015f'
shift, capslock: '\u015e'
+ shift+capslock: '\u015f'
}
key COMMA {
@@ -292,63 +319,76 @@
base: '<'
shift: '>'
ralt: '|'
- ralt+shift, ralt+capslock: '\u00a6'
+ shift+ralt, capslock+ralt: '\u00a6'
+ shift+capslock+ralt: '|'
}
key Z {
label: 'J'
base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
ralt: '\u00ab'
- ralt+shift, ralt+capslock: '<'
+ shift+ralt, capslock+ralt: '<'
+ shift+capslock+ralt: '\u00ab'
}
key X {
label: '\u00f6'
base: '\u00f6'
shift, capslock: '\u00d6'
+ shift+capslock: '\u00f6'
ralt: '\u00bb'
- ralt+shift, ralt+capslock: '>'
+ shift+ralt, capslock+ralt: '>'
+ shift+capslock+ralt: '\u00bb'
}
key C {
label: 'V'
base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
ralt: '\u00a2'
- ralt+shift, ralt+capslock: '\u00a9'
+ shift+ralt, capslock+ralt: '\u00a9'
+ shift+capslock+ralt: '\u00a2'
}
key V {
label: 'C'
base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key B {
label: '\u00e7'
base: '\u00e7'
shift, capslock: '\u00c7'
+ shift+capslock: '\u00e7'
}
key N {
label: 'Z'
base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key M {
label: 'S'
base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u00b5'
- ralt+shift, ralt+capslock: '\u00ba'
+ shift+ralt, capslock+ralt: '\u00ba'
+ shift+capslock+ralt: '\u00b5'
}
key EQUALS {
label: 'B'
base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
ralt: '\u00d7'
}
@@ -356,6 +396,7 @@
label: '.'
base: '.'
shift, capslock: ':'
+ shift+capslock: ':'
ralt: '\u00f7'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_ukrainian.kcm b/packages/InputDevices/res/raw/keyboard_layout_ukrainian.kcm
index a802460..1346bbb 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_ukrainian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_ukrainian.kcm
@@ -28,6 +28,7 @@
label: '\u0401'
base: '\u0451'
shift, capslock: '\u0401'
+ shift+capslock: '\u0451'
ralt: '`'
ralt+shift: '~'
}
@@ -124,86 +125,107 @@
label: '\u0419'
base: '\u0439'
shift, capslock: '\u0419'
+ shift+capslock: '\u0439'
ralt: 'q'
- ralt+shift, ralt+capslock: 'Q'
+ shift+ralt, capslock+ralt: 'Q'
+ shift+capslock+ralt: 'q'
}
key W {
label: '\u0426'
base: '\u0446'
shift, capslock: '\u0426'
+ shift+capslock: '\u0446'
ralt: 'w'
- ralt+shift, ralt+capslock: 'W'
+ shift+ralt, capslock+ralt: 'W'
+ shift+capslock+ralt: 'w'
}
key E {
label: '\u0423'
base: '\u0443'
shift, capslock: '\u0423'
+ shift+capslock: '\u0443'
ralt: 'e'
- ralt+shift, ralt+capslock: 'E'
+ shift+ralt, capslock+ralt: 'E'
+ shift+capslock+ralt: 'e'
}
key R {
label: '\u041a'
base: '\u043a'
shift, capslock: '\u041a'
+ shift+capslock: '\u043a'
ralt: 'r'
- ralt+shift, ralt+capslock: 'R'
+ shift+ralt, capslock+ralt: 'R'
+ shift+capslock+ralt: 'r'
}
key T {
label: '\u0415'
base: '\u0435'
shift, capslock: '\u0415'
+ shift+capslock: '\u0435'
ralt: 't'
- ralt+shift, ralt+capslock: 'T'
+ shift+ralt, capslock+ralt: 'T'
+ shift+capslock+ralt: 't'
}
key Y {
label: '\u041d'
base: '\u043d'
shift, capslock: '\u041d'
+ shift+capslock: '\u043d'
ralt: 'y'
- ralt+shift, ralt+capslock: 'Y'
+ shift+ralt, capslock+ralt: 'Y'
+ shift+capslock+ralt: 'y'
}
key U {
label: '\u0413'
base: '\u0433'
shift, capslock: '\u0413'
+ shift+capslock: '\u0433'
ralt: 'u'
- ralt+shift, ralt+capslock: 'U'
+ shift+ralt, capslock+ralt: 'U'
+ shift+capslock+ralt: 'u'
}
key I {
label: '\u0428'
base: '\u0448'
shift, capslock: '\u0428'
+ shift+capslock: '\u0448'
ralt: 'i'
- ralt+shift, ralt+capslock: 'I'
+ shift+ralt, capslock+ralt: 'I'
+ shift+capslock+ralt: 'i'
}
key O {
label: '\u0429'
base: '\u0449'
shift, capslock: '\u0429'
+ shift+capslock: '\u0449'
ralt: 'o'
- ralt+shift, ralt+capslock: 'O'
+ shift+ralt, capslock+ralt: 'O'
+ shift+capslock+ralt: 'o'
}
key P {
label: '\u0417'
base: '\u0437'
shift, capslock: '\u0417'
+ shift+capslock: '\u0437'
ralt: 'p'
- ralt+shift, ralt+capslock: 'P'
+ shift+ralt, capslock+ralt: 'P'
+ shift+capslock+ralt: 'p'
}
key LEFT_BRACKET {
label: '\u0425'
base: '\u0445'
shift, capslock: '\u0425'
+ shift+capslock: '\u0445'
ralt: '['
ralt+shift: '{'
}
@@ -212,6 +234,7 @@
label: '\u0407'
base: '\u0457'
shift, capslock: '\u0407'
+ shift+capslock: '\u0457'
ralt: ']'
ralt+shift: '}'
}
@@ -222,78 +245,97 @@
label: '\u0424'
base: '\u0444'
shift, capslock: '\u0424'
+ shift+capslock: '\u0444'
ralt: 'a'
- ralt+shift, ralt+capslock: 'A'
+ shift+ralt, capslock+ralt: 'A'
+ shift+capslock+ralt: 'a'
}
key S {
label: '\u0406'
base: '\u0456'
shift, capslock: '\u0406'
+ shift+capslock: '\u0456'
ralt: 's'
- ralt+shift, ralt+capslock: 'S'
+ shift+ralt, capslock+ralt: 'S'
+ shift+capslock+ralt: 's'
}
key D {
label: '\u0412'
base: '\u0432'
shift, capslock: '\u0412'
+ shift+capslock: '\u0432'
ralt: 'd'
- ralt+shift, ralt+capslock: 'D'
+ shift+ralt, capslock+ralt: 'D'
+ shift+capslock+ralt: 'd'
}
key F {
label: '\u0410'
base: '\u0430'
shift, capslock: '\u0410'
+ shift+capslock: '\u0430'
ralt: 'f'
- ralt+shift, ralt+capslock: 'F'
+ shift+ralt, capslock+ralt: 'F'
+ shift+capslock+ralt: 'f'
}
key G {
label: '\u041f'
base: '\u043f'
shift, capslock: '\u041f'
+ shift+capslock: '\u043f'
ralt: 'g'
- ralt+shift, ralt+capslock: 'G'
+ shift+ralt, capslock+ralt: 'G'
+ shift+capslock+ralt: 'g'
}
key H {
label: '\u0420'
base: '\u0440'
shift, capslock: '\u0420'
+ shift+capslock: '\u0440'
ralt: 'h'
- ralt+shift, ralt+capslock: 'H'
+ shift+ralt, capslock+ralt: 'H'
+ shift+capslock+ralt: 'h'
}
key J {
label: '\u041e'
base: '\u043e'
shift, capslock: '\u041e'
+ shift+capslock: '\u043e'
ralt: 'j'
- ralt+shift, ralt+capslock: 'J'
+ shift+ralt, capslock+ralt: 'J'
+ shift+capslock+ralt: 'j'
}
key K {
label: '\u041b'
base: '\u043b'
shift, capslock: '\u041b'
+ shift+capslock: '\u043b'
ralt: 'k'
- ralt+shift, ralt+capslock: 'K'
+ shift+ralt, capslock+ralt: 'K'
+ shift+capslock+ralt: 'k'
}
key L {
label: '\u0414'
base: '\u0434'
shift, capslock: '\u0414'
+ shift+capslock: '\u0434'
ralt: 'l'
- ralt+shift, ralt+capslock: 'L'
+ shift+ralt, capslock+ralt: 'L'
+ shift+capslock+ralt: 'l'
}
key SEMICOLON {
label: '\u0416'
base: '\u0436'
shift, capslock: '\u0416'
+ shift+capslock: '\u0436'
ralt: ';'
ralt+shift: ':'
}
@@ -302,6 +344,7 @@
label: '\u0404'
base: '\u0454'
shift, capslock: '\u0404'
+ shift+capslock: '\u0454'
ralt: '\''
ralt+shift: '"'
}
@@ -319,6 +362,7 @@
label: '\u0490'
base: '\u0491'
shift, capslock: '\u0490'
+ shift+capslock: '\u0491'
ralt: '\\'
ralt+shift: '|'
}
@@ -327,62 +371,77 @@
label: '\u042f'
base: '\u044f'
shift, capslock: '\u042f'
+ shift+capslock: '\u044f'
ralt: 'z'
- ralt+shift, ralt+capslock: 'Z'
+ shift+ralt, capslock+ralt: 'Z'
+ shift+capslock+ralt: 'z'
}
key X {
label: '\u0427'
base: '\u0447'
shift, capslock: '\u0427'
+ shift+capslock: '\u0447'
ralt: 'x'
- ralt+shift, ralt+capslock: 'X'
+ shift+ralt, capslock+ralt: 'X'
+ shift+capslock+ralt: 'x'
}
key C {
label: '\u0421'
base: '\u0441'
shift, capslock: '\u0421'
+ shift+capslock: '\u0441'
ralt: 'c'
- ralt+shift, ralt+capslock: 'C'
+ shift+ralt, capslock+ralt: 'C'
+ shift+capslock+ralt: 'c'
}
key V {
label: '\u041c'
base: '\u043c'
shift, capslock: '\u041c'
+ shift+capslock: '\u043c'
ralt: 'v'
- ralt+shift, ralt+capslock: 'V'
+ shift+ralt, capslock+ralt: 'V'
+ shift+capslock+ralt: 'v'
}
key B {
label: '\u0418'
base: '\u0438'
shift, capslock: '\u0418'
+ shift+capslock: '\u0438'
ralt: 'b'
- ralt+shift, ralt+capslock: 'B'
+ shift+ralt, capslock+ralt: 'B'
+ shift+capslock+ralt: 'b'
}
key N {
label: '\u0422'
base: '\u0442'
shift, capslock: '\u0422'
+ shift+capslock: '\u0442'
ralt: 'n'
- ralt+shift, ralt+capslock: 'N'
+ shift+ralt, capslock+ralt: 'N'
+ shift+capslock+ralt: 'n'
}
key M {
label: '\u042c'
base: '\u044c'
shift, capslock: '\u042c'
+ shift+capslock: '\u044c'
ralt: 'm'
- ralt+shift, ralt+capslock: 'M'
+ shift+ralt, capslock+ralt: 'M'
+ shift+capslock+ralt: 'm'
}
key COMMA {
label: '\u0411'
base: '\u0431'
shift, capslock: '\u0411'
+ shift+capslock: '\u0431'
ralt: ','
ralt+shift: '<'
}
@@ -391,6 +450,7 @@
label: '\u042e'
base: '\u044e'
shift, capslock: '\u042e'
+ shift+capslock: '\u044e'
ralt: '.'
ralt+shift: '>'
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java b/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java
index bd9e760..c8bcabf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java
@@ -35,6 +35,7 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.function.Predicate;
/**
* Class for managing services matching a given intent and requesting a given permission.
@@ -51,12 +52,13 @@
private final HashSet<ComponentName> mEnabledServices = new HashSet<>();
private final List<ServiceInfo> mServices = new ArrayList<>();
private final List<Callback> mCallbacks = new ArrayList<>();
+ private final Predicate mValidator;
private boolean mListening;
private ServiceListing(Context context, String tag,
String setting, String intentAction, String permission, String noun,
- boolean addDeviceLockedFlags) {
+ boolean addDeviceLockedFlags, Predicate validator) {
mContentResolver = context.getContentResolver();
mContext = context;
mTag = tag;
@@ -65,6 +67,7 @@
mPermission = permission;
mNoun = noun;
mAddDeviceLockedFlags = addDeviceLockedFlags;
+ mValidator = validator;
}
public void addCallback(Callback callback) {
@@ -137,7 +140,6 @@
final PackageManager pmWrapper = mContext.getPackageManager();
List<ResolveInfo> installedServices = pmWrapper.queryIntentServicesAsUser(
new Intent(mIntentAction), flags, user);
-
for (ResolveInfo resolveInfo : installedServices) {
ServiceInfo info = resolveInfo.serviceInfo;
@@ -148,6 +150,9 @@
+ mPermission);
continue;
}
+ if (mValidator != null && !mValidator.test(info)) {
+ continue;
+ }
mServices.add(info);
}
for (Callback callback : mCallbacks) {
@@ -194,6 +199,7 @@
private String mPermission;
private String mNoun;
private boolean mAddDeviceLockedFlags = false;
+ private Predicate mValidator;
public Builder(Context context) {
mContext = context;
@@ -224,6 +230,11 @@
return this;
}
+ public Builder setValidator(Predicate<ServiceInfo> validator) {
+ mValidator = validator;
+ return this;
+ }
+
/**
* Set to true to add support for both MATCH_DIRECT_BOOT_AWARE and
* MATCH_DIRECT_BOOT_UNAWARE flags when querying PackageManager. Required to get results
@@ -236,7 +247,7 @@
public ServiceListing build() {
return new ServiceListing(mContext, mTag, mSetting, mIntentAction, mPermission, mNoun,
- mAddDeviceLockedFlags);
+ mAddDeviceLockedFlags, mValidator);
}
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index 688fc72..c4f09ce 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -31,13 +31,15 @@
import android.provider.Settings;
import android.service.dreams.DreamService;
import android.service.dreams.IDreamManager;
+import android.util.ArraySet;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
@@ -116,7 +118,7 @@
private final boolean mDreamsActivatedOnSleepByDefault;
private final boolean mDreamsActivatedOnDockByDefault;
private final Set<ComponentName> mDisabledDreams;
- private final Set<Integer> mSupportedComplications;
+ private Set<Integer> mSupportedComplications;
private static DreamBackend sInstance;
public static DreamBackend getInstance(Context context) {
@@ -281,7 +283,18 @@
/** Gets all complications which have been enabled by the user. */
public Set<Integer> getEnabledComplications() {
- return getComplicationsEnabled() ? mSupportedComplications : Collections.emptySet();
+ final Set<Integer> enabledComplications =
+ getComplicationsEnabled()
+ ? new ArraySet<>(mSupportedComplications) : new ArraySet<>();
+
+ if (!getHomeControlsEnabled()) {
+ enabledComplications.remove(COMPLICATION_TYPE_HOME_CONTROLS);
+ } else if (mSupportedComplications.contains(COMPLICATION_TYPE_HOME_CONTROLS)) {
+ // Add home control type to list of enabled complications, even if other complications
+ // have been disabled.
+ enabledComplications.add(COMPLICATION_TYPE_HOME_CONTROLS);
+ }
+ return enabledComplications;
}
/** Sets complication enabled state. */
@@ -290,6 +303,18 @@
Settings.Secure.SCREENSAVER_COMPLICATIONS_ENABLED, enabled ? 1 : 0);
}
+ /** Sets whether home controls are enabled by the user on the dream */
+ public void setHomeControlsEnabled(boolean enabled) {
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, enabled ? 1 : 0);
+ }
+
+ /** Gets whether home controls button is enabled on the dream */
+ private boolean getHomeControlsEnabled() {
+ return Settings.Secure.getInt(mContext.getContentResolver(),
+ Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, 1) == 1;
+ }
+
/**
* Gets whether complications are enabled on this device
*/
@@ -304,6 +329,14 @@
return mSupportedComplications;
}
+ /**
+ * Sets the list of supported complications. Should only be used in tests.
+ */
+ @VisibleForTesting
+ public void setSupportedComplications(Set<Integer> complications) {
+ mSupportedComplications = complications;
+ }
+
public boolean isEnabled() {
return getBoolean(Settings.Secure.SCREENSAVER_ENABLED, mDreamsEnabledByDefault);
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 684a9aa..c9e8312 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -31,7 +31,6 @@
import static android.media.MediaRoute2Info.TYPE_USB_HEADSET;
import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
-import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR;
import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
@@ -621,9 +620,11 @@
dispatchConnectedDeviceChanged(id);
}
+ /**
+ * Ignore callback here since we'll also receive {@link onRequestFailed} with reason code.
+ */
@Override
public void onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route) {
- dispatchOnRequestFailed(REASON_UNKNOWN_ERROR);
}
@Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index 6b9866b..071ab27 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -33,7 +33,6 @@
import static android.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION;
import static android.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION_MANAGED;
import static android.media.RouteListingPreference.Item.FLAG_SUGGESTED;
-import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_TRANSFER;
import static android.media.RouteListingPreference.Item.SUBTEXT_AD_ROUTING_DISALLOWED;
import static android.media.RouteListingPreference.Item.SUBTEXT_CUSTOM;
import static android.media.RouteListingPreference.Item.SUBTEXT_DEVICE_LOW_POWER;
@@ -45,6 +44,7 @@
import static android.media.RouteListingPreference.Item.SUBTEXT_UNAUTHORIZED;
import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
+import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
import android.annotation.SuppressLint;
import android.content.Context;
@@ -95,6 +95,17 @@
int TYPE_CAST_GROUP_DEVICE = 7;
}
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SelectionBehavior.SELECTION_BEHAVIOR_NONE,
+ SELECTION_BEHAVIOR_TRANSFER,
+ SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP
+ })
+ public @interface SelectionBehavior {
+ int SELECTION_BEHAVIOR_NONE = 0;
+ int SELECTION_BEHAVIOR_TRANSFER = 1;
+ int SELECTION_BEHAVIOR_GO_TO_APP = 2;
+ }
+
@VisibleForTesting
int mType;
@@ -213,7 +224,7 @@
*
* @return selection behavior of device
*/
- @RouteListingPreference.Item.SubText
+ @SelectionBehavior
public int getSelectionBehavior() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null
? mItem.getSelectionBehavior() : SELECTION_BEHAVIOR_TRANSFER;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ServiceListingTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ServiceListingTest.java
index f7fd25b..7ff0988 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ServiceListingTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ServiceListingTest.java
@@ -18,20 +18,35 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
import android.provider.Settings;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.collect.ImmutableList;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
+import java.util.List;
+
@RunWith(RobolectricTestRunner.class)
public class ServiceListingTest {
@@ -39,10 +54,16 @@
private static final String TEST_INTENT = "com.example.intent";
private ServiceListing mServiceListing;
+ private Context mContext;
+ private PackageManager mPm;
@Before
public void setUp() {
- mServiceListing = new ServiceListing.Builder(RuntimeEnvironment.application)
+ mPm = mock(PackageManager.class);
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ when(mContext.getPackageManager()).thenReturn(mPm);
+
+ mServiceListing = new ServiceListing.Builder(mContext)
.setTag("testTag")
.setSetting(TEST_SETTING)
.setNoun("testNoun")
@@ -52,6 +73,81 @@
}
@Test
+ public void testValidator() {
+ ServiceInfo s1 = new ServiceInfo();
+ s1.permission = "testPermission";
+ s1.packageName = "pkg";
+ ServiceInfo s2 = new ServiceInfo();
+ s2.permission = "testPermission";
+ s2.packageName = "pkg2";
+ ResolveInfo r1 = new ResolveInfo();
+ r1.serviceInfo = s1;
+ ResolveInfo r2 = new ResolveInfo();
+ r2.serviceInfo = s2;
+
+ when(mPm.queryIntentServicesAsUser(any(), anyInt(), anyInt())).thenReturn(
+ ImmutableList.of(r1, r2));
+
+ mServiceListing = new ServiceListing.Builder(mContext)
+ .setTag("testTag")
+ .setSetting(TEST_SETTING)
+ .setNoun("testNoun")
+ .setIntentAction(TEST_INTENT)
+ .setValidator(info -> {
+ if (info.packageName.equals("pkg")) {
+ return true;
+ }
+ return false;
+ })
+ .setPermission("testPermission")
+ .build();
+ ServiceListing.Callback callback = mock(ServiceListing.Callback.class);
+ mServiceListing.addCallback(callback);
+ mServiceListing.reload();
+
+ verify(mPm).queryIntentServicesAsUser(any(), anyInt(), anyInt());
+ ArgumentCaptor<List<ServiceInfo>> captor = ArgumentCaptor.forClass(List.class);
+ verify(callback, times(1)).onServicesReloaded(captor.capture());
+
+ assertThat(captor.getValue().size()).isEqualTo(1);
+ assertThat(captor.getValue().get(0)).isEqualTo(s1);
+ }
+
+ @Test
+ public void testNoValidator() {
+ ServiceInfo s1 = new ServiceInfo();
+ s1.permission = "testPermission";
+ s1.packageName = "pkg";
+ ServiceInfo s2 = new ServiceInfo();
+ s2.permission = "testPermission";
+ s2.packageName = "pkg2";
+ ResolveInfo r1 = new ResolveInfo();
+ r1.serviceInfo = s1;
+ ResolveInfo r2 = new ResolveInfo();
+ r2.serviceInfo = s2;
+
+ when(mPm.queryIntentServicesAsUser(any(), anyInt(), anyInt())).thenReturn(
+ ImmutableList.of(r1, r2));
+
+ mServiceListing = new ServiceListing.Builder(mContext)
+ .setTag("testTag")
+ .setSetting(TEST_SETTING)
+ .setNoun("testNoun")
+ .setIntentAction(TEST_INTENT)
+ .setPermission("testPermission")
+ .build();
+ ServiceListing.Callback callback = mock(ServiceListing.Callback.class);
+ mServiceListing.addCallback(callback);
+ mServiceListing.reload();
+
+ verify(mPm).queryIntentServicesAsUser(any(), anyInt(), anyInt());
+ ArgumentCaptor<List<ServiceInfo>> captor = ArgumentCaptor.forClass(List.class);
+ verify(callback, times(1)).onServicesReloaded(captor.capture());
+
+ assertThat(captor.getValue().size()).isEqualTo(2);
+ }
+
+ @Test
public void testCallback() {
ServiceListing.Callback callback = mock(ServiceListing.Callback.class);
mServiceListing.addCallback(callback);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
index 52b9227..22ec12d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
@@ -16,6 +16,10 @@
package com.android.settingslib.dream;
+import static com.android.settingslib.dream.DreamBackend.COMPLICATION_TYPE_DATE;
+import static com.android.settingslib.dream.DreamBackend.COMPLICATION_TYPE_HOME_CONTROLS;
+import static com.android.settingslib.dream.DreamBackend.COMPLICATION_TYPE_TIME;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
@@ -36,13 +40,16 @@
import org.robolectric.shadows.ShadowSettings;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowSettings.ShadowSecure.class})
public final class DreamBackendTest {
- private static final int[] SUPPORTED_DREAM_COMPLICATIONS = {1, 2, 3};
+ private static final int[] SUPPORTED_DREAM_COMPLICATIONS =
+ {COMPLICATION_TYPE_HOME_CONTROLS, COMPLICATION_TYPE_DATE,
+ COMPLICATION_TYPE_TIME};
private static final List<Integer> SUPPORTED_DREAM_COMPLICATIONS_LIST = Arrays.stream(
SUPPORTED_DREAM_COMPLICATIONS).boxed().collect(
Collectors.toList());
@@ -93,8 +100,52 @@
@Test
public void testDisableComplications() {
mBackend.setComplicationsEnabled(false);
- assertThat(mBackend.getEnabledComplications()).isEmpty();
+ assertThat(mBackend.getEnabledComplications())
+ .containsExactly(COMPLICATION_TYPE_HOME_CONTROLS);
assertThat(mBackend.getComplicationsEnabled()).isFalse();
}
-}
+ @Test
+ public void testHomeControlsDisabled_ComplicationsEnabled() {
+ mBackend.setComplicationsEnabled(true);
+ mBackend.setHomeControlsEnabled(false);
+ // Home controls should not be enabled, only date and time.
+ final List<Integer> enabledComplications =
+ Arrays.asList(COMPLICATION_TYPE_DATE, COMPLICATION_TYPE_TIME);
+ assertThat(mBackend.getEnabledComplications())
+ .containsExactlyElementsIn(enabledComplications);
+ }
+
+ @Test
+ public void testHomeControlsDisabled_ComplicationsDisabled() {
+ mBackend.setComplicationsEnabled(false);
+ mBackend.setHomeControlsEnabled(false);
+ assertThat(mBackend.getEnabledComplications()).isEmpty();
+ }
+
+ @Test
+ public void testHomeControlsEnabled_ComplicationsDisabled() {
+ mBackend.setComplicationsEnabled(false);
+ mBackend.setHomeControlsEnabled(true);
+ // Home controls should not be enabled, only date and time.
+ final List<Integer> enabledComplications =
+ Collections.singletonList(COMPLICATION_TYPE_HOME_CONTROLS);
+ assertThat(mBackend.getEnabledComplications())
+ .containsExactlyElementsIn(enabledComplications);
+ }
+
+ @Test
+ public void testHomeControlsEnabled_ComplicationsEnabled() {
+ mBackend.setComplicationsEnabled(true);
+ mBackend.setHomeControlsEnabled(true);
+ // Home controls should not be enabled, only date and time.
+ final List<Integer> enabledComplications =
+ Arrays.asList(
+ COMPLICATION_TYPE_HOME_CONTROLS,
+ COMPLICATION_TYPE_DATE,
+ COMPLICATION_TYPE_TIME
+ );
+ assertThat(mBackend.getEnabledComplications())
+ .containsExactlyElementsIn(enabledComplications);
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
index f63c06a..270fda8 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
@@ -30,6 +30,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -799,12 +800,12 @@
}
@Test
- public void onTransferFailed_shouldDispatchOnRequestFailed() {
+ public void onTransferFailed_notDispatchOnRequestFailed() {
mInfoMediaManager.registerCallback(mCallback);
mInfoMediaManager.mMediaRouterCallback.onTransferFailed(null, null);
- verify(mCallback).onRequestFailed(REASON_UNKNOWN_ERROR);
+ verify(mCallback, never()).onRequestFailed(REASON_UNKNOWN_ERROR);
}
@Test
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
index 09a1ba2..e50f522 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
@@ -49,6 +49,7 @@
Settings.Global.CHARGING_SOUNDS_ENABLED,
Settings.Global.USB_MASS_STORAGE_ENABLED,
Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED,
+ Settings.Global.NETWORK_AVOID_BAD_WIFI,
Settings.Global.WIFI_WAKEUP_ENABLED,
Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON,
Settings.Global.USE_OPEN_WIFI_PACKAGE,
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index f66fcba..3efb41d 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -139,6 +139,7 @@
Settings.Secure.SCREENSAVER_COMPONENTS,
Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+ Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED,
Settings.Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION,
Settings.Secure.VOLUME_HUSH_GESTURE,
Settings.Secure.MANUAL_RINGER_TOGGLE_COUNT,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index e57cf3b..8d07fb6 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -20,6 +20,9 @@
import static android.hardware.display.HdrConversionMode.HDR_CONVERSION_PASSTHROUGH;
import static android.hardware.display.HdrConversionMode.HDR_CONVERSION_SYSTEM;
import static android.media.AudioFormat.SURROUND_SOUND_ENCODING;
+import static android.net.ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI_AVOID;
+import static android.net.ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI_IGNORE;
+import static android.net.ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI_PROMPT;
import static android.provider.settings.validators.SettingsValidators.ANY_INTEGER_VALIDATOR;
import static android.provider.settings.validators.SettingsValidators.ANY_STRING_VALIDATOR;
import static android.provider.settings.validators.SettingsValidators.BOOLEAN_VALIDATOR;
@@ -103,6 +106,14 @@
VALIDATORS.put(
Global.NETWORK_RECOMMENDATIONS_ENABLED,
new DiscreteValueValidator(new String[] {"-1", "0", "1"}));
+ VALIDATORS.put(
+ Global.NETWORK_AVOID_BAD_WIFI,
+ new DiscreteValueValidator(
+ new String[] {
+ String.valueOf(NETWORK_AVOID_BAD_WIFI_IGNORE),
+ String.valueOf(NETWORK_AVOID_BAD_WIFI_PROMPT),
+ String.valueOf(NETWORK_AVOID_BAD_WIFI_AVOID),
+ }));
VALIDATORS.put(Global.WIFI_WAKEUP_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, BOOLEAN_VALIDATOR);
VALIDATORS.put(
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 558e19f..abd2c75 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -206,6 +206,7 @@
VALIDATORS.put(Secure.SCREENSAVER_COMPONENTS, COMMA_SEPARATED_COMPONENT_LIST_VALIDATOR);
VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_DOCK, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.VOLUME_HUSH_GESTURE, NON_NEGATIVE_INTEGER_VALIDATOR);
VALIDATORS.put(
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/GenerationRegistry.java b/packages/SettingsProvider/src/com/android/providers/settings/GenerationRegistry.java
index db6cc1a..a4c59ea 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/GenerationRegistry.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/GenerationRegistry.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.os.Bundle;
+import android.os.UserHandle;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.MemoryIntArray;
@@ -81,6 +82,10 @@
}
private void incrementGenerationInternal(int key, @NonNull String indexMapKey) {
+ if (SettingsState.isGlobalSettingsKey(key)) {
+ // Global settings are shared across users, so ignore the userId in the key
+ key = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM);
+ }
synchronized (mLock) {
final MemoryIntArray backingStore = getBackingStoreLocked(key,
/* createIfNotExist= */ false);
@@ -126,6 +131,10 @@
* returning the result.
*/
public void addGenerationData(Bundle bundle, int key, String indexMapKey) {
+ if (SettingsState.isGlobalSettingsKey(key)) {
+ // Global settings are shared across users, so ignore the userId in the key
+ key = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM);
+ }
synchronized (mLock) {
final MemoryIntArray backingStore = getBackingStoreLocked(key,
/* createIfNotExist= */ true);
@@ -140,11 +149,9 @@
// Should not happen unless having error accessing the backing store
return;
}
- bundle.putParcelable(Settings.CALL_METHOD_TRACK_GENERATION_KEY,
- backingStore);
+ bundle.putParcelable(Settings.CALL_METHOD_TRACK_GENERATION_KEY, backingStore);
bundle.putInt(Settings.CALL_METHOD_GENERATION_INDEX_KEY, index);
- bundle.putInt(Settings.CALL_METHOD_GENERATION_KEY,
- backingStore.get(index));
+ bundle.putInt(Settings.CALL_METHOD_GENERATION_KEY, backingStore.get(index));
if (DEBUG) {
Slog.i(LOG_TAG, "Exported index:" + index + " for "
+ (indexMapKey.isEmpty() ? "unset settings" : "setting:" + indexMapKey)
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index e6408bf..7607909 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -35,6 +35,13 @@
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
import static com.android.internal.accessibility.util.AccessibilityUtils.ACCESSIBILITY_MENU_IN_SYSTEM;
import static com.android.providers.settings.SettingsState.FALLBACK_FILE_SUFFIX;
+import static com.android.providers.settings.SettingsState.getTypeFromKey;
+import static com.android.providers.settings.SettingsState.getUserIdFromKey;
+import static com.android.providers.settings.SettingsState.isConfigSettingsKey;
+import static com.android.providers.settings.SettingsState.isGlobalSettingsKey;
+import static com.android.providers.settings.SettingsState.isSecureSettingsKey;
+import static com.android.providers.settings.SettingsState.isSsaidSettingsKey;
+import static com.android.providers.settings.SettingsState.isSystemSettingsKey;
import static com.android.providers.settings.SettingsState.makeKey;
import android.Manifest;
@@ -376,14 +383,6 @@
@GuardedBy("mLock")
private boolean mSyncConfigDisabledUntilReboot;
- public static int getTypeFromKey(int key) {
- return SettingsState.getTypeFromKey(key);
- }
-
- public static int getUserIdFromKey(int key) {
- return SettingsState.getUserIdFromKey(key);
- }
-
@ChangeId
@EnabledSince(targetSdkVersion=android.os.Build.VERSION_CODES.S)
private static final long ENFORCE_READ_PERMISSION_FOR_MULTI_SIM_DATA_CALL = 172670679L;
@@ -3620,26 +3619,6 @@
}
}
- private boolean isConfigSettingsKey(int key) {
- return getTypeFromKey(key) == SETTINGS_TYPE_CONFIG;
- }
-
- private boolean isGlobalSettingsKey(int key) {
- return getTypeFromKey(key) == SETTINGS_TYPE_GLOBAL;
- }
-
- private boolean isSystemSettingsKey(int key) {
- return getTypeFromKey(key) == SETTINGS_TYPE_SYSTEM;
- }
-
- private boolean isSecureSettingsKey(int key) {
- return getTypeFromKey(key) == SETTINGS_TYPE_SECURE;
- }
-
- private boolean isSsaidSettingsKey(int key) {
- return getTypeFromKey(key) == SETTINGS_TYPE_SSAID;
- }
-
private boolean shouldBan(int type) {
if (SETTINGS_TYPE_CONFIG != type) {
return false;
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 4d8705f..e3153e0 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -255,6 +255,26 @@
}
}
+ public static boolean isConfigSettingsKey(int key) {
+ return getTypeFromKey(key) == SETTINGS_TYPE_CONFIG;
+ }
+
+ public static boolean isGlobalSettingsKey(int key) {
+ return getTypeFromKey(key) == SETTINGS_TYPE_GLOBAL;
+ }
+
+ public static boolean isSystemSettingsKey(int key) {
+ return getTypeFromKey(key) == SETTINGS_TYPE_SYSTEM;
+ }
+
+ public static boolean isSecureSettingsKey(int key) {
+ return getTypeFromKey(key) == SETTINGS_TYPE_SECURE;
+ }
+
+ public static boolean isSsaidSettingsKey(int key) {
+ return getTypeFromKey(key) == SETTINGS_TYPE_SSAID;
+ }
+
public static String keyToString(int key) {
return "Key[user=" + getUserIdFromKey(key) + ";type="
+ settingTypeToString(getTypeFromKey(key)) + "]";
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 278ceb9..1f14723 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -372,7 +372,6 @@
Settings.Global.NETPOLICY_QUOTA_FRAC_JOBS,
Settings.Global.NETPOLICY_QUOTA_FRAC_MULTIPATH,
Settings.Global.NETPOLICY_OVERRIDE_ENABLED,
- Settings.Global.NETWORK_AVOID_BAD_WIFI,
Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES,
Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE,
Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME,
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
new file mode 100644
index 0000000..78ae4af
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
@@ -0,0 +1,59 @@
+package com.android.systemui.animation
+
+private const val TAG_WGHT = "wght"
+private const val TAG_WDTH = "wdth"
+private const val TAG_OPSZ = "opsz"
+private const val TAG_ROND = "ROND"
+
+class FontVariationUtils {
+ private var mWeight = -1
+ private var mWidth = -1
+ private var mOpticalSize = -1
+ private var mRoundness = -1
+ private var isUpdated = false
+
+ /*
+ * generate fontVariationSettings string, used for key in typefaceCache in TextAnimator
+ * the order of axes should align to the order of parameters
+ * if every axis remains unchanged, return ""
+ */
+ fun updateFontVariation(
+ weight: Int = -1,
+ width: Int = -1,
+ opticalSize: Int = -1,
+ roundness: Int = -1
+ ): String {
+ isUpdated = false
+ if (weight >= 0 && mWeight != weight) {
+ isUpdated = true
+ mWeight = weight
+ }
+ if (width >= 0 && mWidth != width) {
+ isUpdated = true
+ mWidth = width
+ }
+ if (opticalSize >= 0 && mOpticalSize != opticalSize) {
+ isUpdated = true
+ mOpticalSize = opticalSize
+ }
+
+ if (roundness >= 0 && mRoundness != roundness) {
+ isUpdated = true
+ mRoundness = roundness
+ }
+ var resultString = ""
+ if (mWeight >= 0) {
+ resultString += "'$TAG_WGHT' $mWeight"
+ }
+ if (mWidth >= 0) {
+ resultString += (if (resultString.isBlank()) "" else ", ") + "'$TAG_WDTH' $mWidth"
+ }
+ if (mOpticalSize >= 0) {
+ resultString += (if (resultString.isBlank()) "" else ", ") + "'$TAG_OPSZ' $mOpticalSize"
+ }
+ if (mRoundness >= 0) {
+ resultString += (if (resultString.isBlank()) "" else ", ") + "'$TAG_ROND' $mRoundness"
+ }
+ return if (isUpdated) resultString else ""
+ }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt
index 6946e6b..03e1e66 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt
@@ -59,12 +59,14 @@
// changes should be ordered top-to-bottom in z
val mode = change.mode
+ var rootIdx = info.findRootIndex(change.endDisplayId)
+ if (rootIdx < 0) rootIdx = 0
// Launcher animates leaf tasks directly, so always reparent all task leashes to root.
- t.reparent(leash, info.rootLeash)
+ t.reparent(leash, info.getRoot(rootIdx).leash)
t.setPosition(
leash,
- (change.startAbsBounds.left - info.rootOffset.x).toFloat(),
- (change.startAbsBounds.top - info.rootOffset.y).toFloat()
+ (change.startAbsBounds.left - info.getRoot(rootIdx).offset.x).toFloat(),
+ (change.startAbsBounds.top - info.getRoot(rootIdx).offset.y).toFloat()
)
t.show(leash)
// Put all the OPEN/SHOW on top
@@ -114,8 +116,11 @@
.setName(change.leash.toString() + "_transition-leash")
.setContainerLayer()
.setParent(
- if (change.parent == null) info.rootLeash
- else info.getChange(change.parent!!)!!.leash
+ if (change.parent == null) {
+ var rootIdx = info.findRootIndex(change.endDisplayId)
+ if (rootIdx < 0) rootIdx = 0
+ info.getRoot(rootIdx).leash
+ } else info.getChange(change.parent!!)!!.leash
)
.build()
// Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index 7fe94d3..9e9929e 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -23,11 +23,8 @@
import android.graphics.Canvas
import android.graphics.Typeface
import android.graphics.fonts.Font
-import android.graphics.fonts.FontVariationAxis
import android.text.Layout
-import android.util.SparseArray
-private const val TAG_WGHT = "wght"
private const val DEFAULT_ANIMATION_DURATION: Long = 300
typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit
@@ -51,7 +48,7 @@
*
* // Change the text size with animation.
* fun setTextSize(sizePx: Float, animate: Boolean) {
- * animator.setTextStyle(-1 /* unchanged weight */, sizePx, animate)
+ * animator.setTextStyle("" /* unchanged fvar... */, sizePx, animate)
* }
* }
* ```
@@ -115,7 +112,9 @@
protected set
}
- private val typefaceCache = SparseArray<Typeface?>()
+ private val fontVariationUtils = FontVariationUtils()
+
+ private val typefaceCache = HashMap<String, Typeface?>()
fun updateLayout(layout: Layout) {
textInterpolator.layout = layout
@@ -186,7 +185,7 @@
* Bu passing -1 to duration, the default text animation, 1000ms, is used.
* By passing false to animate, the text will be updated without animation.
*
- * @param weight an optional text weight.
+ * @param fvar an optional text fontVariationSettings.
* @param textSize an optional font size.
* @param colors an optional colors array that must be the same size as numLines passed to
* the TextInterpolator
@@ -199,7 +198,7 @@
* will be used. This is ignored if animate is false.
*/
fun setTextStyle(
- weight: Int = -1,
+ fvar: String? = "",
textSize: Float = -1f,
color: Int? = null,
strokeWidth: Float = -1f,
@@ -217,42 +216,16 @@
if (textSize >= 0) {
textInterpolator.targetPaint.textSize = textSize
}
- if (weight >= 0) {
- val fontVariationArray =
- FontVariationAxis.fromFontVariationSettings(
- textInterpolator.targetPaint.fontVariationSettings
- )
- if (fontVariationArray.isNullOrEmpty()) {
- textInterpolator.targetPaint.typeface =
- typefaceCache.getOrElse(weight) {
- textInterpolator.targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight"
- textInterpolator.targetPaint.typeface
- }
- } else {
- val idx = fontVariationArray.indexOfFirst { it.tag == "$TAG_WGHT" }
- if (idx == -1) {
- val updatedFontVariation =
- textInterpolator.targetPaint.fontVariationSettings + ",'$TAG_WGHT' $weight"
- textInterpolator.targetPaint.typeface =
- typefaceCache.getOrElse(weight) {
- textInterpolator.targetPaint.fontVariationSettings =
- updatedFontVariation
- textInterpolator.targetPaint.typeface
- }
- } else {
- fontVariationArray[idx] = FontVariationAxis(
- "$TAG_WGHT", weight.toFloat())
- val updatedFontVariation =
- FontVariationAxis.toFontVariationSettings(fontVariationArray)
- textInterpolator.targetPaint.typeface =
- typefaceCache.getOrElse(weight) {
- textInterpolator.targetPaint.fontVariationSettings =
- updatedFontVariation
- textInterpolator.targetPaint.typeface
- }
+
+ if (!fvar.isNullOrBlank()) {
+ textInterpolator.targetPaint.typeface =
+ typefaceCache.getOrElse(fvar) {
+ textInterpolator.targetPaint.fontVariationSettings = fvar
+ typefaceCache.put(fvar, textInterpolator.targetPaint.typeface)
+ textInterpolator.targetPaint.typeface
}
- }
}
+
if (color != null) {
textInterpolator.targetPaint.color = color
}
@@ -291,13 +264,56 @@
invalidateCallback()
}
}
+
+ /**
+ * Set text style with animation. Similar as
+ * fun setTextStyle(
+ * fvar: String? = "",
+ * textSize: Float = -1f,
+ * color: Int? = null,
+ * strokeWidth: Float = -1f,
+ * animate: Boolean = true,
+ * duration: Long = -1L,
+ * interpolator: TimeInterpolator? = null,
+ * delay: Long = 0,
+ * onAnimationEnd: Runnable? = null
+ * )
+ *
+ * @param weight an optional style value for `wght` in fontVariationSettings.
+ * @param width an optional style value for `wdth` in fontVariationSettings.
+ * @param opticalSize an optional style value for `opsz` in fontVariationSettings.
+ * @param roundness an optional style value for `ROND` in fontVariationSettings.
+ */
+ fun setTextStyle(
+ weight: Int = -1,
+ width: Int = -1,
+ opticalSize: Int = -1,
+ roundness: Int = -1,
+ textSize: Float = -1f,
+ color: Int? = null,
+ strokeWidth: Float = -1f,
+ animate: Boolean = true,
+ duration: Long = -1L,
+ interpolator: TimeInterpolator? = null,
+ delay: Long = 0,
+ onAnimationEnd: Runnable? = null
+ ) {
+ val fvar = fontVariationUtils.updateFontVariation(
+ weight = weight,
+ width = width,
+ opticalSize = opticalSize,
+ roundness = roundness,)
+ setTextStyle(
+ fvar = fvar,
+ textSize = textSize,
+ color = color,
+ strokeWidth = strokeWidth,
+ animate = animate,
+ duration = duration,
+ interpolator = interpolator,
+ delay = delay,
+ onAnimationEnd = onAnimationEnd,
+ )
+ }
}
-private fun <V> SparseArray<V>.getOrElse(key: Int, defaultValue: () -> V): V {
- var v = get(key)
- if (v == null) {
- v = defaultValue()
- put(key, v)
- }
- return v
-}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt
index d78e0c1..280e7ed9 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt
@@ -53,6 +53,19 @@
cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy;
}
+ // Perceived luminosity (L′), not absolute luminosity.
+ half getLuminosity(vec3 c) {
+ return 0.3 * c.r + 0.59 * c.g + 0.11 * c.b;
+ }
+
+ // Creates a luminosity mask and clamp to the legal range.
+ vec3 maskLuminosity(vec3 dest, float lum) {
+ dest.rgb *= vec3(lum);
+ // Clip back into the legal range
+ dest = clamp(dest, vec3(0.), vec3(1.0));
+ return dest;
+ }
+
// Return range [-1, 1].
vec3 hash(vec3 p) {
p = fract(p * vec3(.3456, .1234, .9876));
@@ -62,14 +75,14 @@
}
// Skew factors (non-uniform).
- const float SKEW = 0.3333333; // 1/3
- const float UNSKEW = 0.1666667; // 1/6
+ const half SKEW = 0.3333333; // 1/3
+ const half UNSKEW = 0.1666667; // 1/6
// Return range roughly [-1,1].
// It's because the hash function (that returns a random gradient vector) returns
// different magnitude of vectors. Noise doesn't have to be in the precise range thus
// skipped normalize.
- float simplex3d(vec3 p) {
+ half simplex3d(vec3 p) {
// Skew the input coordinate, so that we get squashed cubical grid
vec3 s = floor(p + (p.x + p.y + p.z) * SKEW);
@@ -143,6 +156,22 @@
// Should multiply by the possible max contribution to adjust the range in [-1,1].
return dot(vec4(32.), nc);
}
+
+ // Random rotations.
+ // The way you create fractal noise is layering simplex noise with some rotation.
+ // To make random cloud looking noise, the rotations should not align. (Otherwise it
+ // creates patterned noise).
+ // Below rotations only rotate in one axis.
+ const mat3 rot1 = mat3(1.0, 0. ,0., 0., 0.15, -0.98, 0., 0.98, 0.15);
+ const mat3 rot2 = mat3(-0.95, 0. ,-0.3, 0., 1., 0., 0.3, 0., -0.95);
+ const mat3 rot3 = mat3(1.0, 0. ,0., 0., -0.44, -0.89, 0., 0.89, -0.44);
+
+ // Octave = 4
+ // Divide each coefficient by 3 to produce more grainy noise.
+ half simplex3d_fractal(vec3 mat) {
+ return 0.675 * simplex3d(mat * rot1) + 0.225 * simplex3d(2.0 * mat * rot2)
+ + 0.075 * simplex3d(4.0 * mat * rot3) + 0.025 * simplex3d(8.0 * mat);
+ }
"""
}
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
index 7456c43..0e22667 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
@@ -19,8 +19,13 @@
import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary
import java.lang.Float.max
-/** Shader that renders turbulence simplex noise, with no octave. */
-class TurbulenceNoiseShader : RuntimeShader(TURBULENCE_NOISE_SHADER) {
+/**
+ * Shader that renders turbulence simplex noise, by default no octave.
+ *
+ * @param useFractal whether to use fractal noise (4 octaves).
+ */
+class TurbulenceNoiseShader(useFractal: Boolean = false) :
+ RuntimeShader(if (useFractal) FRACTAL_NOISE_SHADER else SIMPLEX_NOISE_SHADER) {
// language=AGSL
companion object {
private const val UNIFORMS =
@@ -35,21 +40,7 @@
layout(color) uniform vec4 in_backgroundColor;
"""
- private const val SHADER_LIB =
- """
- float getLuminosity(vec3 c) {
- return 0.3*c.r + 0.59*c.g + 0.11*c.b;
- }
-
- vec3 maskLuminosity(vec3 dest, float lum) {
- dest.rgb *= vec3(lum);
- // Clip back into the legal range
- dest = clamp(dest, vec3(0.), vec3(1.0));
- return dest;
- }
- """
-
- private const val MAIN_SHADER =
+ private const val SIMPLEX_SHADER =
"""
vec4 main(vec2 p) {
vec2 uv = p / in_size.xy;
@@ -71,8 +62,26 @@
}
"""
- private const val TURBULENCE_NOISE_SHADER =
- ShaderUtilLibrary.SHADER_LIB + UNIFORMS + SHADER_LIB + MAIN_SHADER
+ private const val FRACTAL_SHADER =
+ """
+ vec4 main(vec2 p) {
+ vec2 uv = p / in_size.xy;
+ uv.x *= in_aspectRatio;
+
+ vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum;
+ float luma = simplex3d_fractal(noiseP) * in_opacity;
+ vec3 mask = maskLuminosity(in_color.rgb, luma);
+ vec3 color = in_backgroundColor.rgb + mask * 0.6;
+
+ // Skip dithering.
+ return vec4(color * in_color.a, in_color.a);
+ }
+ """
+
+ private const val SIMPLEX_NOISE_SHADER =
+ ShaderUtilLibrary.SHADER_LIB + UNIFORMS + SIMPLEX_SHADER
+ private const val FRACTAL_NOISE_SHADER =
+ ShaderUtilLibrary.SHADER_LIB + UNIFORMS + FRACTAL_SHADER
}
/** Sets the number of grid for generating noise. */
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DemotingTestWithoutBugDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DemotingTestWithoutBugDetector.kt
index 459a38e..09762b0 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DemotingTestWithoutBugDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DemotingTestWithoutBugDetector.kt
@@ -25,10 +25,12 @@
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
+import java.util.EnumSet
import java.util.regex.Pattern
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UElement
+@Suppress("UnstableApiUsage") // For linter api
class DemotingTestWithoutBugDetector : Detector(), SourceCodeScanner {
override fun getApplicableUastTypes(): List<Class<out UElement>> {
return listOf(UAnnotation::class.java)
@@ -39,18 +41,15 @@
override fun visitAnnotation(node: UAnnotation) {
// Annotations having int bugId field
if (node.qualifiedName in DEMOTING_ANNOTATION_BUG_ID) {
- val bugId = node.findAttributeValue("bugId")!!.evaluate() as Int
- if (bugId <= 0) {
+ if (!containsBugId(node)) {
val location = context.getLocation(node)
val message = "Please attach a bug id to track demoted test"
context.report(ISSUE, node, location, message)
}
}
- // @Ignore has a String field for reason
+ // @Ignore has a String field for specifying reasons
if (node.qualifiedName == DEMOTING_ANNOTATION_IGNORE) {
- val reason = node.findAttributeValue("value")!!.evaluate() as String
- val bugPattern = Pattern.compile("b/\\d+")
- if (!bugPattern.matcher(reason).find()) {
+ if (!containsBugString(node)) {
val location = context.getLocation(node)
val message = "Please attach a bug (e.g. b/123) to track demoted test"
context.report(ISSUE, node, location, message)
@@ -60,6 +59,17 @@
}
}
+ private fun containsBugId(node: UAnnotation): Boolean {
+ val bugId = node.findAttributeValue("bugId")?.evaluate() as Int?
+ return bugId != null && bugId > 0
+ }
+
+ private fun containsBugString(node: UAnnotation): Boolean {
+ val reason = node.findAttributeValue("value")?.evaluate() as String?
+ val bugPattern = Pattern.compile("b/\\d+")
+ return reason != null && bugPattern.matcher(reason).find()
+ }
+
companion object {
val DEMOTING_ANNOTATION_BUG_ID =
listOf(
@@ -87,7 +97,7 @@
implementation =
Implementation(
DemotingTestWithoutBugDetector::class.java,
- Scope.JAVA_FILE_SCOPE
+ EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
)
)
}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DemotingTestWithoutBugDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DemotingTestWithoutBugDetectorTest.kt
index 63eb263..a1e6f92 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DemotingTestWithoutBugDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DemotingTestWithoutBugDetectorTest.kt
@@ -20,8 +20,11 @@
import com.android.tools.lint.checks.infrastructure.TestFiles
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.Scope
+import java.util.EnumSet
import org.junit.Test
+@Suppress("UnstableApiUsage")
class DemotingTestWithoutBugDetectorTest : SystemUILintDetectorTest() {
override fun getDetector(): Detector = DemotingTestWithoutBugDetector()
@@ -45,6 +48,7 @@
.indented(),
*stubs
)
+ .customScope(testScope)
.issues(DemotingTestWithoutBugDetector.ISSUE)
.run()
.expectClean()
@@ -65,6 +69,7 @@
.indented(),
*stubs
)
+ .customScope(testScope)
.issues(DemotingTestWithoutBugDetector.ISSUE)
.run()
.expectClean()
@@ -88,6 +93,7 @@
.indented(),
*stubs
)
+ .customScope(testScope)
.issues(DemotingTestWithoutBugDetector.ISSUE)
.run()
.expect(
@@ -115,6 +121,7 @@
.indented(),
*stubs
)
+ .customScope(testScope)
.issues(DemotingTestWithoutBugDetector.ISSUE)
.run()
.expect(
@@ -145,6 +152,7 @@
.indented(),
*stubs
)
+ .customScope(testScope)
.issues(DemotingTestWithoutBugDetector.ISSUE)
.run()
.expectClean()
@@ -168,6 +176,7 @@
.indented(),
*stubs
)
+ .customScope(testScope)
.issues(DemotingTestWithoutBugDetector.ISSUE)
.run()
.expect(
@@ -198,6 +207,7 @@
.indented(),
*stubs
)
+ .customScope(testScope)
.issues(DemotingTestWithoutBugDetector.ISSUE)
.run()
.expectClean()
@@ -221,6 +231,7 @@
.indented(),
*stubs
)
+ .customScope(testScope)
.issues(DemotingTestWithoutBugDetector.ISSUE)
.run()
.expect(
@@ -248,6 +259,7 @@
.indented(),
*stubs
)
+ .customScope(testScope)
.issues(DemotingTestWithoutBugDetector.ISSUE)
.run()
.expect(
@@ -260,6 +272,7 @@
)
}
+ private val testScope = EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
private val filtersFlakyTestStub: TestFile =
java(
"""
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt b/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt
new file mode 100644
index 0000000..946e779
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt
@@ -0,0 +1,849 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.compose.swipeable
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.foundation.gestures.DraggableState
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import com.android.compose.swipeable.SwipeableDefaults.AnimationSpec
+import com.android.compose.swipeable.SwipeableDefaults.StandardResistanceFactor
+import com.android.compose.swipeable.SwipeableDefaults.VelocityThreshold
+import com.android.compose.swipeable.SwipeableDefaults.resistanceConfig
+import com.android.compose.ui.util.lerp
+import kotlin.math.PI
+import kotlin.math.abs
+import kotlin.math.sign
+import kotlin.math.sin
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.launch
+
+/**
+ * State of the [swipeable] modifier.
+ *
+ * This contains necessary information about any ongoing swipe or animation and provides methods to
+ * change the state either immediately or by starting an animation. To create and remember a
+ * [SwipeableState] with the default animation clock, use [rememberSwipeableState].
+ *
+ * @param initialValue The initial value of the state.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ *
+ * TODO(b/272311106): this is a fork from material. Unfork it when Swipeable.kt reaches material3.
+ */
+@Stable
+open class SwipeableState<T>(
+ initialValue: T,
+ internal val animationSpec: AnimationSpec<Float> = AnimationSpec,
+ internal val confirmStateChange: (newValue: T) -> Boolean = { true }
+) {
+ /**
+ * The current value of the state.
+ *
+ * If no swipe or animation is in progress, this corresponds to the anchor at which the
+ * [swipeable] is currently settled. If a swipe or animation is in progress, this corresponds
+ * the last anchor at which the [swipeable] was settled before the swipe or animation started.
+ */
+ var currentValue: T by mutableStateOf(initialValue)
+ private set
+
+ /** Whether the state is currently animating. */
+ var isAnimationRunning: Boolean by mutableStateOf(false)
+ private set
+
+ /**
+ * The current position (in pixels) of the [swipeable].
+ *
+ * You should use this state to offset your content accordingly. The recommended way is to use
+ * `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled.
+ */
+ val offset: State<Float>
+ get() = offsetState
+
+ /** The amount by which the [swipeable] has been swiped past its bounds. */
+ val overflow: State<Float>
+ get() = overflowState
+
+ // Use `Float.NaN` as a placeholder while the state is uninitialised.
+ private val offsetState = mutableStateOf(0f)
+ private val overflowState = mutableStateOf(0f)
+
+ // the source of truth for the "real"(non ui) position
+ // basically position in bounds + overflow
+ private val absoluteOffset = mutableStateOf(0f)
+
+ // current animation target, if animating, otherwise null
+ private val animationTarget = mutableStateOf<Float?>(null)
+
+ internal var anchors by mutableStateOf(emptyMap<Float, T>())
+
+ private val latestNonEmptyAnchorsFlow: Flow<Map<Float, T>> =
+ snapshotFlow { anchors }.filter { it.isNotEmpty() }.take(1)
+
+ internal var minBound = Float.NEGATIVE_INFINITY
+ internal var maxBound = Float.POSITIVE_INFINITY
+
+ internal fun ensureInit(newAnchors: Map<Float, T>) {
+ if (anchors.isEmpty()) {
+ // need to do initial synchronization synchronously :(
+ val initialOffset = newAnchors.getOffset(currentValue)
+ requireNotNull(initialOffset) { "The initial value must have an associated anchor." }
+ offsetState.value = initialOffset
+ absoluteOffset.value = initialOffset
+ }
+ }
+
+ internal suspend fun processNewAnchors(oldAnchors: Map<Float, T>, newAnchors: Map<Float, T>) {
+ if (oldAnchors.isEmpty()) {
+ // If this is the first time that we receive anchors, then we need to initialise
+ // the state so we snap to the offset associated to the initial value.
+ minBound = newAnchors.keys.minOrNull()!!
+ maxBound = newAnchors.keys.maxOrNull()!!
+ val initialOffset = newAnchors.getOffset(currentValue)
+ requireNotNull(initialOffset) { "The initial value must have an associated anchor." }
+ snapInternalToOffset(initialOffset)
+ } else if (newAnchors != oldAnchors) {
+ // If we have received new anchors, then the offset of the current value might
+ // have changed, so we need to animate to the new offset. If the current value
+ // has been removed from the anchors then we animate to the closest anchor
+ // instead. Note that this stops any ongoing animation.
+ minBound = Float.NEGATIVE_INFINITY
+ maxBound = Float.POSITIVE_INFINITY
+ val animationTargetValue = animationTarget.value
+ // if we're in the animation already, let's find it a new home
+ val targetOffset =
+ if (animationTargetValue != null) {
+ // first, try to map old state to the new state
+ val oldState = oldAnchors[animationTargetValue]
+ val newState = newAnchors.getOffset(oldState)
+ // return new state if exists, or find the closes one among new anchors
+ newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!!
+ } else {
+ // we're not animating, proceed by finding the new anchors for an old value
+ val actualOldValue = oldAnchors[offset.value]
+ val value = if (actualOldValue == currentValue) currentValue else actualOldValue
+ newAnchors.getOffset(value)
+ ?: newAnchors.keys.minByOrNull { abs(it - offset.value) }!!
+ }
+ try {
+ animateInternalToOffset(targetOffset, animationSpec)
+ } catch (c: CancellationException) {
+ // If the animation was interrupted for any reason, snap as a last resort.
+ snapInternalToOffset(targetOffset)
+ } finally {
+ currentValue = newAnchors.getValue(targetOffset)
+ minBound = newAnchors.keys.minOrNull()!!
+ maxBound = newAnchors.keys.maxOrNull()!!
+ }
+ }
+ }
+
+ internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f })
+
+ internal var velocityThreshold by mutableStateOf(0f)
+
+ internal var resistance: ResistanceConfig? by mutableStateOf(null)
+
+ internal val draggableState = DraggableState {
+ val newAbsolute = absoluteOffset.value + it
+ val clamped = newAbsolute.coerceIn(minBound, maxBound)
+ val overflow = newAbsolute - clamped
+ val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f
+ offsetState.value = clamped + resistanceDelta
+ overflowState.value = overflow
+ absoluteOffset.value = newAbsolute
+ }
+
+ private suspend fun snapInternalToOffset(target: Float) {
+ draggableState.drag { dragBy(target - absoluteOffset.value) }
+ }
+
+ private suspend fun animateInternalToOffset(target: Float, spec: AnimationSpec<Float>) {
+ draggableState.drag {
+ var prevValue = absoluteOffset.value
+ animationTarget.value = target
+ isAnimationRunning = true
+ try {
+ Animatable(prevValue).animateTo(target, spec) {
+ dragBy(this.value - prevValue)
+ prevValue = this.value
+ }
+ } finally {
+ animationTarget.value = null
+ isAnimationRunning = false
+ }
+ }
+ }
+
+ /**
+ * The target value of the state.
+ *
+ * If a swipe is in progress, this is the value that the [swipeable] would animate to if the
+ * swipe finished. If an animation is running, this is the target value of that animation.
+ * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
+ */
+ val targetValue: T
+ get() {
+ // TODO(calintat): Track current velocity (b/149549482) and use that here.
+ val target =
+ animationTarget.value
+ ?: computeTarget(
+ offset = offset.value,
+ lastValue = anchors.getOffset(currentValue) ?: offset.value,
+ anchors = anchors.keys,
+ thresholds = thresholds,
+ velocity = 0f,
+ velocityThreshold = Float.POSITIVE_INFINITY
+ )
+ return anchors[target] ?: currentValue
+ }
+
+ /**
+ * Information about the ongoing swipe or animation, if any. See [SwipeProgress] for details.
+ *
+ * If no swipe or animation is in progress, this returns `SwipeProgress(value, value, 1f)`.
+ */
+ val progress: SwipeProgress<T>
+ get() {
+ val bounds = findBounds(offset.value, anchors.keys)
+ val from: T
+ val to: T
+ val fraction: Float
+ when (bounds.size) {
+ 0 -> {
+ from = currentValue
+ to = currentValue
+ fraction = 1f
+ }
+ 1 -> {
+ from = anchors.getValue(bounds[0])
+ to = anchors.getValue(bounds[0])
+ fraction = 1f
+ }
+ else -> {
+ val (a, b) =
+ if (direction > 0f) {
+ bounds[0] to bounds[1]
+ } else {
+ bounds[1] to bounds[0]
+ }
+ from = anchors.getValue(a)
+ to = anchors.getValue(b)
+ fraction = (offset.value - a) / (b - a)
+ }
+ }
+ return SwipeProgress(from, to, fraction)
+ }
+
+ /**
+ * The direction in which the [swipeable] is moving, relative to the current [currentValue].
+ *
+ * This will be either 1f if it is is moving from left to right or top to bottom, -1f if it is
+ * moving from right to left or bottom to top, or 0f if no swipe or animation is in progress.
+ */
+ val direction: Float
+ get() = anchors.getOffset(currentValue)?.let { sign(offset.value - it) } ?: 0f
+
+ /**
+ * Set the state without any animation and suspend until it's set
+ *
+ * @param targetValue The new target value to set [currentValue] to.
+ */
+ suspend fun snapTo(targetValue: T) {
+ latestNonEmptyAnchorsFlow.collect { anchors ->
+ val targetOffset = anchors.getOffset(targetValue)
+ requireNotNull(targetOffset) { "The target value must have an associated anchor." }
+ snapInternalToOffset(targetOffset)
+ currentValue = targetValue
+ }
+ }
+
+ /**
+ * Set the state to the target value by starting an animation.
+ *
+ * @param targetValue The new value to animate to.
+ * @param anim The animation that will be used to animate to the new value.
+ */
+ suspend fun animateTo(targetValue: T, anim: AnimationSpec<Float> = animationSpec) {
+ latestNonEmptyAnchorsFlow.collect { anchors ->
+ try {
+ val targetOffset = anchors.getOffset(targetValue)
+ requireNotNull(targetOffset) { "The target value must have an associated anchor." }
+ animateInternalToOffset(targetOffset, anim)
+ } finally {
+ val endOffset = absoluteOffset.value
+ val endValue =
+ anchors
+ // fighting rounding error once again, anchor should be as close as 0.5
+ // pixels
+ .filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f }
+ .values
+ .firstOrNull()
+ ?: currentValue
+ currentValue = endValue
+ }
+ }
+ }
+
+ /**
+ * Perform fling with settling to one of the anchors which is determined by the given
+ * [velocity]. Fling with settling [swipeable] will always consume all the velocity provided
+ * since it will settle at the anchor.
+ *
+ * In general cases, [swipeable] flings by itself when being swiped. This method is to be used
+ * for nested scroll logic that wraps the [swipeable]. In nested scroll developer may want to
+ * trigger settling fling when the child scroll container reaches the bound.
+ *
+ * @param velocity velocity to fling and settle with
+ * @return the reason fling ended
+ */
+ suspend fun performFling(velocity: Float) {
+ latestNonEmptyAnchorsFlow.collect { anchors ->
+ val lastAnchor = anchors.getOffset(currentValue)!!
+ val targetValue =
+ computeTarget(
+ offset = offset.value,
+ lastValue = lastAnchor,
+ anchors = anchors.keys,
+ thresholds = thresholds,
+ velocity = velocity,
+ velocityThreshold = velocityThreshold
+ )
+ val targetState = anchors[targetValue]
+ if (targetState != null && confirmStateChange(targetState)) animateTo(targetState)
+ // If the user vetoed the state change, rollback to the previous state.
+ else animateInternalToOffset(lastAnchor, animationSpec)
+ }
+ }
+
+ /**
+ * Force [swipeable] to consume drag delta provided from outside of the regular [swipeable]
+ * gesture flow.
+ *
+ * Note: This method performs generic drag and it won't settle to any particular anchor, *
+ * leaving swipeable in between anchors. When done dragging, [performFling] must be called as
+ * well to ensure swipeable will settle at the anchor.
+ *
+ * In general cases, [swipeable] drags by itself when being swiped. This method is to be used
+ * for nested scroll logic that wraps the [swipeable]. In nested scroll developer may want to
+ * force drag when the child scroll container reaches the bound.
+ *
+ * @param delta delta in pixels to drag by
+ * @return the amount of [delta] consumed
+ */
+ fun performDrag(delta: Float): Float {
+ val potentiallyConsumed = absoluteOffset.value + delta
+ val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
+ val deltaToConsume = clamped - absoluteOffset.value
+ if (abs(deltaToConsume) > 0) {
+ draggableState.dispatchRawDelta(deltaToConsume)
+ }
+ return deltaToConsume
+ }
+
+ companion object {
+ /** The default [Saver] implementation for [SwipeableState]. */
+ fun <T : Any> Saver(
+ animationSpec: AnimationSpec<Float>,
+ confirmStateChange: (T) -> Boolean
+ ) =
+ Saver<SwipeableState<T>, T>(
+ save = { it.currentValue },
+ restore = { SwipeableState(it, animationSpec, confirmStateChange) }
+ )
+ }
+}
+
+/**
+ * Collects information about the ongoing swipe or animation in [swipeable].
+ *
+ * To access this information, use [SwipeableState.progress].
+ *
+ * @param from The state corresponding to the anchor we are moving away from.
+ * @param to The state corresponding to the anchor we are moving towards.
+ * @param fraction The fraction that the current position represents between [from] and [to]. Must
+ * be between `0` and `1`.
+ */
+@Immutable
+class SwipeProgress<T>(
+ val from: T,
+ val to: T,
+ /*@FloatRange(from = 0.0, to = 1.0)*/
+ val fraction: Float
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SwipeProgress<*>) return false
+
+ if (from != other.from) return false
+ if (to != other.to) return false
+ if (fraction != other.fraction) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = from?.hashCode() ?: 0
+ result = 31 * result + (to?.hashCode() ?: 0)
+ result = 31 * result + fraction.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "SwipeProgress(from=$from, to=$to, fraction=$fraction)"
+ }
+}
+
+/**
+ * Create and [remember] a [SwipeableState] with the default animation clock.
+ *
+ * @param initialValue The initial value of the state.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Composable
+fun <T : Any> rememberSwipeableState(
+ initialValue: T,
+ animationSpec: AnimationSpec<Float> = AnimationSpec,
+ confirmStateChange: (newValue: T) -> Boolean = { true }
+): SwipeableState<T> {
+ return rememberSaveable(
+ saver =
+ SwipeableState.Saver(
+ animationSpec = animationSpec,
+ confirmStateChange = confirmStateChange
+ )
+ ) {
+ SwipeableState(
+ initialValue = initialValue,
+ animationSpec = animationSpec,
+ confirmStateChange = confirmStateChange
+ )
+ }
+}
+
+/**
+ * Create and [remember] a [SwipeableState] which is kept in sync with another state, i.e.:
+ * 1. Whenever the [value] changes, the [SwipeableState] will be animated to that new value.
+ * 2. Whenever the value of the [SwipeableState] changes (e.g. after a swipe), the owner of the
+ * [value] will be notified to update their state to the new value of the [SwipeableState] by
+ * invoking [onValueChange]. If the owner does not update their state to the provided value for
+ * some reason, then the [SwipeableState] will perform a rollback to the previous, correct value.
+ */
+@Composable
+internal fun <T : Any> rememberSwipeableStateFor(
+ value: T,
+ onValueChange: (T) -> Unit,
+ animationSpec: AnimationSpec<Float> = AnimationSpec
+): SwipeableState<T> {
+ val swipeableState = remember {
+ SwipeableState(
+ initialValue = value,
+ animationSpec = animationSpec,
+ confirmStateChange = { true }
+ )
+ }
+ val forceAnimationCheck = remember { mutableStateOf(false) }
+ LaunchedEffect(value, forceAnimationCheck.value) {
+ if (value != swipeableState.currentValue) {
+ swipeableState.animateTo(value)
+ }
+ }
+ DisposableEffect(swipeableState.currentValue) {
+ if (value != swipeableState.currentValue) {
+ onValueChange(swipeableState.currentValue)
+ forceAnimationCheck.value = !forceAnimationCheck.value
+ }
+ onDispose {}
+ }
+ return swipeableState
+}
+
+/**
+ * Enable swipe gestures between a set of predefined states.
+ *
+ * To use this, you must provide a map of anchors (in pixels) to states (of type [T]). Note that
+ * this map cannot be empty and cannot have two anchors mapped to the same state.
+ *
+ * When a swipe is detected, the offset of the [SwipeableState] will be updated with the swipe
+ * delta. You should use this offset to move your content accordingly (see `Modifier.offsetPx`).
+ * When the swipe ends, the offset will be animated to one of the anchors and when that anchor is
+ * reached, the value of the [SwipeableState] will also be updated to the state corresponding to the
+ * new anchor. The target anchor is calculated based on the provided positional [thresholds].
+ *
+ * Swiping is constrained between the minimum and maximum anchors. If the user attempts to swipe
+ * past these bounds, a resistance effect will be applied by default. The amount of resistance at
+ * each edge is specified by the [resistance] config. To disable all resistance, set it to `null`.
+ *
+ * For an example of a [swipeable] with three states, see:
+ *
+ * @param T The type of the state.
+ * @param state The state of the [swipeable].
+ * @param anchors Pairs of anchors and states, used to map anchors to states and vice versa.
+ * @param thresholds Specifies where the thresholds between the states are. The thresholds will be
+ * used to determine which state to animate to when swiping stops. This is represented as a lambda
+ * that takes two states and returns the threshold between them in the form of a
+ * [ThresholdConfig]. Note that the order of the states corresponds to the swipe direction.
+ * @param orientation The orientation in which the [swipeable] can be swiped.
+ * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
+ * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom swipe
+ * will behave like bottom to top, and a left to right swipe will behave like right to left.
+ * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
+ * [Modifier.draggable].
+ * @param resistance Controls how much resistance will be applied when swiping past the bounds.
+ * @param velocityThreshold The threshold (in dp per second) that the end velocity has to exceed in
+ * order to animate to the next state, even if the positional [thresholds] have not been reached.
+ * @sample androidx.compose.material.samples.SwipeableSample
+ */
+fun <T> Modifier.swipeable(
+ state: SwipeableState<T>,
+ anchors: Map<Float, T>,
+ orientation: Orientation,
+ enabled: Boolean = true,
+ reverseDirection: Boolean = false,
+ interactionSource: MutableInteractionSource? = null,
+ thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
+ resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
+ velocityThreshold: Dp = VelocityThreshold
+) =
+ composed(
+ inspectorInfo =
+ debugInspectorInfo {
+ name = "swipeable"
+ properties["state"] = state
+ properties["anchors"] = anchors
+ properties["orientation"] = orientation
+ properties["enabled"] = enabled
+ properties["reverseDirection"] = reverseDirection
+ properties["interactionSource"] = interactionSource
+ properties["thresholds"] = thresholds
+ properties["resistance"] = resistance
+ properties["velocityThreshold"] = velocityThreshold
+ }
+ ) {
+ require(anchors.isNotEmpty()) { "You must have at least one anchor." }
+ require(anchors.values.distinct().count() == anchors.size) {
+ "You cannot have two anchors mapped to the same state."
+ }
+ val density = LocalDensity.current
+ state.ensureInit(anchors)
+ LaunchedEffect(anchors, state) {
+ val oldAnchors = state.anchors
+ state.anchors = anchors
+ state.resistance = resistance
+ state.thresholds = { a, b ->
+ val from = anchors.getValue(a)
+ val to = anchors.getValue(b)
+ with(thresholds(from, to)) { density.computeThreshold(a, b) }
+ }
+ with(density) { state.velocityThreshold = velocityThreshold.toPx() }
+ state.processNewAnchors(oldAnchors, anchors)
+ }
+
+ Modifier.draggable(
+ orientation = orientation,
+ enabled = enabled,
+ reverseDirection = reverseDirection,
+ interactionSource = interactionSource,
+ startDragImmediately = state.isAnimationRunning,
+ onDragStopped = { velocity -> launch { state.performFling(velocity) } },
+ state = state.draggableState
+ )
+ }
+
+/**
+ * Interface to compute a threshold between two anchors/states in a [swipeable].
+ *
+ * To define a [ThresholdConfig], consider using [FixedThreshold] and [FractionalThreshold].
+ */
+@Stable
+interface ThresholdConfig {
+ /** Compute the value of the threshold (in pixels), once the values of the anchors are known. */
+ fun Density.computeThreshold(fromValue: Float, toValue: Float): Float
+}
+
+/**
+ * A fixed threshold will be at an [offset] away from the first anchor.
+ *
+ * @param offset The offset (in dp) that the threshold will be at.
+ */
+@Immutable
+data class FixedThreshold(private val offset: Dp) : ThresholdConfig {
+ override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
+ return fromValue + offset.toPx() * sign(toValue - fromValue)
+ }
+}
+
+/**
+ * A fractional threshold will be at a [fraction] of the way between the two anchors.
+ *
+ * @param fraction The fraction (between 0 and 1) that the threshold will be at.
+ */
+@Immutable
+data class FractionalThreshold(
+ /*@FloatRange(from = 0.0, to = 1.0)*/
+ private val fraction: Float
+) : ThresholdConfig {
+ override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
+ return lerp(fromValue, toValue, fraction)
+ }
+}
+
+/**
+ * Specifies how resistance is calculated in [swipeable].
+ *
+ * There are two things needed to calculate resistance: the resistance basis determines how much
+ * overflow will be consumed to achieve maximum resistance, and the resistance factor determines the
+ * amount of resistance (the larger the resistance factor, the stronger the resistance).
+ *
+ * The resistance basis is usually either the size of the component which [swipeable] is applied to,
+ * or the distance between the minimum and maximum anchors. For a constructor in which the
+ * resistance basis defaults to the latter, consider using [resistanceConfig].
+ *
+ * You may specify different resistance factors for each bound. Consider using one of the default
+ * resistance factors in [SwipeableDefaults]: `StandardResistanceFactor` to convey that the user has
+ * run out of things to see, and `StiffResistanceFactor` to convey that the user cannot swipe this
+ * right now. Also, you can set either factor to 0 to disable resistance at that bound.
+ *
+ * @param basis Specifies the maximum amount of overflow that will be consumed. Must be positive.
+ * @param factorAtMin The factor by which to scale the resistance at the minimum bound. Must not be
+ * negative.
+ * @param factorAtMax The factor by which to scale the resistance at the maximum bound. Must not be
+ * negative.
+ */
+@Immutable
+class ResistanceConfig(
+ /*@FloatRange(from = 0.0, fromInclusive = false)*/
+ val basis: Float,
+ /*@FloatRange(from = 0.0)*/
+ val factorAtMin: Float = StandardResistanceFactor,
+ /*@FloatRange(from = 0.0)*/
+ val factorAtMax: Float = StandardResistanceFactor
+) {
+ fun computeResistance(overflow: Float): Float {
+ val factor = if (overflow < 0) factorAtMin else factorAtMax
+ if (factor == 0f) return 0f
+ val progress = (overflow / basis).coerceIn(-1f, 1f)
+ return basis / factor * sin(progress * PI.toFloat() / 2)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is ResistanceConfig) return false
+
+ if (basis != other.basis) return false
+ if (factorAtMin != other.factorAtMin) return false
+ if (factorAtMax != other.factorAtMax) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = basis.hashCode()
+ result = 31 * result + factorAtMin.hashCode()
+ result = 31 * result + factorAtMax.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "ResistanceConfig(basis=$basis, factorAtMin=$factorAtMin, factorAtMax=$factorAtMax)"
+ }
+}
+
+/**
+ * Given an offset x and a set of anchors, return a list of anchors:
+ * 1. [ ] if the set of anchors is empty,
+ * 2. [ x' ] if x is equal to one of the anchors, accounting for a small rounding error, where x' is
+ * x rounded to the exact value of the matching anchor,
+ * 3. [ min ] if min is the minimum anchor and x < min,
+ * 4. [ max ] if max is the maximum anchor and x > max, or
+ * 5. [ a , b ] if a and b are anchors such that a < x < b and b - a is minimal.
+ */
+private fun findBounds(offset: Float, anchors: Set<Float>): List<Float> {
+ // Find the anchors the target lies between with a little bit of rounding error.
+ val a = anchors.filter { it <= offset + 0.001 }.maxOrNull()
+ val b = anchors.filter { it >= offset - 0.001 }.minOrNull()
+
+ return when {
+ a == null ->
+ // case 1 or 3
+ listOfNotNull(b)
+ b == null ->
+ // case 4
+ listOf(a)
+ a == b ->
+ // case 2
+ // Can't return offset itself here since it might not be exactly equal
+ // to the anchor, despite being considered an exact match.
+ listOf(a)
+ else ->
+ // case 5
+ listOf(a, b)
+ }
+}
+
+private fun computeTarget(
+ offset: Float,
+ lastValue: Float,
+ anchors: Set<Float>,
+ thresholds: (Float, Float) -> Float,
+ velocity: Float,
+ velocityThreshold: Float
+): Float {
+ val bounds = findBounds(offset, anchors)
+ return when (bounds.size) {
+ 0 -> lastValue
+ 1 -> bounds[0]
+ else -> {
+ val lower = bounds[0]
+ val upper = bounds[1]
+ if (lastValue <= offset) {
+ // Swiping from lower to upper (positive).
+ if (velocity >= velocityThreshold) {
+ return upper
+ } else {
+ val threshold = thresholds(lower, upper)
+ if (offset < threshold) lower else upper
+ }
+ } else {
+ // Swiping from upper to lower (negative).
+ if (velocity <= -velocityThreshold) {
+ return lower
+ } else {
+ val threshold = thresholds(upper, lower)
+ if (offset > threshold) upper else lower
+ }
+ }
+ }
+ }
+}
+
+private fun <T> Map<Float, T>.getOffset(state: T): Float? {
+ return entries.firstOrNull { it.value == state }?.key
+}
+
+/** Contains useful defaults for [swipeable] and [SwipeableState]. */
+object SwipeableDefaults {
+ /** The default animation used by [SwipeableState]. */
+ val AnimationSpec = SpringSpec<Float>()
+
+ /** The default velocity threshold (1.8 dp per millisecond) used by [swipeable]. */
+ val VelocityThreshold = 125.dp
+
+ /** A stiff resistance factor which indicates that swiping isn't available right now. */
+ const val StiffResistanceFactor = 20f
+
+ /** A standard resistance factor which indicates that the user has run out of things to see. */
+ const val StandardResistanceFactor = 10f
+
+ /**
+ * The default resistance config used by [swipeable].
+ *
+ * This returns `null` if there is one anchor. If there are at least two anchors, it returns a
+ * [ResistanceConfig] with the resistance basis equal to the distance between the two bounds.
+ */
+ fun resistanceConfig(
+ anchors: Set<Float>,
+ factorAtMin: Float = StandardResistanceFactor,
+ factorAtMax: Float = StandardResistanceFactor
+ ): ResistanceConfig? {
+ return if (anchors.size <= 1) {
+ null
+ } else {
+ val basis = anchors.maxOrNull()!! - anchors.minOrNull()!!
+ ResistanceConfig(basis, factorAtMin, factorAtMax)
+ }
+ }
+}
+
+// temp default nested scroll connection for swipeables which desire as an opt in
+// revisit in b/174756744 as all types will have their own specific connection probably
+internal val <T> SwipeableState<T>.PreUpPostDownNestedScrollConnection: NestedScrollConnection
+ get() =
+ object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ val delta = available.toFloat()
+ return if (delta < 0 && source == NestedScrollSource.Drag) {
+ performDrag(delta).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ return if (source == NestedScrollSource.Drag) {
+ performDrag(available.toFloat()).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override suspend fun onPreFling(available: Velocity): Velocity {
+ val toFling = Offset(available.x, available.y).toFloat()
+ return if (toFling < 0 && offset.value > minBound) {
+ performFling(velocity = toFling)
+ // since we go to the anchor with tween settling, consume all for the best UX
+ available
+ } else {
+ Velocity.Zero
+ }
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ performFling(velocity = Offset(available.x, available.y).toFloat())
+ return available
+ }
+
+ private fun Float.toOffset(): Offset = Offset(0f, this)
+
+ private fun Offset.toFloat(): Float = this.y
+ }
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt
new file mode 100644
index 0000000..c1defb7
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.compose.ui.util
+
+import kotlin.math.roundToInt
+import kotlin.math.roundToLong
+
+// TODO(b/272311106): this is a fork from material. Unfork it when MathHelpers.kt reaches material3.
+
+/** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
+fun lerp(start: Float, stop: Float, fraction: Float): Float {
+ return (1 - fraction) * start + fraction * stop
+}
+
+/** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
+fun lerp(start: Int, stop: Int, fraction: Float): Int {
+ return start + ((stop - start) * fraction.toDouble()).roundToInt()
+}
+
+/** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
+fun lerp(start: Long, stop: Long, fraction: Float): Long {
+ return start + ((stop - start) * fraction.toDouble()).roundToLong()
+}
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index e253fb9..cc33745 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -21,8 +21,10 @@
import android.view.View
import androidx.activity.ComponentActivity
import androidx.lifecycle.LifecycleOwner
+import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import com.android.systemui.util.time.SystemClock
/** The Compose facade, when Compose is *not* available. */
object ComposeFacade : BaseComposeFacade {
@@ -48,6 +50,14 @@
throwComposeUnavailableError()
}
+ override fun createMultiShadeView(
+ context: Context,
+ viewModel: MultiShadeViewModel,
+ clock: SystemClock,
+ ): View {
+ throwComposeUnavailableError()
+ }
+
private fun throwComposeUnavailableError(): Nothing {
error(
"Compose is not available. Make sure to check isComposeAvailable() before calling any" +
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index 1ea18fe..0e79b18 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -23,10 +23,13 @@
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.LifecycleOwner
import com.android.compose.theme.PlatformTheme
+import com.android.systemui.multishade.ui.composable.MultiShade
+import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.compose.PeopleScreen
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.compose.FooterActions
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import com.android.systemui.util.time.SystemClock
/** The Compose facade, when Compose is available. */
object ComposeFacade : BaseComposeFacade {
@@ -51,4 +54,21 @@
setContent { PlatformTheme { FooterActions(viewModel, qsVisibilityLifecycleOwner) } }
}
}
+
+ override fun createMultiShadeView(
+ context: Context,
+ viewModel: MultiShadeViewModel,
+ clock: SystemClock,
+ ): View {
+ return ComposeView(context).apply {
+ setContent {
+ PlatformTheme {
+ MultiShade(
+ viewModel = viewModel,
+ clock = clock,
+ )
+ }
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt
new file mode 100644
index 0000000..b9e38cf
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.ui.composable
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.detectVerticalDragGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.unit.IntSize
+import com.android.systemui.R
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
+import com.android.systemui.notifications.ui.composable.Notifications
+import com.android.systemui.qs.footer.ui.compose.QuickSettings
+import com.android.systemui.statusbar.ui.composable.StatusBar
+import com.android.systemui.util.time.SystemClock
+
+@Composable
+fun MultiShade(
+ viewModel: MultiShadeViewModel,
+ clock: SystemClock,
+ modifier: Modifier = Modifier,
+) {
+ val isScrimEnabled: Boolean by viewModel.isScrimEnabled.collectAsState()
+
+ // TODO(b/273298030): find a different way to get the height constraint from its parent.
+ BoxWithConstraints(modifier = modifier) {
+ val maxHeightPx = with(LocalDensity.current) { maxHeight.toPx() }
+
+ Scrim(
+ modifier = Modifier.fillMaxSize(),
+ remoteTouch = viewModel::onScrimTouched,
+ alpha = { viewModel.scrimAlpha.value },
+ isScrimEnabled = isScrimEnabled,
+ )
+ Shade(
+ viewModel = viewModel.leftShade,
+ currentTimeMillis = clock::elapsedRealtime,
+ containerHeightPx = maxHeightPx,
+ modifier = Modifier.align(Alignment.TopStart),
+ ) {
+ Column {
+ StatusBar()
+ Notifications()
+ }
+ }
+ Shade(
+ viewModel = viewModel.rightShade,
+ currentTimeMillis = clock::elapsedRealtime,
+ containerHeightPx = maxHeightPx,
+ modifier = Modifier.align(Alignment.TopEnd),
+ ) {
+ Column {
+ StatusBar()
+ QuickSettings()
+ }
+ }
+ Shade(
+ viewModel = viewModel.singleShade,
+ currentTimeMillis = clock::elapsedRealtime,
+ containerHeightPx = maxHeightPx,
+ modifier = Modifier,
+ ) {
+ Column {
+ StatusBar()
+ Notifications()
+ QuickSettings()
+ }
+ }
+ }
+}
+
+@Composable
+private fun Scrim(
+ remoteTouch: (ProxiedInputModel) -> Unit,
+ alpha: () -> Float,
+ isScrimEnabled: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+
+ Box(
+ modifier =
+ modifier
+ .graphicsLayer { this.alpha = alpha() }
+ .background(colorResource(R.color.opaque_scrim))
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ .then(
+ if (isScrimEnabled) {
+ Modifier.pointerInput(Unit) {
+ detectTapGestures(onTap = { remoteTouch(ProxiedInputModel.OnTap) })
+ }
+ .pointerInput(Unit) {
+ detectVerticalDragGestures(
+ onVerticalDrag = { change, dragAmount ->
+ remoteTouch(
+ ProxiedInputModel.OnDrag(
+ xFraction = change.position.x / size.width,
+ yDragAmountPx = dragAmount,
+ )
+ )
+ },
+ onDragEnd = { remoteTouch(ProxiedInputModel.OnDragEnd) },
+ onDragCancel = { remoteTouch(ProxiedInputModel.OnDragCancel) }
+ )
+ }
+ } else {
+ Modifier
+ }
+ )
+ )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt
new file mode 100644
index 0000000..98ef57f9
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.ui.composable
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.interaction.DragInteraction
+import androidx.compose.foundation.interaction.InteractionSource
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.util.VelocityTracker
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.android.compose.modifiers.height
+import com.android.compose.swipeable.FixedThreshold
+import com.android.compose.swipeable.SwipeableState
+import com.android.compose.swipeable.ThresholdConfig
+import com.android.compose.swipeable.rememberSwipeableState
+import com.android.compose.swipeable.swipeable
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import com.android.systemui.multishade.ui.viewmodel.ShadeViewModel
+import kotlin.math.roundToInt
+import kotlinx.coroutines.launch
+
+/**
+ * Renders a shade (container and content).
+ *
+ * This should be allowed to grow to fill the width and height of its container.
+ *
+ * @param viewModel The view-model for this shade.
+ * @param currentTimeMillis A provider for the current time, in milliseconds.
+ * @param containerHeightPx The height of the container that this shade is being shown in, in
+ * pixels.
+ * @param modifier The Modifier.
+ * @param content The content of the shade.
+ */
+@Composable
+fun Shade(
+ viewModel: ShadeViewModel,
+ currentTimeMillis: () -> Long,
+ containerHeightPx: Float,
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit = {},
+) {
+ val isVisible: Boolean by viewModel.isVisible.collectAsState()
+ if (!isVisible) {
+ return
+ }
+
+ val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+ ReportNonProxiedInput(viewModel, interactionSource)
+
+ val swipeableState = rememberSwipeableState(initialValue = ShadeState.FullyCollapsed)
+ HandleForcedCollapse(viewModel, swipeableState)
+ HandleProxiedInput(viewModel, swipeableState, currentTimeMillis)
+ ReportShadeExpansion(viewModel, swipeableState, containerHeightPx)
+
+ val isSwipingEnabled: Boolean by viewModel.isSwipingEnabled.collectAsState()
+ val collapseThreshold: Float by viewModel.swipeCollapseThreshold.collectAsState()
+ val expandThreshold: Float by viewModel.swipeExpandThreshold.collectAsState()
+
+ val width: ShadeViewModel.Size by viewModel.width.collectAsState()
+ val density = LocalDensity.current
+
+ val anchors: Map<Float, ShadeState> =
+ remember(containerHeightPx) { swipeableAnchors(containerHeightPx) }
+
+ ShadeContent(
+ shadeHeightPx = { swipeableState.offset.value },
+ overstretch = { swipeableState.overflow.value / containerHeightPx },
+ isSwipingEnabled = isSwipingEnabled,
+ swipeableState = swipeableState,
+ interactionSource = interactionSource,
+ anchors = anchors,
+ thresholds = { _, to ->
+ swipeableThresholds(
+ to = to,
+ swipeCollapseThreshold = collapseThreshold.fractionToDp(density, containerHeightPx),
+ swipeExpandThreshold = expandThreshold.fractionToDp(density, containerHeightPx),
+ )
+ },
+ modifier = modifier.shadeWidth(width, density),
+ content = content,
+ )
+}
+
+/**
+ * Draws the content of the shade.
+ *
+ * @param shadeHeightPx Provider for the current expansion of the shade, in pixels, where `0` is
+ * fully collapsed.
+ * @param overstretch Provider for the current amount of vertical "overstretch" that the shade
+ * should be rendered with. This is `0` or a positive number that is a percentage of the total
+ * height of the shade when fully expanded. A value of `0` means that the shade is not stretched
+ * at all.
+ * @param isSwipingEnabled Whether swiping inside the shade is enabled or not.
+ * @param swipeableState The state to use for the [swipeable] modifier, allowing external control in
+ * addition to direct control (proxied user input in addition to non-proxied/direct user input).
+ * @param anchors A map of [ShadeState] keyed by the vertical position, in pixels, where that state
+ * occurs; this is used to configure the [swipeable] modifier.
+ * @param thresholds Function that returns the [ThresholdConfig] for going from one [ShadeState] to
+ * another. This controls how the [swipeable] decides which [ShadeState] to animate to once the
+ * user lets go of the shade; e.g. does it animate to fully collapsed or fully expanded.
+ * @param content The content to render inside the shade.
+ * @param modifier The [Modifier].
+ */
+@Composable
+private fun ShadeContent(
+ shadeHeightPx: () -> Float,
+ overstretch: () -> Float,
+ isSwipingEnabled: Boolean,
+ swipeableState: SwipeableState<ShadeState>,
+ interactionSource: MutableInteractionSource,
+ anchors: Map<Float, ShadeState>,
+ thresholds: (from: ShadeState, to: ShadeState) -> ThresholdConfig,
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit = {},
+) {
+ Surface(
+ shape = RoundedCornerShape(32.dp),
+ modifier =
+ modifier
+ .padding(12.dp)
+ .fillMaxWidth()
+ .height { shadeHeightPx().roundToInt() }
+ .graphicsLayer {
+ // Applies the vertical over-stretching of the shade content that may happen if
+ // the user keep dragging down when the shade is already fully-expanded.
+ transformOrigin = transformOrigin.copy(pivotFractionY = 0f)
+ this.scaleY = 1 + overstretch().coerceAtLeast(0f)
+ }
+ .swipeable(
+ enabled = isSwipingEnabled,
+ state = swipeableState,
+ interactionSource = interactionSource,
+ anchors = anchors,
+ thresholds = thresholds,
+ orientation = Orientation.Vertical,
+ ),
+ content = content,
+ )
+}
+
+/** Funnels current shade expansion values into the view-model. */
+@Composable
+private fun ReportShadeExpansion(
+ viewModel: ShadeViewModel,
+ swipeableState: SwipeableState<ShadeState>,
+ containerHeightPx: Float,
+) {
+ LaunchedEffect(swipeableState.offset, containerHeightPx) {
+ snapshotFlow { swipeableState.offset.value / containerHeightPx }
+ .collect { expansion -> viewModel.onExpansionChanged(expansion) }
+ }
+}
+
+/** Funnels drag gesture start and end events into the view-model. */
+@Composable
+private fun ReportNonProxiedInput(
+ viewModel: ShadeViewModel,
+ interactionSource: InteractionSource,
+) {
+ LaunchedEffect(interactionSource) {
+ interactionSource.interactions.collect {
+ when (it) {
+ is DragInteraction.Start -> {
+ viewModel.onDragStarted()
+ }
+ is DragInteraction.Stop -> {
+ viewModel.onDragEnded()
+ }
+ }
+ }
+ }
+}
+
+/** When told to force collapse, collapses the shade. */
+@Composable
+private fun HandleForcedCollapse(
+ viewModel: ShadeViewModel,
+ swipeableState: SwipeableState<ShadeState>,
+) {
+ LaunchedEffect(viewModel) {
+ viewModel.isForceCollapsed.collect {
+ launch { swipeableState.animateTo(ShadeState.FullyCollapsed) }
+ }
+ }
+}
+
+/**
+ * Handles proxied input (input originating outside of the UI of the shade) by driving the
+ * [SwipeableState] accordingly.
+ */
+@Composable
+private fun HandleProxiedInput(
+ viewModel: ShadeViewModel,
+ swipeableState: SwipeableState<ShadeState>,
+ currentTimeMillis: () -> Long,
+) {
+ val velocityTracker: VelocityTracker = remember { VelocityTracker() }
+ LaunchedEffect(viewModel) {
+ viewModel.proxiedInput.collect {
+ when (it) {
+ is ProxiedInputModel.OnDrag -> {
+ velocityTracker.addPosition(
+ timeMillis = currentTimeMillis.invoke(),
+ position = Offset(0f, it.yDragAmountPx),
+ )
+ swipeableState.performDrag(it.yDragAmountPx)
+ }
+ is ProxiedInputModel.OnDragEnd -> {
+ launch {
+ val velocity = velocityTracker.calculateVelocity().y
+ velocityTracker.resetTracking()
+ // We use a VelocityTracker to keep a record of how fast the pointer was
+ // moving such that we know how far to fling the shade when the gesture
+ // ends. Flinging the SwipeableState using performFling is required after
+ // one or more calls to performDrag such that the swipeable settles into one
+ // of the states. Without doing that, the shade would remain unmoving in an
+ // in-between state on the screen.
+ swipeableState.performFling(velocity)
+ }
+ }
+ is ProxiedInputModel.OnDragCancel -> {
+ launch {
+ velocityTracker.resetTracking()
+ swipeableState.animateTo(swipeableState.progress.from)
+ }
+ }
+ else -> Unit
+ }
+ }
+ }
+}
+
+/**
+ * Converts the [Float] (which is assumed to be a fraction between `0` and `1`) to a value in dp.
+ *
+ * @param density The [Density] of the display.
+ * @param wholePx The whole amount that the given [Float] is a fraction of.
+ * @return The dp size that's a fraction of the whole amount.
+ */
+private fun Float.fractionToDp(density: Density, wholePx: Float): Dp {
+ return with(density) { (this@fractionToDp * wholePx).toDp() }
+}
+
+private fun Modifier.shadeWidth(
+ size: ShadeViewModel.Size,
+ density: Density,
+): Modifier {
+ return then(
+ when (size) {
+ is ShadeViewModel.Size.Fraction -> Modifier.fillMaxWidth(size.fraction)
+ is ShadeViewModel.Size.Pixels -> Modifier.width(with(density) { size.pixels.toDp() })
+ }
+ )
+}
+
+/** Returns the pixel positions for each of the supported shade states. */
+private fun swipeableAnchors(containerHeightPx: Float): Map<Float, ShadeState> {
+ return mapOf(
+ 0f to ShadeState.FullyCollapsed,
+ containerHeightPx to ShadeState.FullyExpanded,
+ )
+}
+
+/**
+ * Returns the [ThresholdConfig] for how far the shade should be expanded or collapsed such that it
+ * actually completes the expansion or collapse after the user lifts their pointer.
+ */
+private fun swipeableThresholds(
+ to: ShadeState,
+ swipeExpandThreshold: Dp,
+ swipeCollapseThreshold: Dp,
+): ThresholdConfig {
+ return FixedThreshold(
+ when (to) {
+ ShadeState.FullyExpanded -> swipeExpandThreshold
+ ShadeState.FullyCollapsed -> swipeCollapseThreshold
+ }
+ )
+}
+
+/** Enumerates the shade UI states for [SwipeableState]. */
+private enum class ShadeState {
+ FullyCollapsed,
+ FullyExpanded,
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
new file mode 100644
index 0000000..ca91b8a
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.notifications.ui.composable
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun Notifications(
+ modifier: Modifier = Modifier,
+) {
+ // TODO(b/272779828): implement.
+ Column(
+ modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 300.dp).padding(4.dp),
+ ) {
+ Text("Notifications", modifier = Modifier.align(Alignment.CenterHorizontally))
+ Spacer(modifier = Modifier.weight(1f))
+ Text("Shelf", modifier = Modifier.align(Alignment.CenterHorizontally))
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt
new file mode 100644
index 0000000..665d6dd
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.qs.footer.ui.compose
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun QuickSettings(
+ modifier: Modifier = Modifier,
+) {
+ // TODO(b/272780058): implement.
+ Column(
+ modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 300.dp).padding(4.dp),
+ ) {
+ Text("Quick settings", modifier = Modifier.align(Alignment.CenterHorizontally))
+ Spacer(modifier = Modifier.weight(1f))
+ Text("QS footer actions", modifier = Modifier.align(Alignment.CenterHorizontally))
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/ui/composable/StatusBar.kt b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/ui/composable/StatusBar.kt
new file mode 100644
index 0000000..f514ab4
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/ui/composable/StatusBar.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.statusbar.ui.composable
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun StatusBar(
+ modifier: Modifier = Modifier,
+) {
+ // TODO(b/272780101): implement.
+ Row(
+ modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 48.dp).padding(4.dp),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text("Status bar")
+ }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index 1d28c63..c0b69c1 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -189,10 +189,12 @@
/** Get the text for secondaryLabel. */
public String getSecondaryLabel(String stateText) {
- if (TextUtils.isEmpty(secondaryLabel)) {
+ // Use a local reference as the value might change from other threads
+ CharSequence localSecondaryLabel = secondaryLabel;
+ if (TextUtils.isEmpty(localSecondaryLabel)) {
return stateText;
}
- return secondaryLabel.toString();
+ return localSecondaryLabel.toString();
}
public boolean copyTo(State other) {
diff --git a/packages/SystemUI/res/drawable/ic_important_outline.xml b/packages/SystemUI/res/drawable/ic_important_outline.xml
index 7a628bb..642582c 100644
--- a/packages/SystemUI/res/drawable/ic_important_outline.xml
+++ b/packages/SystemUI/res/drawable/ic_important_outline.xml
@@ -20,6 +20,7 @@
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
+ android:autoMirrored="true"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 311990c..e5cd0c5 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -844,4 +844,44 @@
<!-- Configuration to set Learn more in device logs as URL link -->
<bool name="log_access_confirmation_learn_more_as_link">true</bool>
+
+ <!-- [START] MULTI SHADE -->
+ <!-- Whether the device should use dual shade. If false, the device uses single shade. -->
+ <bool name="dual_shade_enabled">true</bool>
+ <!--
+ When in dual shade, where should the horizontal split be on the screen to help determine whether
+ the user is pulling down the left shade or the right shade. Must be between 0.0 and 1.0,
+ inclusive. In other words: how much of the left-hand side of the screen, when pulled down on,
+ would reveal the left-hand side shade.
+
+ More concretely:
+ A value of 0.67 means that the left two-thirds of the screen are dedicated to the left-hand side
+ shade and the remaining one-third of the screen on the right is dedicated to the right-hand side
+ shade.
+ -->
+ <dimen name="dual_shade_split_fraction">0.67</dimen>
+ <!-- Width of the left-hand side shade. -->
+ <dimen name="left_shade_width">436dp</dimen>
+ <!-- Width of the right-hand side shade. -->
+ <dimen name="right_shade_width">436dp</dimen>
+ <!--
+ Opaque version of the scrim that shows up behind dual shades. The alpha channel is driven
+ programmatically.
+ -->
+ <color name="opaque_scrim">#D9D9D9</color>
+ <!-- Maximum opacity when the scrim that shows up behind the dual shades is fully visible. -->
+ <dimen name="dual_shade_scrim_alpha">0.1</dimen>
+ <!--
+ The amount that the user must swipe down when the shade is fully collapsed to automatically
+ expand once the user lets go of the shade. If the user swipes less than this amount, the shade
+ will automatically revert back to fully collapsed once the user stops swiping.
+ -->
+ <dimen name="shade_swipe_expand_threshold">0.5</dimen>
+ <!--
+ The amount that the user must swipe up when the shade is fully expanded to automatically
+ collapse once the user lets go of the shade. If the user swipes less than this amount, the shade
+ will automatically revert back to fully expanded once the user stops swiping.
+ -->
+ <dimen name="shade_swipe_collapse_threshold">0.5</dimen>
+ <!-- [END] MULTI SHADE -->
</resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
index 6f7d66d..58e7747 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
@@ -321,7 +321,9 @@
} else {
// We are receiving new opening tasks, so convert to onTasksAppeared.
targets[i] = TransitionUtil.newTarget(change, layer, info, t, mLeashMap);
- t.reparent(targets[i].leash, mInfo.getRootLeash());
+ // reparent into the original `mInfo` since that's where we are animating.
+ final int rootIdx = TransitionUtil.rootIndexFor(change, mInfo);
+ t.reparent(targets[i].leash, mInfo.getRoot(rootIdx).getLeash());
t.setLayer(targets[i].leash, layer);
mOpeningTasks.add(new TaskState(change, targets[i].leash));
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java
index 0e5f8c1..553453d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java
@@ -18,13 +18,9 @@
import android.content.Context;
import android.content.res.TypedArray;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.SystemClock;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
-import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
@@ -33,22 +29,10 @@
import com.android.internal.policy.SystemBarUtils;
import com.android.systemui.R;
-import java.lang.ref.WeakReference;
-
/***
* Manages a number of views inside of the given layout. See below for a list of widgets.
*/
public abstract class KeyguardMessageArea extends TextView implements SecurityMessageDisplay {
- /** Handler token posted with accessibility announcement runnables. */
- private static final Object ANNOUNCE_TOKEN = new Object();
-
- /**
- * Delay before speaking an accessibility announcement. Used to prevent
- * lift-to-type from interrupting itself.
- */
- private static final long ANNOUNCEMENT_DELAY = 250;
-
- private final Handler mHandler;
private CharSequence mMessage;
private boolean mIsVisible;
@@ -65,7 +49,6 @@
super(context, attrs);
setLayerType(LAYER_TYPE_HARDWARE, null); // work around nested unclipped SaveLayer bug
- mHandler = new Handler(Looper.myLooper());
onThemeChanged();
}
@@ -127,9 +110,6 @@
private void securityMessageChanged(CharSequence message) {
mMessage = message;
update();
- mHandler.removeCallbacksAndMessages(ANNOUNCE_TOKEN);
- mHandler.postAtTime(new AnnounceRunnable(this, getText()), ANNOUNCE_TOKEN,
- (SystemClock.uptimeMillis() + ANNOUNCEMENT_DELAY));
}
private void clearMessage() {
@@ -156,25 +136,4 @@
/** Set the text color */
protected abstract void updateTextColor();
-
- /**
- * Runnable used to delay accessibility announcements.
- */
- private static class AnnounceRunnable implements Runnable {
- private final WeakReference<View> mHost;
- private final CharSequence mTextToAnnounce;
-
- AnnounceRunnable(View host, CharSequence textToAnnounce) {
- mHost = new WeakReference<View>(host);
- mTextToAnnounce = textToAnnounce;
- }
-
- @Override
- public void run() {
- final View host = mHost.get();
- if (host != null) {
- host.announceForAccessibility(mTextToAnnounce);
- }
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
index 6a92162..c1896fc 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
@@ -18,11 +18,17 @@
import android.content.res.ColorStateList;
import android.content.res.Configuration;
+import android.text.TextUtils;
+import android.view.View;
+
+import androidx.annotation.VisibleForTesting;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.util.ViewController;
+import java.lang.ref.WeakReference;
+
import javax.inject.Inject;
/**
@@ -31,8 +37,14 @@
*/
public class KeyguardMessageAreaController<T extends KeyguardMessageArea>
extends ViewController<T> {
+ /**
+ * Delay before speaking an accessibility announcement. Used to prevent
+ * lift-to-type from interrupting itself.
+ */
+ private static final long ANNOUNCEMENT_DELAY = 250;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private final ConfigurationController mConfigurationController;
+ private final AnnounceRunnable mAnnounceRunnable;
private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
public void onFinishedGoingToSleep(int why) {
@@ -68,6 +80,7 @@
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mConfigurationController = configurationController;
+ mAnnounceRunnable = new AnnounceRunnable(mView);
}
@Override
@@ -100,6 +113,12 @@
*/
public void setMessage(CharSequence s, boolean animate) {
mView.setMessage(s, animate);
+ CharSequence msg = mView.getText();
+ if (!TextUtils.isEmpty(msg)) {
+ mView.removeCallbacks(mAnnounceRunnable);
+ mAnnounceRunnable.setTextToAnnounce(msg);
+ mView.postDelayed(mAnnounceRunnable, ANNOUNCEMENT_DELAY);
+ }
}
public void setMessage(int resId) {
@@ -134,4 +153,30 @@
view, mKeyguardUpdateMonitor, mConfigurationController);
}
}
+
+ /**
+ * Runnable used to delay accessibility announcements.
+ */
+ @VisibleForTesting
+ public static class AnnounceRunnable implements Runnable {
+ private final WeakReference<View> mHost;
+ private CharSequence mTextToAnnounce;
+
+ AnnounceRunnable(View host) {
+ mHost = new WeakReference<>(host);
+ }
+
+ /** Sets the text to announce. */
+ public void setTextToAnnounce(CharSequence textToAnnounce) {
+ mTextToAnnounce = textToAnnounce;
+ }
+
+ @Override
+ public void run() {
+ final View host = mHost.get();
+ if (host != null) {
+ host.announceForAccessibility(mTextToAnnounce);
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 98866c6..7255383 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -1066,23 +1066,28 @@
}
private void reloadColors() {
- reinflateViewFlipper();
- mView.reloadColors();
+ reinflateViewFlipper(() -> mView.reloadColors());
}
/** Handles density or font scale changes. */
private void onDensityOrFontScaleChanged() {
- reinflateViewFlipper();
- mView.onDensityOrFontScaleChanged();
+ reinflateViewFlipper(() -> mView.onDensityOrFontScaleChanged());
}
/**
* Reinflate the view flipper child view.
*/
- public void reinflateViewFlipper() {
+ public void reinflateViewFlipper(
+ KeyguardSecurityViewFlipperController.OnViewInflatedListener onViewInflatedListener) {
mSecurityViewFlipperController.clearViews();
- mSecurityViewFlipperController.getSecurityView(mCurrentSecurityMode,
- mKeyguardSecurityCallback);
+ if (mFeatureFlags.isEnabled(Flags.ASYNC_INFLATE_BOUNCER)) {
+ mSecurityViewFlipperController.asynchronouslyInflateView(mCurrentSecurityMode,
+ mKeyguardSecurityCallback, onViewInflatedListener);
+ } else {
+ mSecurityViewFlipperController.getSecurityView(mCurrentSecurityMode,
+ mKeyguardSecurityCallback);
+ onViewInflatedListener.onViewInflated();
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
index 39b567f..68e1dd7 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
@@ -19,11 +19,16 @@
import android.util.Log;
import android.view.LayoutInflater;
+import androidx.annotation.Nullable;
+import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.KeyguardInputViewController.Factory;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.keyguard.dagger.KeyguardBouncerScope;
import com.android.systemui.R;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.util.ViewController;
import java.util.ArrayList;
@@ -44,18 +49,24 @@
private final List<KeyguardInputViewController<KeyguardInputView>> mChildren =
new ArrayList<>();
private final LayoutInflater mLayoutInflater;
+ private final AsyncLayoutInflater mAsyncLayoutInflater;
private final EmergencyButtonController.Factory mEmergencyButtonControllerFactory;
private final Factory mKeyguardSecurityViewControllerFactory;
+ private final FeatureFlags mFeatureFlags;
@Inject
protected KeyguardSecurityViewFlipperController(KeyguardSecurityViewFlipper view,
LayoutInflater layoutInflater,
+ AsyncLayoutInflater asyncLayoutInflater,
KeyguardInputViewController.Factory keyguardSecurityViewControllerFactory,
- EmergencyButtonController.Factory emergencyButtonControllerFactory) {
+ EmergencyButtonController.Factory emergencyButtonControllerFactory,
+ FeatureFlags featureFlags) {
super(view);
mKeyguardSecurityViewControllerFactory = keyguardSecurityViewControllerFactory;
mLayoutInflater = layoutInflater;
mEmergencyButtonControllerFactory = emergencyButtonControllerFactory;
+ mAsyncLayoutInflater = asyncLayoutInflater;
+ mFeatureFlags = featureFlags;
}
@Override
@@ -92,13 +103,12 @@
}
}
- if (childController == null
+ if (!mFeatureFlags.isEnabled(Flags.ASYNC_INFLATE_BOUNCER) && childController == null
&& securityMode != SecurityMode.None && securityMode != SecurityMode.Invalid) {
-
int layoutId = getLayoutIdFor(securityMode);
KeyguardInputView view = null;
if (layoutId != 0) {
- if (DEBUG) Log.v(TAG, "inflating id = " + layoutId);
+ if (DEBUG) Log.v(TAG, "inflating on main thread id = " + layoutId);
view = (KeyguardInputView) mLayoutInflater.inflate(
layoutId, mView, false);
mView.addView(view);
@@ -119,6 +129,36 @@
return childController;
}
+ /**
+ * Asynchronously inflate view and then add it to view flipper on the main thread when complete.
+ *
+ * OnInflateFinishedListener will be called on the main thread.
+ *
+ * @param securityMode
+ * @param keyguardSecurityCallback
+ */
+ public void asynchronouslyInflateView(SecurityMode securityMode,
+ KeyguardSecurityCallback keyguardSecurityCallback,
+ @Nullable OnViewInflatedListener onViewInflatedListener) {
+ int layoutId = getLayoutIdFor(securityMode);
+ if (layoutId != 0) {
+ if (DEBUG) Log.v(TAG, "inflating on bg thread id = " + layoutId);
+ mAsyncLayoutInflater.inflate(layoutId, mView,
+ (view, resId, parent) -> {
+ mView.addView(view);
+ KeyguardInputViewController<KeyguardInputView> childController =
+ mKeyguardSecurityViewControllerFactory.create(
+ (KeyguardInputView) view, securityMode,
+ keyguardSecurityCallback);
+ childController.init();
+ mChildren.add(childController);
+ if (onViewInflatedListener != null) {
+ onViewInflatedListener.onViewInflated();
+ }
+ });
+ }
+ }
+
private int getLayoutIdFor(SecurityMode securityMode) {
switch (securityMode) {
case Pattern: return R.layout.keyguard_pattern_view;
@@ -162,4 +202,10 @@
return 0;
}
}
+
+ /** Listener to when view has finished inflation. */
+ public interface OnViewInflatedListener {
+ /** Notifies that view has been inflated */
+ void onViewInflated();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags b/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags
index 9c847be..08236b7 100644
--- a/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags
+++ b/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags
@@ -61,3 +61,8 @@
## 4: SYSTEM_REGISTER_USER System sysui registers user's callbacks
## 5: SYSTEM_UNREGISTER_USER System sysui unregisters user's callbacks (after death)
36060 sysui_recents_connection (type|1),(user|1)
+
+# ---------------------------
+# KeyguardViewMediator.java
+# ---------------------------
+36080 sysui_keyguard (isOccluded|1),(animate|1)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index c31d45f..4aa985b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -198,32 +198,36 @@
final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- if (mCurrentDialog != null
- && Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
+ if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
String reason = intent.getStringExtra("reason");
reason = (reason != null) ? reason : "unknown";
- Log.d(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received, reason: " + reason);
-
- mCurrentDialog.dismissWithoutCallback(true /* animate */);
- mCurrentDialog = null;
-
- for (Callback cb : mCallbacks) {
- cb.onBiometricPromptDismissed();
- }
-
- try {
- if (mReceiver != null) {
- mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL,
- null /* credentialAttestation */);
- mReceiver = null;
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Remote exception", e);
- }
+ closeDioalog(reason);
}
}
};
+ private void closeDioalog(String reason) {
+ if (isShowing()) {
+ Log.i(TAG, "Close BP, reason :" + reason);
+ mCurrentDialog.dismissWithoutCallback(true /* animate */);
+ mCurrentDialog = null;
+
+ for (Callback cb : mCallbacks) {
+ cb.onBiometricPromptDismissed();
+ }
+
+ try {
+ if (mReceiver != null) {
+ mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL,
+ null /* credentialAttestation */);
+ mReceiver = null;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Remote exception", e);
+ }
+ }
+ }
+
private void cancelIfOwnerIsNotInForeground() {
mExecution.assertIsMainThread();
if (mCurrentDialog != null) {
@@ -546,6 +550,11 @@
}
}
+ @Override
+ public void handleShowGlobalActionsMenu() {
+ closeDioalog("PowerMenu shown");
+ }
+
/**
* @return where the UDFPS exists on the screen in pixels in portrait mode.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index c0f8549..4173bdc 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -21,8 +21,10 @@
import android.view.View
import androidx.activity.ComponentActivity
import androidx.lifecycle.LifecycleOwner
+import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import com.android.systemui.util.time.SystemClock
/**
* A facade to interact with Compose, when it is available.
@@ -57,4 +59,11 @@
viewModel: FooterActionsViewModel,
qsVisibilityLifecycleOwner: LifecycleOwner,
): View
+
+ /** Create a [View] to represent [viewModel] on screen. */
+ fun createMultiShadeView(
+ context: Context,
+ viewModel: MultiShadeViewModel,
+ clock: SystemClock,
+ ): View
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt
index 06d4a08..ce0f2e9 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt
@@ -155,17 +155,18 @@
d.show()
}
- private fun turnOnSettingSecurely(settings: List<String>) {
+ private fun turnOnSettingSecurely(settings: List<String>, onComplete: () -> Unit) {
val action =
ActivityStarter.OnDismissAction {
settings.forEach { setting ->
secureSettings.putIntForUser(setting, 1, userTracker.userId)
}
+ onComplete()
true
}
activityStarter.dismissKeyguardThenExecute(
action,
- /* cancel */ null,
+ /* cancel */ onComplete,
/* afterKeyguardGone */ true
)
}
@@ -186,7 +187,11 @@
if (!showDeviceControlsInLockscreen) {
settings.add(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS)
}
- turnOnSettingSecurely(settings)
+ // If we are toggling the flag, we want to call onComplete after the keyguard is
+ // dismissed (and the setting is turned on), to pass the correct value.
+ turnOnSettingSecurely(settings, onComplete)
+ } else {
+ onComplete()
}
if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) {
prefs
@@ -194,7 +199,6 @@
.putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG)
.apply()
}
- onComplete()
}
override fun onCancel(dialog: DialogInterface?) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index b054c7e..0be3bb6 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -98,6 +98,7 @@
import android.view.inputmethod.InputMethodManager;
import android.view.textclassifier.TextClassificationManager;
+import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
import androidx.core.app.NotificationManagerCompat;
import com.android.internal.app.IBatteryStats;
@@ -395,6 +396,13 @@
return LayoutInflater.from(context);
}
+ /** */
+ @Provides
+ @Singleton
+ public AsyncLayoutInflater provideAsyncLayoutInflater(Context context) {
+ return new AsyncLayoutInflater(context);
+ }
+
@Provides
static MediaProjectionManager provideMediaProjectionManager(Context context) {
return context.getSystemService(MediaProjectionManager.class);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 9374ad9..471c445 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -156,6 +156,7 @@
ComponentName lowLightDreamComponent,
DreamOverlayCallbackController dreamOverlayCallbackController,
@Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) {
+ super(executor);
mContext = context;
mExecutor = executor;
mWindowManager = windowManager;
@@ -202,55 +203,50 @@
@Override
public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
- mExecutor.execute(() -> {
- setCurrentStateLocked(Lifecycle.State.STARTED);
+ setCurrentStateLocked(Lifecycle.State.STARTED);
- mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
+ mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
- if (mDestroyed) {
- // The task could still be executed after the service has been destroyed. Bail if
- // that is the case.
- return;
- }
+ if (mDestroyed) {
+ // The task could still be executed after the service has been destroyed. Bail if
+ // that is the case.
+ return;
+ }
- if (mStarted) {
- // Reset the current dream overlay before starting a new one. This can happen
- // when two dreams overlap (briefly, for a smoother dream transition) and both
- // dreams are bound to the dream overlay service.
- resetCurrentDreamOverlayLocked();
- }
+ if (mStarted) {
+ // Reset the current dream overlay before starting a new one. This can happen
+ // when two dreams overlap (briefly, for a smoother dream transition) and both
+ // dreams are bound to the dream overlay service.
+ resetCurrentDreamOverlayLocked();
+ }
- mDreamOverlayContainerViewController =
- mDreamOverlayComponent.getDreamOverlayContainerViewController();
- mDreamOverlayTouchMonitor = mDreamOverlayComponent.getDreamOverlayTouchMonitor();
- mDreamOverlayTouchMonitor.init();
+ mDreamOverlayContainerViewController =
+ mDreamOverlayComponent.getDreamOverlayContainerViewController();
+ mDreamOverlayTouchMonitor = mDreamOverlayComponent.getDreamOverlayTouchMonitor();
+ mDreamOverlayTouchMonitor.init();
- mStateController.setShouldShowComplications(shouldShowComplications());
+ mStateController.setShouldShowComplications(shouldShowComplications());
- // If we are not able to add the overlay window, reset the overlay.
- if (!addOverlayWindowLocked(layoutParams)) {
- resetCurrentDreamOverlayLocked();
- return;
- }
+ // If we are not able to add the overlay window, reset the overlay.
+ if (!addOverlayWindowLocked(layoutParams)) {
+ resetCurrentDreamOverlayLocked();
+ return;
+ }
+ setCurrentStateLocked(Lifecycle.State.RESUMED);
+ mStateController.setOverlayActive(true);
+ final ComponentName dreamComponent = getDreamComponent();
+ mStateController.setLowLightActive(
+ dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
+ mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START);
- setCurrentStateLocked(Lifecycle.State.RESUMED);
- mStateController.setOverlayActive(true);
- final ComponentName dreamComponent = getDreamComponent();
- mStateController.setLowLightActive(
- dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
- mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START);
-
- mDreamOverlayCallbackController.onStartDream();
- mStarted = true;
- });
+ mDreamOverlayCallbackController.onStartDream();
+ mStarted = true;
}
@Override
public void onEndDream() {
- mExecutor.execute(() -> {
- resetCurrentDreamOverlayLocked();
- });
+ resetCurrentDreamOverlayLocked();
}
private Lifecycle.State getCurrentStateLocked() {
@@ -263,12 +259,10 @@
@Override
public void onWakeUp(@NonNull Runnable onCompletedCallback) {
- mExecutor.execute(() -> {
- if (mDreamOverlayContainerViewController != null) {
- mDreamOverlayCallbackController.onWakeUp();
- mDreamOverlayContainerViewController.wakeUp(onCompletedCallback, mExecutor);
- }
- });
+ if (mDreamOverlayContainerViewController != null) {
+ mDreamOverlayCallbackController.onWakeUp();
+ mDreamOverlayContainerViewController.wakeUp(onCompletedCallback, mExecutor);
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java
index 244212b..1702eac 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java
@@ -75,6 +75,10 @@
Settings.Secure.SCREENSAVER_COMPLICATIONS_ENABLED,
settingsObserver,
UserHandle.myUserId());
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED,
+ settingsObserver,
+ UserHandle.myUserId());
settingsObserver.onChange(false);
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
index 9b954f5f..6808142 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
@@ -51,6 +51,7 @@
int DREAM_MEDIA_COMPLICATION_WEIGHT = 0;
int DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT = 4;
int DREAM_MEDIA_ENTRY_COMPLICATION_WEIGHT = 3;
+ int DREAM_WEATHER_COMPLICATION_WEIGHT = 0;
/**
* Provides layout parameters for the clock time complication.
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt
index 63f63a5..78e132f 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt
@@ -30,14 +30,15 @@
import com.android.systemui.plugins.BcSmartspaceDataPlugin
import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
+import com.android.systemui.plugins.BcSmartspaceDataPlugin.UI_SURFACE_DREAM
import com.android.systemui.smartspace.SmartspacePrecondition
import com.android.systemui.smartspace.SmartspaceTargetFilter
+import com.android.systemui.smartspace.dagger.SmartspaceModule
import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_DATA_PLUGIN
import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_PRECONDITION
import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_TARGET_FILTER
import com.android.systemui.smartspace.dagger.SmartspaceViewComponent
import com.android.systemui.util.concurrency.Execution
-import java.lang.RuntimeException
import java.util.Optional
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -56,13 +57,16 @@
@Named(DREAM_SMARTSPACE_PRECONDITION) private val precondition: SmartspacePrecondition,
@Named(DREAM_SMARTSPACE_TARGET_FILTER)
private val optionalTargetFilter: Optional<SmartspaceTargetFilter>,
- @Named(DREAM_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional<BcSmartspaceDataPlugin>
+ @Named(DREAM_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional<BcSmartspaceDataPlugin>,
+ @Named(SmartspaceModule.WEATHER_SMARTSPACE_DATA_PLUGIN)
+ optionalWeatherPlugin: Optional<BcSmartspaceDataPlugin>,
) {
companion object {
private const val TAG = "DreamSmartspaceCtrlr"
}
private var session: SmartspaceSession? = null
+ private val weatherPlugin: BcSmartspaceDataPlugin? = optionalWeatherPlugin.orElse(null)
private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null)
private var targetFilter: SmartspaceTargetFilter? = optionalTargetFilter.orElse(null)
@@ -116,31 +120,54 @@
private val sessionListener = SmartspaceSession.OnTargetsAvailableListener { targets ->
execution.assertIsMainThread()
+ // The weather data plugin takes unfiltered targets and performs the filtering internally.
+ weatherPlugin?.onTargetsAvailable(targets)
+
onTargetsAvailableUnfiltered(targets)
val filteredTargets = targets.filter { targetFilter?.filterSmartspaceTarget(it) ?: true }
plugin?.onTargetsAvailable(filteredTargets)
}
/**
+ * Constructs the weather view with custom layout and connects it to the weather plugin.
+ */
+ fun buildAndConnectWeatherView(parent: ViewGroup, customView: View?): View? {
+ return buildAndConnectViewWithPlugin(parent, weatherPlugin, customView)
+ }
+
+ /**
* Constructs the smartspace view and connects it to the smartspace service.
*/
fun buildAndConnectView(parent: ViewGroup): View? {
+ return buildAndConnectViewWithPlugin(parent, plugin, null)
+ }
+
+ private fun buildAndConnectViewWithPlugin(
+ parent: ViewGroup,
+ smartspaceDataPlugin: BcSmartspaceDataPlugin?,
+ customView: View?
+ ): View? {
execution.assertIsMainThread()
if (!precondition.conditionsMet()) {
throw RuntimeException("Cannot build view when not enabled")
}
- val view = buildView(parent)
+ val view = buildView(parent, smartspaceDataPlugin, customView)
connectSession()
return view
}
- private fun buildView(parent: ViewGroup): View? {
- return if (plugin != null) {
- var view = smartspaceViewComponentFactory.create(parent, plugin, stateChangeListener)
+ private fun buildView(
+ parent: ViewGroup,
+ smartspaceDataPlugin: BcSmartspaceDataPlugin?,
+ customView: View?
+ ): View? {
+ return if (smartspaceDataPlugin != null) {
+ val view = smartspaceViewComponentFactory.create(parent, smartspaceDataPlugin,
+ stateChangeListener, customView)
.getView()
if (view !is View) {
return null
@@ -157,7 +184,10 @@
}
private fun connectSession() {
- if (plugin == null || session != null || !hasActiveSessionListeners()) {
+ if (plugin == null && weatherPlugin == null) {
+ return
+ }
+ if (session != null || !hasActiveSessionListeners()) {
return
}
@@ -166,13 +196,14 @@
}
val newSession = smartspaceManager.createSmartspaceSession(
- SmartspaceConfig.Builder(context, "dream").build()
+ SmartspaceConfig.Builder(context, UI_SURFACE_DREAM).build()
)
Log.d(TAG, "Starting smartspace session for dream")
newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener)
this.session = newSession
- plugin.registerSmartspaceEventNotifier {
+ weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
+ plugin?.registerSmartspaceEventNotifier {
e ->
session?.notifySmartspaceEvent(e)
}
@@ -199,22 +230,47 @@
session = null
+ weatherPlugin?.registerSmartspaceEventNotifier(null)
+ weatherPlugin?.onTargetsAvailable(emptyList())
+
plugin?.registerSmartspaceEventNotifier(null)
plugin?.onTargetsAvailable(emptyList())
Log.d(TAG, "Ending smartspace session for dream")
}
fun addListener(listener: SmartspaceTargetListener) {
+ addAndRegisterListener(listener, plugin)
+ }
+
+ fun removeListener(listener: SmartspaceTargetListener) {
+ removeAndUnregisterListener(listener, plugin)
+ }
+
+ fun addListenerForWeatherPlugin(listener: SmartspaceTargetListener) {
+ addAndRegisterListener(listener, weatherPlugin)
+ }
+
+ fun removeListenerForWeatherPlugin(listener: SmartspaceTargetListener) {
+ removeAndUnregisterListener(listener, weatherPlugin)
+ }
+
+ private fun addAndRegisterListener(
+ listener: SmartspaceTargetListener,
+ smartspaceDataPlugin: BcSmartspaceDataPlugin?
+ ) {
execution.assertIsMainThread()
- plugin?.registerListener(listener)
+ smartspaceDataPlugin?.registerListener(listener)
listeners.add(listener)
connectSession()
}
- fun removeListener(listener: SmartspaceTargetListener) {
+ private fun removeAndUnregisterListener(
+ listener: SmartspaceTargetListener,
+ smartspaceDataPlugin: BcSmartspaceDataPlugin?
+ ) {
execution.assertIsMainThread()
- plugin?.unregisterListener(listener)
+ smartspaceDataPlugin?.unregisterListener(listener)
listeners.remove(listener)
disconnect()
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index b611f46..661b2b6 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -244,6 +244,11 @@
@JvmField
val HIDE_SMARTSPACE_ON_DREAM_OVERLAY = unreleasedFlag(404, "hide_smartspace_on_dream_overlay")
+ // TODO(b/271460958): Tracking Bug
+ @JvmField
+ val SHOW_WEATHER_COMPLICATION_ON_DREAM_OVERLAY = unreleasedFlag(405,
+ "show_weather_complication_on_dream_overlay")
+
// 500 - quick settings
val PEOPLE_TILE = resourceBooleanFlag(502, R.bool.flag_conversations, "people_tile")
@@ -591,8 +596,7 @@
// 1700 - clipboard
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
// TODO(b/267162944): Tracking bug
- @JvmField
- val CLIPBOARD_MINIMIZED_LAYOUT = unreleasedFlag(1702, "clipboard_data_model", teamfood = true)
+ @JvmField val CLIPBOARD_MINIMIZED_LAYOUT = releasedFlag(1702, "clipboard_data_model")
// 1800 - shade container
@JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 377a136..5ecc00f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -118,6 +118,7 @@
import com.android.systemui.CoreStartable;
import com.android.systemui.DejankUtils;
import com.android.systemui.Dumpable;
+import com.android.systemui.EventLogTags;
import com.android.systemui.R;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.Interpolators;
@@ -1849,6 +1850,8 @@
private void handleSetOccluded(boolean isOccluded, boolean animate) {
Trace.beginSection("KeyguardViewMediator#handleSetOccluded");
Log.d(TAG, "handleSetOccluded(" + isOccluded + ")");
+ EventLogTags.writeSysuiKeyguard(isOccluded ? 1 : 0, animate ? 1 : 0);
+
mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_TRANSITION_FROM_AOD);
synchronized (KeyguardViewMediator.this) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
index 450fa14..82be009 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
@@ -176,10 +176,10 @@
return;
}
- final Intent credential = getKeyguardManager()
+ final Intent confirmCredentialIntent = getKeyguardManager()
.createConfirmDeviceCredentialIntent(null, null, getTargetUserId(),
true /* disallowBiometricsIfPolicyExists */);
- if (credential == null) {
+ if (confirmCredentialIntent == null) {
return;
}
@@ -193,14 +193,18 @@
PendingIntent.FLAG_IMMUTABLE, options.toBundle());
if (target != null) {
- credential.putExtra(Intent.EXTRA_INTENT, target.getIntentSender());
+ confirmCredentialIntent.putExtra(Intent.EXTRA_INTENT, target.getIntentSender());
}
+ // WorkLockActivity is started as a task overlay, so unless credential confirmation is also
+ // started as an overlay, it won't be visible.
final ActivityOptions launchOptions = ActivityOptions.makeBasic();
launchOptions.setLaunchTaskId(getTaskId());
launchOptions.setTaskOverlay(true /* taskOverlay */, true /* canResume */);
+ // Propagate it in case more than one activity is launched.
+ confirmCredentialIntent.putExtra(KeyguardManager.EXTRA_FORCE_TASK_OVERLAY, true);
- startActivityForResult(credential, REQUEST_CODE_CONFIRM_CREDENTIALS,
+ startActivityForResult(confirmCredentialIntent, REQUEST_CODE_CONFIRM_CREDENTIALS,
launchOptions.toBundle());
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
index d716784..5fcf105 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
@@ -34,7 +34,6 @@
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.ActivityStarter
import kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
@@ -112,16 +111,18 @@
launch {
viewModel.show.collect {
// Reset Security Container entirely.
- view.visibility = View.VISIBLE
- securityContainerController.onBouncerVisibilityChanged(
- /* isVisible= */ true
- )
- securityContainerController.reinflateViewFlipper()
- securityContainerController.showPrimarySecurityScreen(
- /* turningOff= */ false
- )
- securityContainerController.appear()
- securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
+ securityContainerController.reinflateViewFlipper {
+ // Reset Security Container entirely.
+ view.visibility = View.VISIBLE
+ securityContainerController.onBouncerVisibilityChanged(
+ /* isVisible= */ true
+ )
+ securityContainerController.showPrimarySecurityScreen(
+ /* turningOff= */ false
+ )
+ securityContainerController.appear()
+ securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
index 50722d5..6d95882 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
@@ -26,18 +26,21 @@
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.shared.quickaffordance.shared.model.KeyguardQuickAffordancePreviewConstants
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
@SysUISingleton
class KeyguardRemotePreviewManager
@Inject
constructor(
private val previewRendererFactory: KeyguardPreviewRendererFactory,
+ @Application private val applicationScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
@Background private val backgroundHandler: Handler,
) {
@@ -55,7 +58,13 @@
// Destroy any previous renderer associated with this token.
activePreviews[renderer.hostToken]?.let { destroyObserver(it) }
- observer = PreviewLifecycleObserver(renderer, mainDispatcher, ::destroyObserver)
+ observer =
+ PreviewLifecycleObserver(
+ renderer,
+ applicationScope,
+ mainDispatcher,
+ ::destroyObserver,
+ )
activePreviews[renderer.hostToken] = observer
renderer.render()
renderer.hostToken?.linkToDeath(observer, 0)
@@ -92,13 +101,18 @@
private class PreviewLifecycleObserver(
private val renderer: KeyguardPreviewRenderer,
+ private val scope: CoroutineScope,
private val mainDispatcher: CoroutineDispatcher,
private val requestDestruction: (PreviewLifecycleObserver) -> Unit,
) : Handler.Callback, IBinder.DeathRecipient {
- private var isDestroyed = false
+ private var isDestroyedOrDestroying = false
override fun handleMessage(message: Message): Boolean {
+ if (isDestroyedOrDestroying) {
+ return true
+ }
+
when (message.what) {
KeyguardQuickAffordancePreviewConstants.MESSAGE_ID_SLOT_SELECTED -> {
message.data
@@ -118,14 +132,14 @@
}
fun onDestroy(): IBinder? {
- if (isDestroyed) {
+ if (isDestroyedOrDestroying) {
return null
}
- isDestroyed = true
+ isDestroyedOrDestroying = true
val hostToken = renderer.hostToken
hostToken?.unlinkToDeath(this, 0)
- runBlocking(mainDispatcher) { renderer.destroy() }
+ scope.launch(mainDispatcher) { renderer.destroy() }
return hostToken
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
index 68910c6..0656c9b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -70,7 +70,7 @@
/** Observe whether we should update fps is showing. */
val shouldUpdateSideFps: Flow<Unit> =
merge(
- interactor.startingToHide,
+ interactor.hide,
interactor.show,
interactor.startingDisappearAnimation.filterNotNull().map {}
)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index a31c1e5..00e5aac 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -519,16 +519,15 @@
mLogger.logTapContentView(mUid, mPackageName, mInstanceId);
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
- // See StatusBarNotificationActivityStarter#onNotificationClicked
boolean showOverLockscreen = mKeyguardStateController.isShowing()
- && mActivityIntentHelper.wouldShowOverLockscreen(clickIntent.getIntent(),
+ && mActivityIntentHelper.wouldPendingShowOverLockscreen(clickIntent,
mLockscreenUserManager.getCurrentUserId());
-
if (showOverLockscreen) {
- mActivityStarter.startActivity(clickIntent.getIntent(),
- /* dismissShade */ true,
- /* animationController */ null,
- /* showOverLockscreenWhenLocked */ true);
+ try {
+ clickIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "Pending intent for " + key + " was cancelled");
+ }
} else {
mActivityStarter.postStartActivityDismissingKeyguard(clickIntent,
buildLaunchAnimatorController(mMediaViewHolder.getPlayer()));
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index 00e9a79..b71a9193 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -16,17 +16,16 @@
package com.android.systemui.media.dialog;
-import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_GO_TO_APP;
-import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_NONE;
-import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_TRANSFER;
import static android.media.RouteListingPreference.Item.SUBTEXT_AD_ROUTING_DISALLOWED;
import static android.media.RouteListingPreference.Item.SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED;
import static android.media.RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQUIRED;
+import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP;
+import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE;
+import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
+
import android.content.Context;
import android.content.res.ColorStateList;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
@@ -296,6 +295,8 @@
&& mController.isAdvancedLayoutSupported()) {
//If device is connected and there's other selectable devices, layout as
// one of selected devices.
+ updateTitleIcon(R.drawable.media_output_icon_volume,
+ mController.getColorItemContent());
boolean isDeviceDeselectable = isDeviceIncluded(
mController.getDeselectableMediaDevice(), device);
updateGroupableCheckBox(true, isDeviceDeselectable, device);
@@ -371,7 +372,8 @@
mEndClickIcon.setOnClickListener(null);
mEndTouchArea.setOnClickListener(null);
updateEndClickAreaColor(mController.getColorSeekbarProgress());
- mEndClickIcon.setColorFilter(mController.getColorItemContent());
+ mEndClickIcon.setImageTintList(
+ ColorStateList.valueOf(mController.getColorItemContent()));
mEndClickIcon.setOnClickListener(
v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v));
mEndTouchArea.setOnClickListener(v -> mCheckBox.performClick());
@@ -379,8 +381,8 @@
public void updateEndClickAreaColor(int color) {
if (mController.isAdvancedLayoutSupported()) {
- mEndTouchArea.getBackground().setColorFilter(
- new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
+ mEndTouchArea.setBackgroundTintList(
+ ColorStateList.valueOf(color));
}
}
@@ -394,22 +396,22 @@
private void updateConnectionFailedStatusIcon() {
mStatusIcon.setImageDrawable(
mContext.getDrawable(R.drawable.media_output_status_failed));
- mStatusIcon.setColorFilter(mController.getColorItemContent());
+ mStatusIcon.setImageTintList(
+ ColorStateList.valueOf(mController.getColorItemContent()));
}
private void updateDeviceStatusIcon(Drawable drawable) {
mStatusIcon.setImageDrawable(drawable);
- mStatusIcon.setColorFilter(mController.getColorItemContent());
+ mStatusIcon.setImageTintList(
+ ColorStateList.valueOf(mController.getColorItemContent()));
if (drawable instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) drawable).start();
}
}
private void updateProgressBarColor() {
- mProgressBar.getIndeterminateDrawable().setColorFilter(
- new PorterDuffColorFilter(
- mController.getColorItemContent(),
- PorterDuff.Mode.SRC_IN));
+ mProgressBar.getIndeterminateDrawable().setTintList(
+ ColorStateList.valueOf(mController.getColorItemContent()));
}
public void updateEndClickArea(MediaDevice device, boolean isDeviceDeselectable) {
@@ -419,9 +421,8 @@
mEndTouchArea.setImportantForAccessibility(
View.IMPORTANT_FOR_ACCESSIBILITY_YES);
if (mController.isAdvancedLayoutSupported()) {
- mEndTouchArea.getBackground().setColorFilter(
- new PorterDuffColorFilter(mController.getColorItemBackground(),
- PorterDuff.Mode.SRC_IN));
+ mEndTouchArea.setBackgroundTintList(
+ ColorStateList.valueOf(mController.getColorItemBackground()));
}
setUpContentDescriptionForView(mEndTouchArea, true, device);
}
@@ -450,11 +451,11 @@
setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new));
final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
mTitleIcon.setImageDrawable(addDrawable);
- mTitleIcon.setColorFilter(mController.getColorItemContent());
+ mTitleIcon.setImageTintList(
+ ColorStateList.valueOf(mController.getColorItemContent()));
if (mController.isAdvancedLayoutSupported()) {
- mIconAreaLayout.getBackground().setColorFilter(
- new PorterDuffColorFilter(mController.getColorItemBackground(),
- PorterDuff.Mode.SRC_IN));
+ mIconAreaLayout.setBackgroundTintList(
+ ColorStateList.valueOf(mController.getColorItemBackground()));
}
mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 2a2cf63..f76f049 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -23,8 +23,7 @@
import android.annotation.DrawableRes;
import android.app.WallpaperColors;
import android.content.Context;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
+import android.content.res.ColorStateList;
import android.graphics.Typeface;
import android.graphics.drawable.ClipDrawable;
import android.graphics.drawable.Drawable;
@@ -196,9 +195,8 @@
mIconAreaLayout.setOnClickListener(null);
mVolumeValueText.setTextColor(mController.getColorItemContent());
}
- mSeekBar.getProgressDrawable().setColorFilter(
- new PorterDuffColorFilter(mController.getColorSeekbarProgress(),
- PorterDuff.Mode.SRC_IN));
+ mSeekBar.setProgressTintList(
+ ColorStateList.valueOf(mController.getColorSeekbarProgress()));
}
abstract void onBind(int customizedItem);
@@ -224,16 +222,14 @@
updateSeekbarProgressBackground();
}
}
- mItemLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
- isActive ? mController.getColorConnectedItemBackground()
- : mController.getColorItemBackground(),
- PorterDuff.Mode.SRC_IN));
+ mItemLayout.setBackgroundTintList(
+ ColorStateList.valueOf(isActive ? mController.getColorConnectedItemBackground()
+ : mController.getColorItemBackground()));
if (mController.isAdvancedLayoutSupported()) {
- mIconAreaLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
- showSeekBar ? mController.getColorSeekbarProgress()
+ mIconAreaLayout.setBackgroundTintList(
+ ColorStateList.valueOf(showSeekBar ? mController.getColorSeekbarProgress()
: showProgressBar ? mController.getColorConnectedItemBackground()
- : mController.getColorItemBackground(),
- PorterDuff.Mode.SRC_IN));
+ : mController.getColorItemBackground()));
}
mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
mSeekBar.setAlpha(1);
@@ -251,7 +247,8 @@
params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable()
: mController.getItemMarginEndDefault();
}
- mTitleIcon.setColorFilter(mController.getColorItemContent());
+ mTitleIcon.setBackgroundTintList(
+ ColorStateList.valueOf(mController.getColorItemContent()));
}
void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
@@ -274,15 +271,14 @@
backgroundDrawable = mContext.getDrawable(
showSeekBar ? R.drawable.media_output_item_background_active
: R.drawable.media_output_item_background).mutate();
- backgroundDrawable.setColorFilter(new PorterDuffColorFilter(
+ backgroundDrawable.setTint(
showSeekBar ? mController.getColorConnectedItemBackground()
- : mController.getColorItemBackground(), PorterDuff.Mode.SRC_IN));
- mIconAreaLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
- showProgressBar || isFakeActive
+ : mController.getColorItemBackground());
+ mIconAreaLayout.setBackgroundTintList(
+ ColorStateList.valueOf(showProgressBar || isFakeActive
? mController.getColorConnectedItemBackground()
: showSeekBar ? mController.getColorSeekbarProgress()
- : mController.getColorItemBackground(),
- PorterDuff.Mode.SRC_IN));
+ : mController.getColorItemBackground()));
if (showSeekBar) {
updateSeekbarProgressBackground();
}
@@ -297,9 +293,7 @@
backgroundDrawable = mContext.getDrawable(
R.drawable.media_output_item_background)
.mutate();
- backgroundDrawable.setColorFilter(new PorterDuffColorFilter(
- mController.getColorItemBackground(),
- PorterDuff.Mode.SRC_IN));
+ backgroundDrawable.setTint(mController.getColorItemBackground());
}
mItemLayout.setBackground(backgroundDrawable);
mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
@@ -442,11 +436,10 @@
void updateTitleIcon(@DrawableRes int id, int color) {
mTitleIcon.setImageDrawable(mContext.getDrawable(id));
- mTitleIcon.setColorFilter(color);
+ mTitleIcon.setImageTintList(ColorStateList.valueOf(color));
if (mController.isAdvancedLayoutSupported()) {
- mIconAreaLayout.getBackground().setColorFilter(
- new PorterDuffColorFilter(mController.getColorSeekbarProgress(),
- PorterDuff.Mode.SRC_IN));
+ mIconAreaLayout.setBackgroundTintList(
+ ColorStateList.valueOf(mController.getColorSeekbarProgress()));
}
}
@@ -462,9 +455,7 @@
final Drawable backgroundDrawable = mContext.getDrawable(
R.drawable.media_output_item_background_active)
.mutate();
- backgroundDrawable.setColorFilter(
- new PorterDuffColorFilter(mController.getColorConnectedItemBackground(),
- PorterDuff.Mode.SRC_IN));
+ backgroundDrawable.setTint(mController.getColorConnectedItemBackground());
mItemLayout.setBackground(backgroundDrawable);
}
@@ -539,10 +530,8 @@
Drawable getSpeakerDrawable() {
final Drawable drawable = mContext.getDrawable(R.drawable.ic_speaker_group_black_24dp)
.mutate();
- drawable.setColorFilter(
- new PorterDuffColorFilter(Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_item_main_content),
- PorterDuff.Mode.SRC_IN));
+ drawable.setTint(Utils.getColorStateListDefaultColor(mContext,
+ R.color.media_dialog_item_main_content));
return drawable;
}
@@ -574,7 +563,9 @@
return;
}
mTitleIcon.setImageIcon(icon);
- mTitleIcon.setColorFilter(mController.getColorItemContent());
+ icon.setTint(mController.getColorItemContent());
+ mTitleIcon.setImageTintList(
+ ColorStateList.valueOf(mController.getColorItemContent()));
});
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
index 2250d72..39d4e6e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
@@ -80,6 +80,10 @@
Log.d(TAG, "logOutputSuccess - selected device: " + selectedDeviceType);
}
+ if (mSourceDevice == null && mTargetDevice == null) {
+ return;
+ }
+
updateLoggingDeviceCount(deviceList);
SysUiStatsLog.write(
@@ -105,6 +109,10 @@
Log.d(TAG, "logOutputSuccess - selected device: " + selectedDeviceType);
}
+ if (mSourceDevice == null && mTargetDevice == null) {
+ return;
+ }
+
updateLoggingMediaItemCount(deviceItemList);
SysUiStatsLog.write(
@@ -176,6 +184,10 @@
Log.e(TAG, "logRequestFailed - " + reason);
}
+ if (mSourceDevice == null && mTargetDevice == null) {
+ return;
+ }
+
updateLoggingDeviceCount(deviceList);
SysUiStatsLog.write(
@@ -201,6 +213,10 @@
Log.e(TAG, "logRequestFailed - " + reason);
}
+ if (mSourceDevice == null && mTargetDevice == null) {
+ return;
+ }
+
updateLoggingMediaItemCount(deviceItemList);
SysUiStatsLog.write(
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt
new file mode 100644
index 0000000..c48028c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.data.model
+
+import com.android.systemui.multishade.shared.model.ShadeId
+
+/** Models the current interaction with one of the shades. */
+data class MultiShadeInteractionModel(
+ /** The ID of the shade that the user is currently interacting with. */
+ val shadeId: ShadeId,
+ /** Whether the interaction is proxied (as in: coming from an external app or different UI). */
+ val isProxied: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt
new file mode 100644
index 0000000..86f0c0d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.data.remoteproxy
+
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+
+/**
+ * Acts as a hub for routing proxied user input into the multi shade system.
+ *
+ * "Proxied" user input is coming through a proxy; typically from an external app or different UI.
+ * In other words: it's not user input that's occurring directly on the shade UI itself. This class
+ * is that proxy.
+ */
+@Singleton
+class MultiShadeInputProxy @Inject constructor() {
+ private val _proxiedTouch =
+ MutableSharedFlow<ProxiedInputModel>(
+ replay = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST,
+ )
+ val proxiedInput: Flow<ProxiedInputModel> = _proxiedTouch.asSharedFlow()
+
+ fun onProxiedInput(proxiedInput: ProxiedInputModel) {
+ _proxiedTouch.tryEmit(proxiedInput)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt
new file mode 100644
index 0000000..1172030
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.data.repository
+
+import android.content.Context
+import androidx.annotation.FloatRange
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
+import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import com.android.systemui.multishade.shared.model.ShadeConfig
+import com.android.systemui.multishade.shared.model.ShadeId
+import com.android.systemui.multishade.shared.model.ShadeModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Encapsulates application state for all shades. */
+@SysUISingleton
+class MultiShadeRepository
+@Inject
+constructor(
+ @Application private val applicationContext: Context,
+ inputProxy: MultiShadeInputProxy,
+) {
+ /**
+ * Remote input coming from sources outside of system UI (for example, swiping down on the
+ * Launcher or from the status bar).
+ */
+ val proxiedInput: Flow<ProxiedInputModel> = inputProxy.proxiedInput
+
+ /** Width of the left-hand side shade, in pixels. */
+ private val leftShadeWidthPx =
+ applicationContext.resources.getDimensionPixelSize(R.dimen.left_shade_width)
+
+ /** Width of the right-hand side shade, in pixels. */
+ private val rightShadeWidthPx =
+ applicationContext.resources.getDimensionPixelSize(R.dimen.right_shade_width)
+
+ /**
+ * The amount that the user must swipe up when the shade is fully expanded to automatically
+ * collapse once the user lets go of the shade. If the user swipes less than this amount, the
+ * shade will automatically revert back to fully expanded once the user stops swiping.
+ *
+ * This is a fraction between `0` and `1`.
+ */
+ private val swipeCollapseThreshold =
+ checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_collapse_threshold))
+
+ /**
+ * The amount that the user must swipe down when the shade is fully collapsed to automatically
+ * expand once the user lets go of the shade. If the user swipes less than this amount, the
+ * shade will automatically revert back to fully collapsed once the user stops swiping.
+ *
+ * This is a fraction between `0` and `1`.
+ */
+ private val swipeExpandThreshold =
+ checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_expand_threshold))
+
+ /**
+ * Maximum opacity when the scrim that shows up behind the dual shades is fully visible.
+ *
+ * This is a fraction between `0` and `1`.
+ */
+ private val dualShadeScrimAlpha =
+ checkInBounds(applicationContext.resources.getFloat(R.dimen.dual_shade_scrim_alpha))
+
+ /** The current configuration of the shade system. */
+ val shadeConfig: StateFlow<ShadeConfig> =
+ MutableStateFlow(
+ if (applicationContext.resources.getBoolean(R.bool.dual_shade_enabled)) {
+ ShadeConfig.DualShadeConfig(
+ leftShadeWidthPx = leftShadeWidthPx,
+ rightShadeWidthPx = rightShadeWidthPx,
+ swipeCollapseThreshold = swipeCollapseThreshold,
+ swipeExpandThreshold = swipeExpandThreshold,
+ splitFraction =
+ applicationContext.resources.getFloat(
+ R.dimen.dual_shade_split_fraction
+ ),
+ scrimAlpha = dualShadeScrimAlpha,
+ )
+ } else {
+ ShadeConfig.SingleShadeConfig(
+ swipeCollapseThreshold = swipeCollapseThreshold,
+ swipeExpandThreshold = swipeExpandThreshold,
+ )
+ }
+ )
+ .asStateFlow()
+
+ private val _forceCollapseAll = MutableStateFlow(false)
+ /** Whether all shades should be collapsed. */
+ val forceCollapseAll: StateFlow<Boolean> = _forceCollapseAll.asStateFlow()
+
+ private val _shadeInteraction = MutableStateFlow<MultiShadeInteractionModel?>(null)
+ /** The current shade interaction or `null` if no shade is interacted with currently. */
+ val shadeInteraction: StateFlow<MultiShadeInteractionModel?> = _shadeInteraction.asStateFlow()
+
+ private val stateByShade = mutableMapOf<ShadeId, MutableStateFlow<ShadeModel>>()
+
+ /** The model for the shade with the given ID. */
+ fun getShade(
+ shadeId: ShadeId,
+ ): StateFlow<ShadeModel> {
+ return getMutableShade(shadeId).asStateFlow()
+ }
+
+ /** Sets the expansion amount for the shade with the given ID. */
+ fun setExpansion(
+ shadeId: ShadeId,
+ @FloatRange(from = 0.0, to = 1.0) expansion: Float,
+ ) {
+ getMutableShade(shadeId).let { mutableState ->
+ mutableState.value = mutableState.value.copy(expansion = expansion)
+ }
+ }
+
+ /** Sets whether all shades should be immediately forced to collapse. */
+ fun setForceCollapseAll(isForced: Boolean) {
+ _forceCollapseAll.value = isForced
+ }
+
+ /** Sets the current shade interaction; use `null` if no shade is interacted with currently. */
+ fun setShadeInteraction(shadeInteraction: MultiShadeInteractionModel?) {
+ _shadeInteraction.value = shadeInteraction
+ }
+
+ private fun getMutableShade(id: ShadeId): MutableStateFlow<ShadeModel> {
+ return stateByShade.getOrPut(id) { MutableStateFlow(ShadeModel(id)) }
+ }
+
+ /** Asserts that the given [Float] is in the range of `0` and `1`, inclusive. */
+ private fun checkInBounds(float: Float): Float {
+ check(float in 0f..1f) { "$float isn't between 0 and 1." }
+ return float
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt
new file mode 100644
index 0000000..b9f6d83
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.domain.interactor
+
+import androidx.annotation.FloatRange
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
+import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
+import com.android.systemui.multishade.data.repository.MultiShadeRepository
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import com.android.systemui.multishade.shared.model.ShadeConfig
+import com.android.systemui.multishade.shared.model.ShadeId
+import com.android.systemui.multishade.shared.model.ShadeModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.yield
+
+/** Encapsulates business logic related to interactions with the multi-shade system. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MultiShadeInteractor
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ private val repository: MultiShadeRepository,
+ private val inputProxy: MultiShadeInputProxy,
+) {
+ /** The current configuration of the shade system. */
+ val shadeConfig: StateFlow<ShadeConfig> = repository.shadeConfig
+
+ /** The expansion of the shade that's most expanded. */
+ val maxShadeExpansion: Flow<Float> =
+ repository.shadeConfig.flatMapLatest { shadeConfig ->
+ combine(allShades(shadeConfig)) { shadeModels ->
+ shadeModels.maxOfOrNull { it.expansion } ?: 0f
+ }
+ }
+
+ /**
+ * A _processed_ version of the proxied input flow.
+ *
+ * All internal dependencies on the proxied input flow *must* use this one for two reasons:
+ * 1. It's a [SharedFlow] so we only do the upstream work once, no matter how many usages we
+ * actually have.
+ * 2. It actually does some preprocessing as the proxied input events stream through, handling
+ * common things like recording the current state of the system based on incoming input
+ * events.
+ */
+ private val processedProxiedInput: SharedFlow<ProxiedInputModel> =
+ combine(
+ repository.shadeConfig,
+ repository.proxiedInput.distinctUntilChanged(),
+ ::Pair,
+ )
+ .map { (shadeConfig, proxiedInput) ->
+ if (proxiedInput !is ProxiedInputModel.OnTap) {
+ // If the user is interacting with any other gesture type (for instance,
+ // dragging),
+ // we no longer want to force collapse all shades.
+ repository.setForceCollapseAll(false)
+ }
+
+ when (proxiedInput) {
+ is ProxiedInputModel.OnDrag -> {
+ val affectedShadeId = affectedShadeId(shadeConfig, proxiedInput.xFraction)
+ // This might be the start of a new drag gesture, let's update our
+ // application
+ // state to record that fact.
+ onUserInteractionStarted(
+ shadeId = affectedShadeId,
+ isProxied = true,
+ )
+ }
+ is ProxiedInputModel.OnTap -> {
+ // Tapping outside any shade collapses all shades. This code path is not hit
+ // for
+ // taps that happen _inside_ a shade as that input event is directly applied
+ // through the UI and is, hence, not a proxied input.
+ collapseAll()
+ }
+ else -> Unit
+ }
+
+ proxiedInput
+ }
+ .shareIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ replay = 1,
+ )
+
+ /** Whether the shade with the given ID should be visible. */
+ fun isVisible(shadeId: ShadeId): Flow<Boolean> {
+ return repository.shadeConfig.map { shadeConfig -> shadeConfig.shadeIds.contains(shadeId) }
+ }
+
+ /** Whether direct user input is allowed on the shade with the given ID. */
+ fun isNonProxiedInputAllowed(shadeId: ShadeId): Flow<Boolean> {
+ return combine(
+ isForceCollapsed(shadeId),
+ repository.shadeInteraction,
+ ::Pair,
+ )
+ .map { (isForceCollapsed, shadeInteraction) ->
+ !isForceCollapsed && shadeInteraction?.isProxied != true
+ }
+ }
+
+ /** Whether the shade with the given ID is forced to collapse. */
+ fun isForceCollapsed(shadeId: ShadeId): Flow<Boolean> {
+ return combine(
+ repository.forceCollapseAll,
+ repository.shadeInteraction.map { it?.shadeId },
+ ::Pair,
+ )
+ .map { (collapseAll, userInteractedShadeIdOrNull) ->
+ val counterpartShadeIdOrNull =
+ when (shadeId) {
+ ShadeId.SINGLE -> null
+ ShadeId.LEFT -> ShadeId.RIGHT
+ ShadeId.RIGHT -> ShadeId.LEFT
+ }
+
+ when {
+ // If all shades have been told to collapse (by a tap outside, for example),
+ // then this shade is collapsed.
+ collapseAll -> true
+ // A shade that doesn't have a counterpart shade cannot be force-collapsed by
+ // interactions on the counterpart shade.
+ counterpartShadeIdOrNull == null -> false
+ // If the current user interaction is on the counterpart shade, then this shade
+ // should be force-collapsed.
+ else -> userInteractedShadeIdOrNull == counterpartShadeIdOrNull
+ }
+ }
+ }
+
+ /**
+ * Proxied input affecting the shade with the given ID. This is input coming from sources
+ * outside of system UI (for example, swiping down on the Launcher or from the status bar) or
+ * outside the UI of any shade (for example, the scrim that's shown behind the shades).
+ */
+ fun proxiedInput(shadeId: ShadeId): Flow<ProxiedInputModel?> {
+ return combine(
+ processedProxiedInput,
+ isForceCollapsed(shadeId).distinctUntilChanged(),
+ repository.shadeInteraction,
+ ::Triple,
+ )
+ .map { (proxiedInput, isForceCollapsed, shadeInteraction) ->
+ when {
+ // If the shade is force-collapsed, we ignored proxied input on it.
+ isForceCollapsed -> null
+ // If the proxied input does not belong to this shade, ignore it.
+ shadeInteraction?.shadeId != shadeId -> null
+ // If there is ongoing non-proxied user input on any shade, ignore the
+ // proxied input.
+ !shadeInteraction.isProxied -> null
+ // Otherwise, send the proxied input downstream.
+ else -> proxiedInput
+ }
+ }
+ .onEach { proxiedInput ->
+ // We use yield() to make sure that the following block of code happens _after_
+ // downstream collectors had a chance to process the proxied input. Otherwise, we
+ // might change our state to clear the current UserInteraction _before_ those
+ // downstream collectors get a chance to process the proxied input, which will make
+ // them ignore it (since they ignore proxied input when the current user interaction
+ // doesn't match their shade).
+ yield()
+
+ if (
+ proxiedInput is ProxiedInputModel.OnDragEnd ||
+ proxiedInput is ProxiedInputModel.OnDragCancel
+ ) {
+ onUserInteractionEnded(shadeId = shadeId, isProxied = true)
+ }
+ }
+ }
+
+ /** Sets the expansion amount for the shade with the given ID. */
+ fun setExpansion(
+ shadeId: ShadeId,
+ @FloatRange(from = 0.0, to = 1.0) expansion: Float,
+ ) {
+ repository.setExpansion(shadeId, expansion)
+ }
+
+ /** Collapses all shades. */
+ fun collapseAll() {
+ repository.setForceCollapseAll(true)
+ }
+
+ /**
+ * Notifies that a new non-proxied interaction may have started. Safe to call multiple times for
+ * the same interaction as it won't overwrite an existing interaction.
+ *
+ * Existing interactions can be cleared by calling [onUserInteractionEnded].
+ */
+ fun onUserInteractionStarted(shadeId: ShadeId) {
+ onUserInteractionStarted(
+ shadeId = shadeId,
+ isProxied = false,
+ )
+ }
+
+ /**
+ * Notifies that the current non-proxied interaction has ended.
+ *
+ * Safe to call multiple times, even if there's no current interaction or even if the current
+ * interaction doesn't belong to the given shade or is proxied as the code is a no-op unless
+ * there's a match between the parameters and the current interaction.
+ */
+ fun onUserInteractionEnded(
+ shadeId: ShadeId,
+ ) {
+ onUserInteractionEnded(
+ shadeId = shadeId,
+ isProxied = false,
+ )
+ }
+
+ fun sendProxiedInput(proxiedInput: ProxiedInputModel) {
+ inputProxy.onProxiedInput(proxiedInput)
+ }
+
+ /**
+ * Notifies that a new interaction may have started. Safe to call multiple times for the same
+ * interaction as it won't overwrite an existing interaction.
+ *
+ * Existing interactions can be cleared by calling [onUserInteractionEnded].
+ */
+ private fun onUserInteractionStarted(
+ shadeId: ShadeId,
+ isProxied: Boolean,
+ ) {
+ if (repository.shadeInteraction.value != null) {
+ return
+ }
+
+ repository.setShadeInteraction(
+ MultiShadeInteractionModel(
+ shadeId = shadeId,
+ isProxied = isProxied,
+ )
+ )
+ }
+
+ /**
+ * Notifies that the current interaction has ended.
+ *
+ * Safe to call multiple times, even if there's no current interaction or even if the current
+ * interaction doesn't belong to the given shade or [isProxied] value as the code is a no-op
+ * unless there's a match between the parameters and the current interaction.
+ */
+ private fun onUserInteractionEnded(
+ shadeId: ShadeId,
+ isProxied: Boolean,
+ ) {
+ repository.shadeInteraction.value?.let { (interactionShadeId, isInteractionProxied) ->
+ if (shadeId == interactionShadeId && isProxied == isInteractionProxied) {
+ repository.setShadeInteraction(null)
+ }
+ }
+ }
+
+ /**
+ * Returns the ID of the shade that's affected by user input at a given coordinate.
+ *
+ * @param config The shade configuration being used.
+ * @param xFraction The horizontal position of the user input as a fraction along the width of
+ * its container where `0` is all the way to the left and `1` is all the way to the right.
+ */
+ private fun affectedShadeId(
+ config: ShadeConfig,
+ @FloatRange(from = 0.0, to = 1.0) xFraction: Float,
+ ): ShadeId {
+ return if (config is ShadeConfig.DualShadeConfig) {
+ if (xFraction <= config.splitFraction) {
+ ShadeId.LEFT
+ } else {
+ ShadeId.RIGHT
+ }
+ } else {
+ ShadeId.SINGLE
+ }
+ }
+
+ /** Returns the list of flows of all the shades in the given configuration. */
+ private fun allShades(
+ config: ShadeConfig,
+ ): List<Flow<ShadeModel>> {
+ return config.shadeIds.map { shadeId -> repository.getShade(shadeId) }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt
new file mode 100644
index 0000000..ee1dd65
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.shared.model
+
+import androidx.annotation.FloatRange
+
+/**
+ * Models a part of an ongoing proxied user input gesture.
+ *
+ * "Proxied" user input is coming through a proxy; typically from an external app or different UI.
+ * In other words: it's not user input that's occurring directly on the shade UI itself.
+ */
+sealed class ProxiedInputModel {
+ /** The user is dragging their pointer. */
+ data class OnDrag(
+ /**
+ * The relative position of the pointer as a fraction of its container width where `0` is
+ * all the way to the left and `1` is all the way to the right.
+ */
+ @FloatRange(from = 0.0, to = 1.0) val xFraction: Float,
+ /** The amount that the pointer was dragged, in pixels. */
+ val yDragAmountPx: Float,
+ ) : ProxiedInputModel()
+
+ /** The user finished dragging by lifting up their pointer. */
+ object OnDragEnd : ProxiedInputModel()
+
+ /**
+ * The drag gesture has been canceled. Usually because the pointer exited the draggable area.
+ */
+ object OnDragCancel : ProxiedInputModel()
+
+ /** The user has tapped (clicked). */
+ object OnTap : ProxiedInputModel()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt
new file mode 100644
index 0000000..a4cd35c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.shared.model
+
+import androidx.annotation.FloatRange
+
+/** Enumerates the various possible configurations of the shade system. */
+sealed class ShadeConfig(
+
+ /** IDs of the shade(s) in this configuration. */
+ open val shadeIds: List<ShadeId>,
+
+ /**
+ * The amount that the user must swipe up when the shade is fully expanded to automatically
+ * collapse once the user lets go of the shade. If the user swipes less than this amount, the
+ * shade will automatically revert back to fully expanded once the user stops swiping.
+ */
+ @FloatRange(from = 0.0, to = 1.0) open val swipeCollapseThreshold: Float,
+
+ /**
+ * The amount that the user must swipe down when the shade is fully collapsed to automatically
+ * expand once the user lets go of the shade. If the user swipes less than this amount, the
+ * shade will automatically revert back to fully collapsed once the user stops swiping.
+ */
+ @FloatRange(from = 0.0, to = 1.0) open val swipeExpandThreshold: Float,
+) {
+
+ /** There is a single shade. */
+ data class SingleShadeConfig(
+ @FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float,
+ @FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float,
+ ) :
+ ShadeConfig(
+ shadeIds = listOf(ShadeId.SINGLE),
+ swipeCollapseThreshold = swipeCollapseThreshold,
+ swipeExpandThreshold = swipeExpandThreshold,
+ )
+
+ /** There are two shades arranged side-by-side. */
+ data class DualShadeConfig(
+ /** Width of the left-hand side shade. */
+ val leftShadeWidthPx: Int,
+ /** Width of the right-hand side shade. */
+ val rightShadeWidthPx: Int,
+ @FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float,
+ @FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float,
+ /**
+ * The position of the "split" between interaction areas for each of the shades, as a
+ * fraction of the width of the container.
+ *
+ * Interactions that occur on the start-side (left-hand side in left-to-right languages like
+ * English) affect the start-side shade. Interactions that occur on the end-side (right-hand
+ * side in left-to-right languages like English) affect the end-side shade.
+ */
+ @FloatRange(from = 0.0, to = 1.0) val splitFraction: Float,
+ /** Maximum opacity when the scrim that shows up behind the dual shades is fully visible. */
+ @FloatRange(from = 0.0, to = 1.0) val scrimAlpha: Float,
+ ) :
+ ShadeConfig(
+ shadeIds = listOf(ShadeId.LEFT, ShadeId.RIGHT),
+ swipeCollapseThreshold = swipeCollapseThreshold,
+ swipeExpandThreshold = swipeExpandThreshold,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt
new file mode 100644
index 0000000..9e02657
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.shared.model
+
+/** Enumerates all known shade IDs. */
+enum class ShadeId {
+ /** ID of the shade on the left in dual shade configurations. */
+ LEFT,
+ /** ID of the shade on the right in dual shade configurations. */
+ RIGHT,
+ /** ID of the single shade in single shade configurations. */
+ SINGLE,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt
new file mode 100644
index 0000000..49ac64c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.shared.model
+
+import androidx.annotation.FloatRange
+
+/** Models the current state of a shade. */
+data class ShadeModel(
+ val id: ShadeId,
+ @FloatRange(from = 0.0, to = 1.0) val expansion: Float = 0f,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt
new file mode 100644
index 0000000..ce6ab97
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.ui.viewmodel
+
+import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import com.android.systemui.multishade.shared.model.ShadeConfig
+import com.android.systemui.multishade.shared.model.ShadeId
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Models UI state for UI that supports multi (or single) shade. */
+@OptIn(ExperimentalCoroutinesApi::class)
+class MultiShadeViewModel(
+ viewModelScope: CoroutineScope,
+ private val interactor: MultiShadeInteractor,
+) {
+ /** Models UI state for the single shade. */
+ val singleShade =
+ ShadeViewModel(
+ viewModelScope,
+ ShadeId.SINGLE,
+ interactor,
+ )
+
+ /** Models UI state for the shade on the left-hand side. */
+ val leftShade =
+ ShadeViewModel(
+ viewModelScope,
+ ShadeId.LEFT,
+ interactor,
+ )
+
+ /** Models UI state for the shade on the right-hand side. */
+ val rightShade =
+ ShadeViewModel(
+ viewModelScope,
+ ShadeId.RIGHT,
+ interactor,
+ )
+
+ /** The amount of alpha that the scrim should have. This is a value between `0` and `1`. */
+ val scrimAlpha: StateFlow<Float> =
+ combine(
+ interactor.maxShadeExpansion,
+ interactor.shadeConfig
+ .map { it as? ShadeConfig.DualShadeConfig }
+ .map { dualShadeConfigOrNull -> dualShadeConfigOrNull?.scrimAlpha ?: 0f },
+ ::Pair,
+ )
+ .map { (anyShadeExpansion, scrimAlpha) ->
+ (anyShadeExpansion * scrimAlpha).coerceIn(0f, 1f)
+ }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = 0f,
+ )
+
+ /** Whether the scrim should accept touch events. */
+ val isScrimEnabled: StateFlow<Boolean> =
+ interactor.shadeConfig
+ .flatMapLatest { shadeConfig ->
+ when (shadeConfig) {
+ // In the dual shade configuration, the scrim is enabled when the expansion is
+ // greater than zero on any one of the shades.
+ is ShadeConfig.DualShadeConfig ->
+ interactor.maxShadeExpansion
+ .map { expansion -> expansion > 0 }
+ .distinctUntilChanged()
+ // No scrim in the single shade configuration.
+ is ShadeConfig.SingleShadeConfig -> flowOf(false)
+ }
+ }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
+
+ /** Notifies that the scrim has been touched. */
+ fun onScrimTouched(proxiedInput: ProxiedInputModel) {
+ if (!isScrimEnabled.value) {
+ return
+ }
+
+ interactor.sendProxiedInput(proxiedInput)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt
new file mode 100644
index 0000000..e828dbd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.ui.viewmodel
+
+import androidx.annotation.FloatRange
+import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import com.android.systemui.multishade.shared.model.ShadeConfig
+import com.android.systemui.multishade.shared.model.ShadeId
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Models UI state for a single shade. */
+class ShadeViewModel(
+ viewModelScope: CoroutineScope,
+ private val shadeId: ShadeId,
+ private val interactor: MultiShadeInteractor,
+) {
+ /** Whether the shade is visible. */
+ val isVisible: StateFlow<Boolean> =
+ interactor
+ .isVisible(shadeId)
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
+
+ /** Whether swiping on the shade UI is currently enabled. */
+ val isSwipingEnabled: StateFlow<Boolean> =
+ interactor
+ .isNonProxiedInputAllowed(shadeId)
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
+
+ /** Whether the shade must be collapsed immediately. */
+ val isForceCollapsed: Flow<Boolean> =
+ interactor.isForceCollapsed(shadeId).distinctUntilChanged()
+
+ /** The width of the shade. */
+ val width: StateFlow<Size> =
+ interactor.shadeConfig
+ .map { shadeWidth(it) }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = shadeWidth(interactor.shadeConfig.value),
+ )
+
+ /**
+ * The amount that the user must swipe up when the shade is fully expanded to automatically
+ * collapse once the user lets go of the shade. If the user swipes less than this amount, the
+ * shade will automatically revert back to fully expanded once the user stops swiping.
+ */
+ val swipeCollapseThreshold: StateFlow<Float> =
+ interactor.shadeConfig
+ .map { it.swipeCollapseThreshold }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = interactor.shadeConfig.value.swipeCollapseThreshold,
+ )
+
+ /**
+ * The amount that the user must swipe down when the shade is fully collapsed to automatically
+ * expand once the user lets go of the shade. If the user swipes less than this amount, the
+ * shade will automatically revert back to fully collapsed once the user stops swiping.
+ */
+ val swipeExpandThreshold: StateFlow<Float> =
+ interactor.shadeConfig
+ .map { it.swipeExpandThreshold }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = interactor.shadeConfig.value.swipeExpandThreshold,
+ )
+
+ /**
+ * Proxied input affecting the shade. This is input coming from sources outside of system UI
+ * (for example, swiping down on the Launcher or from the status bar) or outside the UI of any
+ * shade (for example, the scrim that's shown behind the shades).
+ */
+ val proxiedInput: Flow<ProxiedInputModel?> =
+ interactor
+ .proxiedInput(shadeId)
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = null,
+ )
+
+ /** Notifies that the expansion amount for the shade has changed. */
+ fun onExpansionChanged(
+ expansion: Float,
+ ) {
+ interactor.setExpansion(shadeId, expansion.coerceIn(0f, 1f))
+ }
+
+ /** Notifies that a drag gesture has started. */
+ fun onDragStarted() {
+ interactor.onUserInteractionStarted(shadeId)
+ }
+
+ /** Notifies that a drag gesture has ended. */
+ fun onDragEnded() {
+ interactor.onUserInteractionEnded(shadeId = shadeId)
+ }
+
+ private fun shadeWidth(shadeConfig: ShadeConfig): Size {
+ return when (shadeId) {
+ ShadeId.LEFT ->
+ Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.leftShadeWidthPx ?: 0)
+ ShadeId.RIGHT ->
+ Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.rightShadeWidthPx ?: 0)
+ ShadeId.SINGLE -> Size.Fraction(1f)
+ }
+ }
+
+ sealed class Size {
+ data class Fraction(
+ @FloatRange(from = 0.0, to = 1.0) val fraction: Float,
+ ) : Size()
+ data class Pixels(
+ val pixels: Int,
+ ) : Size()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
index ca8e101..02a60ad 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
@@ -481,7 +481,6 @@
mCropView.setExtraPadding(extraPadding + mPreview.getPaddingTop(),
extraPadding + mPreview.getPaddingBottom());
imageTop += (previewHeight - imageHeight) / 2;
- mCropView.setExtraPadding(extraPadding, extraPadding);
mCropView.setImageWidth(previewWidth);
scale = previewWidth / (float) mPreview.getDrawable().getIntrinsicWidth();
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 587c65d..06426b3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -3581,12 +3581,15 @@
}
private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
+ // don't fling while in keyguard to avoid jump in shade expand animation
+ boolean fullyExpandedInKeyguard = mBarState == KEYGUARD && mExpandedFraction >= 1.0;
mTrackingPointer = -1;
mAmbientState.setSwipingUp(false);
- if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop
+ if (!fullyExpandedInKeyguard && ((mTracking && mTouchSlopExceeded)
+ || Math.abs(x - mInitialExpandX) > mTouchSlop
|| Math.abs(y - mInitialExpandY) > mTouchSlop
|| (!isFullyExpanded() && !isFullyCollapsed())
- || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel)) {
mVelocityTracker.computeCurrentVelocity(1000);
float vel = mVelocityTracker.getYVelocity();
float vectorVel = (float) Math.hypot(
@@ -3634,9 +3637,9 @@
if (mUpdateFlingOnLayout) {
mUpdateFlingVelocity = vel;
}
- } else if (!mCentralSurfaces.isBouncerShowing()
+ } else if (fullyExpandedInKeyguard || (!mCentralSurfaces.isBouncerShowing()
&& !mAlternateBouncerInteractor.isVisibleState()
- && !mKeyguardStateController.isKeyguardGoingAway()) {
+ && !mKeyguardStateController.isKeyguardGoingAway())) {
onEmptySpaceClick();
onTrackingStopped(true);
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index e2f31e8..2899081 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -16,6 +16,7 @@
package com.android.systemui.shade;
+import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_BACK;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import android.app.StatusBarManager;
@@ -39,6 +40,7 @@
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.dock.DockManager;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
@@ -88,10 +90,12 @@
private final NotificationInsetsController mNotificationInsetsController;
private final AlternateBouncerInteractor mAlternateBouncerInteractor;
private final UdfpsOverlayInteractor mUdfpsOverlayInteractor;
+ private final boolean mIsTrackpadGestureBackEnabled;
private GestureDetector mPulsingWakeupGestureHandler;
private View mBrightnessMirror;
private boolean mTouchActive;
private boolean mTouchCancelled;
+ private MotionEvent mDownEvent;
private boolean mExpandAnimationRunning;
private NotificationStackScrollLayout mStackScrollLayout;
private PhoneStatusBarViewController mStatusBarViewController;
@@ -137,8 +141,8 @@
AlternateBouncerInteractor alternateBouncerInteractor,
UdfpsOverlayInteractor udfpsOverlayInteractor,
KeyguardTransitionInteractor keyguardTransitionInteractor,
- PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel
- ) {
+ PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
+ FeatureFlags featureFlags) {
mLockscreenShadeTransitionController = transitionController;
mFalsingCollector = falsingCollector;
mStatusBarStateController = statusBarStateController;
@@ -159,6 +163,7 @@
mNotificationInsetsController = notificationInsetsController;
mAlternateBouncerInteractor = alternateBouncerInteractor;
mUdfpsOverlayInteractor = udfpsOverlayInteractor;
+ mIsTrackpadGestureBackEnabled = featureFlags.isEnabled(TRACKPAD_GESTURE_BACK);
// This view is not part of the newly inflated expanded status bar.
mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
@@ -219,9 +224,11 @@
if (isDown) {
mTouchActive = true;
mTouchCancelled = false;
+ mDownEvent = ev;
} else if (ev.getActionMasked() == MotionEvent.ACTION_UP
|| ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
mTouchActive = false;
+ mDownEvent = null;
}
if (mTouchCancelled || mExpandAnimationRunning) {
return false;
@@ -447,9 +454,17 @@
public void cancelCurrentTouch() {
if (mTouchActive) {
final long now = SystemClock.uptimeMillis();
- MotionEvent event = MotionEvent.obtain(now, now,
- MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
- event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ final MotionEvent event;
+ if (mIsTrackpadGestureBackEnabled) {
+ event = MotionEvent.obtain(mDownEvent);
+ event.setDownTime(now);
+ event.setAction(MotionEvent.ACTION_CANCEL);
+ event.setLocation(0.0f, 0.0f);
+ } else {
+ event = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ }
mView.dispatchTouchEvent(event);
event.recycle();
mTouchCancelled = true;
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt
index 5736a5c..26149321 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt
@@ -37,7 +37,8 @@
fun create(
@BindsInstance parent: ViewGroup,
@BindsInstance @Named(PLUGIN) plugin: BcSmartspaceDataPlugin,
- @BindsInstance onAttachListener: View.OnAttachStateChangeListener
+ @BindsInstance onAttachListener: View.OnAttachStateChangeListener,
+ @BindsInstance viewWithCustomLayout: View? = null
): SmartspaceViewComponent
}
@@ -53,10 +54,13 @@
falsingManager: FalsingManager,
parent: ViewGroup,
@Named(PLUGIN) plugin: BcSmartspaceDataPlugin,
+ viewWithCustomLayout: View?,
onAttachListener: View.OnAttachStateChangeListener
):
BcSmartspaceDataPlugin.SmartspaceView {
- val ssView = plugin.getView(parent)
+ val ssView = viewWithCustomLayout
+ as? BcSmartspaceDataPlugin.SmartspaceView
+ ?: plugin.getView(parent)
// Currently, this is only used to provide SmartspaceView on Dream surface.
ssView.setUiSurface(UI_SURFACE_DREAM)
ssView.registerDataProvider(plugin)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 1004ec1..4d0e746 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -389,11 +389,11 @@
// First check whether this notification should launch a full screen intent, and
// launch it if needed.
val fsiDecision = mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
- if (fsiDecision != null && fsiDecision.shouldLaunch) {
- mNotificationInterruptStateProvider.logFullScreenIntentDecision(entry, fsiDecision)
+ mNotificationInterruptStateProvider.logFullScreenIntentDecision(entry, fsiDecision)
+ if (fsiDecision.shouldLaunch) {
mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
} else if (mFlags.fsiOnDNDUpdate() &&
- fsiDecision.equals(FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)) {
+ fsiDecision == FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND) {
// If DND was the only reason this entry was suppressed, note it for potential
// reconsideration on later ranking updates.
addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
@@ -514,14 +514,24 @@
mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
if (decision.shouldLaunch) {
// Log both the launch of the full screen and also that this was via a
- // ranking update.
- mLogger.logEntryUpdatedToFullScreen(entry.key)
+ // ranking update, and finally revoke candidacy for FSI reconsideration
+ mLogger.logEntryUpdatedToFullScreen(entry.key, decision.name)
mNotificationInterruptStateProvider.logFullScreenIntentDecision(
entry, decision)
mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+ mFSIUpdateCandidates.remove(entry.key)
// if we launch the FSI then this is no longer a candidate for HUN
continue
+ } else if (decision == FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND) {
+ // decision has not changed; no need to log
+ } else {
+ // some other condition is now blocking FSI; log that and revoke candidacy
+ // for FSI reconsideration
+ mLogger.logEntryDisqualifiedFromFullScreen(entry.key, decision.name)
+ mNotificationInterruptStateProvider.logFullScreenIntentDecision(
+ entry, decision)
+ mFSIUpdateCandidates.remove(entry.key)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index 2c6bf6b..e936559 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -70,11 +70,21 @@
})
}
- fun logEntryUpdatedToFullScreen(key: String) {
+ fun logEntryUpdatedToFullScreen(key: String, reason: String) {
buffer.log(TAG, LogLevel.DEBUG, {
str1 = key
+ str2 = reason
}, {
- "updating entry to launch full screen intent: $str1"
+ "updating entry to launch full screen intent: $str1 because $str2"
+ })
+ }
+
+ fun logEntryDisqualifiedFromFullScreen(key: String, reason: String) {
+ buffer.log(TAG, LogLevel.DEBUG, {
+ str1 = key
+ str2 = reason
+ }, {
+ "updated entry no longer qualifies for full screen intent: $str1 because $str2"
})
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
index 9001470..5ba8801 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.interruption;
+import androidx.annotation.NonNull;
+
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
/**
@@ -153,7 +155,8 @@
* @param entry the entry to evaluate
* @return FullScreenIntentDecision representing the decision for whether to show the intent
*/
- FullScreenIntentDecision getFullScreenIntentDecision(NotificationEntry entry);
+ @NonNull
+ FullScreenIntentDecision getFullScreenIntentDecision(@NonNull NotificationEntry entry);
/**
* Write the full screen launch decision for the given entry to logs.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index 9f45b9d..274377f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -35,6 +35,8 @@
import android.service.notification.StatusBarNotification;
import android.util.Log;
+import androidx.annotation.NonNull;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
@@ -232,6 +234,7 @@
// suppressor.
//
// If the entry was not suppressed by DND, just returns the given decision.
+ @NonNull
private FullScreenIntentDecision getDecisionGivenSuppression(FullScreenIntentDecision decision,
boolean suppressedByDND) {
if (suppressedByDND) {
@@ -243,7 +246,7 @@
}
@Override
- public FullScreenIntentDecision getFullScreenIntentDecision(NotificationEntry entry) {
+ public FullScreenIntentDecision getFullScreenIntentDecision(@NonNull NotificationEntry entry) {
if (entry.getSbn().getNotification().fullScreenIntent == null) {
if (entry.isStickyAndNotDemoted()) {
return FullScreenIntentDecision.NO_FSI_SHOW_STICKY_HUN;
@@ -336,52 +339,30 @@
final int uid = entry.getSbn().getUid();
final String packageName = entry.getSbn().getPackageName();
switch (decision) {
- case NO_FSI_SHOW_STICKY_HUN:
- mLogger.logNoFullscreen(entry, "Permission denied, show sticky HUN");
- return;
case NO_FULL_SCREEN_INTENT:
- return;
- case NO_FSI_SUPPRESSED_BY_DND:
- case NO_FSI_SUPPRESSED_ONLY_BY_DND:
- mLogger.logNoFullscreen(entry, "Suppressed by DND");
- return;
- case NO_FSI_NOT_IMPORTANT_ENOUGH:
- mLogger.logNoFullscreen(entry, "Not important enough");
+ // explicitly prevent logging for this (frequent) case
return;
case NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR:
android.util.EventLog.writeEvent(0x534e4554, "231322873", uid,
"groupAlertBehavior");
mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, uid,
packageName);
- mLogger.logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN");
- return;
- case FSI_DEVICE_NOT_INTERACTIVE:
- mLogger.logFullscreen(entry, "Device is not interactive");
- return;
- case FSI_DEVICE_IS_DREAMING:
- mLogger.logFullscreen(entry, "Device is dreaming");
- return;
- case FSI_KEYGUARD_SHOWING:
- mLogger.logFullscreen(entry, "Keyguard is showing");
- return;
- case NO_FSI_EXPECTED_TO_HUN:
- mLogger.logNoFullscreen(entry, "Expected to HUN");
- return;
- case FSI_KEYGUARD_OCCLUDED:
- mLogger.logFullscreen(entry,
- "Expected not to HUN while keyguard occluded");
- return;
- case FSI_LOCKED_SHADE:
- mLogger.logFullscreen(entry, "Keyguard is showing and not occluded");
+ mLogger.logNoFullscreenWarning(entry,
+ decision + ": GroupAlertBehavior will prevent HUN");
return;
case NO_FSI_NO_HUN_OR_KEYGUARD:
android.util.EventLog.writeEvent(0x534e4554, "231322873", uid,
"no hun or keyguard");
mUiEventLogger.log(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, uid, packageName);
- mLogger.logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard");
+ mLogger.logNoFullscreenWarning(entry,
+ decision + ": Expected not to HUN while not on keyguard");
return;
- case FSI_EXPECTED_NOT_TO_HUN:
- mLogger.logFullscreen(entry, "Expected not to HUN");
+ default:
+ if (decision.shouldLaunch) {
+ mLogger.logFullscreen(entry, decision.name());
+ } else {
+ mLogger.logNoFullscreen(entry, decision.name());
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index a5b7e94..33cbf06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -184,7 +184,6 @@
private int mMaxSmallHeight;
private int mMaxSmallHeightLarge;
private int mMaxExpandedHeight;
- private int mIncreasedPaddingBetweenElements;
private int mNotificationLaunchHeight;
private boolean mMustStayOnScreen;
@@ -3065,14 +3064,6 @@
return showingLayout != null && showingLayout.requireRowToHaveOverlappingRendering();
}
- @Override
- public int getExtraBottomPadding() {
- if (mIsSummaryWithChildren && isGroupExpanded()) {
- return mIncreasedPaddingBetweenElements;
- }
- return 0;
- }
-
public void setInlineReplyAnimationFlagEnabled(boolean isEnabled) {
mIsInlineReplyAnimationFlagEnabled = isEnabled;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 25c7264..9df6ba9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -451,7 +451,7 @@
protected void updateClipping() {
if (mClipToActualHeight && shouldClipToActualHeight()) {
int top = getClipTopAmount();
- int bottom = Math.max(Math.max(getActualHeight() + getExtraBottomPadding()
+ int bottom = Math.max(Math.max(getActualHeight()
- mClipBottomAmount, top), mMinimumHeightForClipping);
mClipRect.set(Integer.MIN_VALUE, top, Integer.MAX_VALUE, bottom);
setClipBounds(mClipRect);
@@ -592,13 +592,6 @@
}
/**
- * @return padding used to alter how much of the view is clipped.
- */
- public int getExtraBottomPadding() {
- return 0;
- }
-
- /**
* @return true if the group's expansion state is changing, false otherwise.
*/
public boolean isGroupExpansionChanging() {
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 e2e2a23..2c088fa 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
@@ -4499,7 +4499,7 @@
expandableView.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
} else {
float yLocation = previous.getTranslationY() + previous.getActualHeight() -
- expandableView.getTranslationY() - previous.getExtraBottomPadding();
+ expandableView.getTranslationY();
expandableView.setFakeShadowIntensity(
diff / FakeShadowView.SHADOW_SIBLING_TRESHOLD,
previous.getOutlineAlpha(), (int) yLocation,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index f1fc386..f866d65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -18,7 +18,6 @@
import android.content.Context
import android.content.IntentFilter
-import android.telephony.CellSignalStrength
import android.telephony.CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
import android.telephony.CellSignalStrengthCdma
import android.telephony.ServiceState
@@ -28,12 +27,12 @@
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
import android.telephony.TelephonyManager
-import android.telephony.TelephonyManager.ERI_OFF
+import android.telephony.TelephonyManager.ERI_FLASH
+import android.telephony.TelephonyManager.ERI_ON
import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import com.android.settingslib.Utils
import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.table.TableLogBuffer
@@ -59,16 +58,14 @@
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.stateIn
/**
@@ -100,8 +97,6 @@
}
}
- private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
-
/**
* This flow defines the single shared connection to system_server via TelephonyCallback. Any
* new callback should be added to this listener and funneled through callbackEvents via a data
@@ -109,9 +104,15 @@
*
* The reason we need to do this is because TelephonyManager limits the number of registered
* listeners per-process, so we don't want to create a new listener for every callback.
+ *
+ * A note on the design for back pressure here: We use the [coalesce] operator here to change
+ * the backpressure strategy to store exactly the last callback event of _each type_ here, as
+ * opposed to the default strategy which is to drop the oldest event (regardless of type). This
+ * means that we should never miss any single event as long as the flow has been started.
*/
- private val callbackEvents: SharedFlow<CallbackEvent> =
- conflatedCallbackFlow {
+ private val callbackEvents: StateFlow<TelephonyCallbackState> = run {
+ val initial = TelephonyCallbackState()
+ callbackFlow {
val callback =
object :
TelephonyCallback(),
@@ -165,48 +166,50 @@
telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
- .shareIn(scope, SharingStarted.WhileSubscribed())
+ .scan(initial = initial) { state, event -> state.applyEvent(event) }
+ .stateIn(scope = scope, started = SharingStarted.WhileSubscribed(), initial)
+ }
override val isEmergencyOnly =
callbackEvents
- .filterIsInstance<CallbackEvent.OnServiceStateChanged>()
+ .mapNotNull { it.onServiceStateChanged }
.map { it.serviceState.isEmergencyOnly }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val isRoaming =
callbackEvents
- .filterIsInstance<CallbackEvent.OnServiceStateChanged>()
+ .mapNotNull { it.onServiceStateChanged }
.map { it.serviceState.roaming }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val operatorAlphaShort =
callbackEvents
- .filterIsInstance<CallbackEvent.OnServiceStateChanged>()
+ .mapNotNull { it.onServiceStateChanged }
.map { it.serviceState.operatorAlphaShort }
.stateIn(scope, SharingStarted.WhileSubscribed(), null)
override val isInService =
callbackEvents
- .filterIsInstance<CallbackEvent.OnServiceStateChanged>()
+ .mapNotNull { it.onServiceStateChanged }
.map { Utils.isInService(it.serviceState) }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val isGsm =
callbackEvents
- .filterIsInstance<CallbackEvent.OnSignalStrengthChanged>()
+ .mapNotNull { it.onSignalStrengthChanged }
.map { it.signalStrength.isGsm }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val cdmaLevel =
callbackEvents
- .filterIsInstance<CallbackEvent.OnSignalStrengthChanged>()
+ .mapNotNull { it.onSignalStrengthChanged }
.map {
it.signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java).let {
strengths ->
if (strengths.isNotEmpty()) {
strengths[0].level
} else {
- CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
+ SIGNAL_STRENGTH_NONE_OR_UNKNOWN
}
}
}
@@ -214,19 +217,19 @@
override val primaryLevel =
callbackEvents
- .filterIsInstance<CallbackEvent.OnSignalStrengthChanged>()
+ .mapNotNull { it.onSignalStrengthChanged }
.map { it.signalStrength.level }
.stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
override val dataConnectionState =
callbackEvents
- .filterIsInstance<CallbackEvent.OnDataConnectionStateChanged>()
+ .mapNotNull { it.onDataConnectionStateChanged }
.map { it.dataState.toDataConnectionType() }
.stateIn(scope, SharingStarted.WhileSubscribed(), Disconnected)
override val dataActivityDirection =
callbackEvents
- .filterIsInstance<CallbackEvent.OnDataActivity>()
+ .mapNotNull { it.onDataActivity }
.map { it.direction.toMobileDataActivityModel() }
.stateIn(
scope,
@@ -236,28 +239,26 @@
override val carrierNetworkChangeActive =
callbackEvents
- .filterIsInstance<CallbackEvent.OnCarrierNetworkChange>()
+ .mapNotNull { it.onCarrierNetworkChange }
.map { it.active }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val resolvedNetworkType =
callbackEvents
- .filterIsInstance<CallbackEvent.OnDisplayInfoChanged>()
+ .mapNotNull { it.onDisplayInfoChanged }
.map {
- if (it.telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) {
- UnknownNetworkType
- } else if (
- it.telephonyDisplayInfo.overrideNetworkType == OVERRIDE_NETWORK_TYPE_NONE
- ) {
- DefaultNetworkType(
- mobileMappingsProxy.toIconKey(it.telephonyDisplayInfo.networkType)
- )
- } else {
+ if (it.telephonyDisplayInfo.overrideNetworkType != OVERRIDE_NETWORK_TYPE_NONE) {
OverrideNetworkType(
mobileMappingsProxy.toIconKeyOverride(
it.telephonyDisplayInfo.overrideNetworkType
)
)
+ } else if (it.telephonyDisplayInfo.networkType != NETWORK_TYPE_UNKNOWN) {
+ DefaultNetworkType(
+ mobileMappingsProxy.toIconKey(it.telephonyDisplayInfo.networkType)
+ )
+ } else {
+ UnknownNetworkType
}
}
.stateIn(scope, SharingStarted.WhileSubscribed(), UnknownNetworkType)
@@ -282,7 +283,10 @@
override val cdmaRoaming: StateFlow<Boolean> =
telephonyPollingEvent
- .mapLatest { telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber != ERI_OFF }
+ .mapLatest {
+ val cdmaEri = telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber
+ cdmaEri == ERI_ON || cdmaEri == ERI_FLASH
+ }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val networkName: StateFlow<NetworkNameModel> =
@@ -300,7 +304,8 @@
override val dataEnabled = run {
val initial = telephonyManager.isDataConnectionAllowed
callbackEvents
- .mapNotNull { (it as? CallbackEvent.OnDataEnabledChanged)?.enabled }
+ .mapNotNull { it.onDataEnabledChanged }
+ .map { it.enabled }
.stateIn(scope, SharingStarted.WhileSubscribed(), initial)
}
@@ -344,12 +349,41 @@
* Wrap every [TelephonyCallback] we care about in a data class so we can accept them in a single
* shared flow and then split them back out into other flows.
*/
-private sealed interface CallbackEvent {
+sealed interface CallbackEvent {
+ data class OnCarrierNetworkChange(val active: Boolean) : CallbackEvent
+ data class OnDataActivity(val direction: Int) : CallbackEvent
+ data class OnDataConnectionStateChanged(val dataState: Int) : CallbackEvent
+ data class OnDataEnabledChanged(val enabled: Boolean) : CallbackEvent
+ data class OnDisplayInfoChanged(val telephonyDisplayInfo: TelephonyDisplayInfo) : CallbackEvent
data class OnServiceStateChanged(val serviceState: ServiceState) : CallbackEvent
data class OnSignalStrengthChanged(val signalStrength: SignalStrength) : CallbackEvent
- data class OnDataConnectionStateChanged(val dataState: Int) : CallbackEvent
- data class OnDataActivity(val direction: Int) : CallbackEvent
- data class OnCarrierNetworkChange(val active: Boolean) : CallbackEvent
- data class OnDisplayInfoChanged(val telephonyDisplayInfo: TelephonyDisplayInfo) : CallbackEvent
- data class OnDataEnabledChanged(val enabled: Boolean) : CallbackEvent
+}
+
+/**
+ * A simple box type for 1-to-1 mapping of [CallbackEvent] to the batched event. Used in conjunction
+ * with [scan] to make sure we don't drop important callbacks due to late subscribers
+ */
+data class TelephonyCallbackState(
+ val onDataActivity: CallbackEvent.OnDataActivity? = null,
+ val onCarrierNetworkChange: CallbackEvent.OnCarrierNetworkChange? = null,
+ val onDataConnectionStateChanged: CallbackEvent.OnDataConnectionStateChanged? = null,
+ val onDataEnabledChanged: CallbackEvent.OnDataEnabledChanged? = null,
+ val onDisplayInfoChanged: CallbackEvent.OnDisplayInfoChanged? = null,
+ val onServiceStateChanged: CallbackEvent.OnServiceStateChanged? = null,
+ val onSignalStrengthChanged: CallbackEvent.OnSignalStrengthChanged? = null,
+) {
+ fun applyEvent(event: CallbackEvent): TelephonyCallbackState {
+ return when (event) {
+ is CallbackEvent.OnCarrierNetworkChange -> copy(onCarrierNetworkChange = event)
+ is CallbackEvent.OnDataActivity -> copy(onDataActivity = event)
+ is CallbackEvent.OnDataConnectionStateChanged ->
+ copy(onDataConnectionStateChanged = event)
+ is CallbackEvent.OnDataEnabledChanged -> copy(onDataEnabledChanged = event)
+ is CallbackEvent.OnDisplayInfoChanged -> copy(onDisplayInfoChanged = event)
+ is CallbackEvent.OnServiceStateChanged -> {
+ copy(onServiceStateChanged = event)
+ }
+ is CallbackEvent.OnSignalStrengthChanged -> copy(onSignalStrengthChanged = event)
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 3c7d092..95cc12a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -1416,11 +1416,7 @@
@Override
public void onAnimationCancel(@NonNull Animator animation) {
mInteractionJankMonitor.cancel(CUJ_VOLUME_CONTROL);
- Log.i(TAG, "onAnimationCancel");
-
- // We can only have one animation listener for cancel, so the jank listener should
- // also call for cleanup.
- finishDismiss();
+ Log.d(TAG, "onAnimationCancel");
}
@Override
@@ -1529,7 +1525,12 @@
.setDuration(mDialogHideAnimationDurationMs)
.setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
.withEndAction(() -> mHandler.postDelayed(() -> {
- finishDismiss();
+ mController.notifyVisible(false);
+ mDialog.dismiss();
+ tryToRemoveCaptionsTooltip();
+ mIsAnimatingDismiss = false;
+
+ hideRingerDrawer();
}, 50));
if (!shouldSlideInVolumeTray()) {
animator.translationX(
@@ -1547,18 +1548,6 @@
Trace.endSection();
}
- /**
- * Clean up and hide volume dialog. Called when animation is finished/cancelled.
- */
- private void finishDismiss() {
- mController.notifyVisible(false);
- mDialog.dismiss();
- tryToRemoveCaptionsTooltip();
- mIsAnimatingDismiss = false;
-
- hideRingerDrawer();
- }
-
private boolean showActiveStreamOnly() {
return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|| mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
index 0e837d2..a35e5b5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
@@ -18,12 +18,15 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -37,6 +40,7 @@
import org.mockito.MockitoAnnotations;
@SmallTest
+@TestableLooper.RunWithLooper
@RunWith(AndroidTestingRunner.class)
public class KeyguardMessageAreaControllerTest extends SysuiTestCase {
@Mock
@@ -45,14 +49,14 @@
private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@Mock
private KeyguardMessageArea mKeyguardMessageArea;
-
private KeyguardMessageAreaController mMessageAreaController;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mMessageAreaController = new KeyguardMessageAreaController.Factory(
- mKeyguardUpdateMonitor, mConfigurationController).create(mKeyguardMessageArea);
+ mKeyguardUpdateMonitor, mConfigurationController).create(
+ mKeyguardMessageArea);
}
@Test
@@ -89,6 +93,19 @@
}
@Test
+ public void testSetMessage_AnnounceForAccessibility() {
+ ArgumentCaptor<Runnable> argumentCaptor = ArgumentCaptor.forClass(Runnable.class);
+ when(mKeyguardMessageArea.getText()).thenReturn("abc");
+ mMessageAreaController.setMessage("abc");
+
+ verify(mKeyguardMessageArea).setMessage("abc", /* animate= */ true);
+ verify(mKeyguardMessageArea).removeCallbacks(any(Runnable.class));
+ verify(mKeyguardMessageArea).postDelayed(argumentCaptor.capture(), anyLong());
+ argumentCaptor.getValue().run();
+ verify(mKeyguardMessageArea).announceForAccessibility("abc");
+ }
+
+ @Test
public void testSetBouncerVisible() {
mMessageAreaController.setIsVisible(true);
verify(mKeyguardMessageArea).setIsVisible(true);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 38d3a3e..f966eb3 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -66,6 +66,7 @@
import com.android.systemui.classifier.FalsingA11yDelegate;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.log.SessionTracker;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
@@ -619,13 +620,26 @@
@Test
public void testReinflateViewFlipper() {
- mKeyguardSecurityContainerController.reinflateViewFlipper();
+ mKeyguardSecurityContainerController.reinflateViewFlipper(() -> {});
verify(mKeyguardSecurityViewFlipperController).clearViews();
verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class),
any(KeyguardSecurityCallback.class));
}
@Test
+ public void testReinflateViewFlipper_asyncBouncerFlagOn() {
+ when(mFeatureFlags.isEnabled(Flags.ASYNC_INFLATE_BOUNCER)).thenReturn(true);
+ KeyguardSecurityViewFlipperController.OnViewInflatedListener onViewInflatedListener =
+ () -> {
+ };
+ mKeyguardSecurityContainerController.reinflateViewFlipper(onViewInflatedListener);
+ verify(mKeyguardSecurityViewFlipperController).clearViews();
+ verify(mKeyguardSecurityViewFlipperController).asynchronouslyInflateView(
+ any(SecurityMode.class),
+ any(KeyguardSecurityCallback.class), eq(onViewInflatedListener));
+ }
+
+ @Test
public void testSideFpsControllerShow() {
mKeyguardSecurityContainerController.updateSideFpsVisibility(/* isVisible= */ true);
verify(mSideFpsController).show(
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
index 1614b57..afb54d2 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
@@ -31,10 +31,12 @@
import android.view.ViewGroup;
import android.view.WindowInsetsController;
+import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
import org.junit.Before;
import org.junit.Rule;
@@ -57,6 +59,8 @@
@Mock
private LayoutInflater mLayoutInflater;
@Mock
+ private AsyncLayoutInflater mAsyncLayoutInflater;
+ @Mock
private KeyguardInputViewController.Factory mKeyguardSecurityViewControllerFactory;
@Mock
private EmergencyButtonController.Factory mEmergencyButtonControllerFactory;
@@ -70,6 +74,8 @@
private WindowInsetsController mWindowInsetsController;
@Mock
private KeyguardSecurityCallback mKeyguardSecurityCallback;
+ @Mock
+ private FeatureFlags mFeatureFlags;
private KeyguardSecurityViewFlipperController mKeyguardSecurityViewFlipperController;
@@ -82,10 +88,11 @@
when(mView.getWindowInsetsController()).thenReturn(mWindowInsetsController);
when(mEmergencyButtonControllerFactory.create(any(EmergencyButton.class)))
.thenReturn(mEmergencyButtonController);
+ when(mView.getContext()).thenReturn(getContext());
mKeyguardSecurityViewFlipperController = new KeyguardSecurityViewFlipperController(mView,
- mLayoutInflater, mKeyguardSecurityViewControllerFactory,
- mEmergencyButtonControllerFactory);
+ mLayoutInflater, mAsyncLayoutInflater, mKeyguardSecurityViewControllerFactory,
+ mEmergencyButtonControllerFactory, mFeatureFlags);
}
@Test
@@ -108,6 +115,14 @@
}
@Test
+ public void asynchronouslyInflateView() {
+ mKeyguardSecurityViewFlipperController.asynchronouslyInflateView(SecurityMode.PIN,
+ mKeyguardSecurityCallback, null);
+ verify(mAsyncLayoutInflater).inflate(anyInt(), eq(mView), any(
+ AsyncLayoutInflater.OnInflateFinishedListener.class));
+ }
+
+ @Test
public void onDensityOrFontScaleChanged() {
mKeyguardSecurityViewFlipperController.clearViews();
verify(mView).removeAllViews();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/FontVariationUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
new file mode 100644
index 0000000..070cad7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
@@ -0,0 +1,62 @@
+package com.android.systemui.animation
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import junit.framework.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TAG_WGHT = "wght"
+private const val TAG_WDTH = "wdth"
+private const val TAG_OPSZ = "opsz"
+private const val TAG_ROND = "ROND"
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class FontVariationUtilsTest : SysuiTestCase() {
+ @Test
+ fun testUpdateFontVariation_getCorrectFvarStr() {
+ val fontVariationUtils = FontVariationUtils()
+ val initFvar =
+ fontVariationUtils.updateFontVariation(
+ weight = 100,
+ width = 100,
+ opticalSize = -1,
+ roundness = 100
+ )
+ Assert.assertEquals("'$TAG_WGHT' 100, '$TAG_WDTH' 100, '$TAG_ROND' 100", initFvar)
+ val updatedFvar =
+ fontVariationUtils.updateFontVariation(
+ weight = 200,
+ width = 100,
+ opticalSize = 0,
+ roundness = 100
+ )
+ Assert.assertEquals(
+ "'$TAG_WGHT' 200, '$TAG_WDTH' 100, '$TAG_OPSZ' 0, '$TAG_ROND' 100",
+ updatedFvar
+ )
+ }
+
+ @Test
+ fun testStyleValueUnchange_getBlankStr() {
+ val fontVariationUtils = FontVariationUtils()
+ fontVariationUtils.updateFontVariation(
+ weight = 100,
+ width = 100,
+ opticalSize = 0,
+ roundness = 100
+ )
+ val updatedFvar1 =
+ fontVariationUtils.updateFontVariation(
+ weight = 100,
+ width = 100,
+ opticalSize = 0,
+ roundness = 100
+ )
+ Assert.assertEquals("", updatedFvar1)
+ val updatedFvar2 = fontVariationUtils.updateFontVariation()
+ Assert.assertEquals("", updatedFvar2)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
index b389558..d7aa6e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
@@ -19,7 +19,6 @@
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.graphics.Typeface
-import android.graphics.fonts.FontVariationAxis
import android.testing.AndroidTestingRunner
import android.text.Layout
import android.text.StaticLayout
@@ -180,71 +179,4 @@
assertThat(paint.typeface).isSameInstanceAs(prevTypeface)
}
-
- @Test
- fun testSetTextStyle_addWeight() {
- testWeightChange("", 100, FontVariationAxis.fromFontVariationSettings("'wght' 100")!!)
- }
-
- @Test
- fun testSetTextStyle_changeWeight() {
- testWeightChange(
- "'wght' 500",
- 100,
- FontVariationAxis.fromFontVariationSettings("'wght' 100")!!
- )
- }
-
- @Test
- fun testSetTextStyle_addWeightWithOtherAxis() {
- testWeightChange(
- "'wdth' 100",
- 100,
- FontVariationAxis.fromFontVariationSettings("'wght' 100, 'wdth' 100")!!
- )
- }
-
- @Test
- fun testSetTextStyle_changeWeightWithOtherAxis() {
- testWeightChange(
- "'wght' 500, 'wdth' 100",
- 100,
- FontVariationAxis.fromFontVariationSettings("'wght' 100, 'wdth' 100")!!
- )
- }
-
- private fun testWeightChange(
- initialFontVariationSettings: String,
- weight: Int,
- expectedFontVariationSettings: Array<FontVariationAxis>
- ) {
- val layout = makeLayout("Hello, World", PAINT)
- val valueAnimator = mock(ValueAnimator::class.java)
- val textInterpolator = mock(TextInterpolator::class.java)
- val paint =
- TextPaint().apply {
- typeface = Typeface.createFromFile("/system/fonts/Roboto-Regular.ttf")
- fontVariationSettings = initialFontVariationSettings
- }
- `when`(textInterpolator.targetPaint).thenReturn(paint)
-
- val textAnimator =
- TextAnimator(layout, {}).apply {
- this.textInterpolator = textInterpolator
- this.animator = valueAnimator
- }
- textAnimator.setTextStyle(weight = weight, animate = false)
-
- val resultFontVariationList =
- FontVariationAxis.fromFontVariationSettings(
- textInterpolator.targetPaint.fontVariationSettings
- )
- expectedFontVariationSettings.forEach { expectedAxis ->
- val resultAxis = resultFontVariationList?.filter { it.tag == expectedAxis.tag }?.get(0)
- assertThat(resultAxis).isNotNull()
- if (resultAxis != null) {
- assertThat(resultAxis.styleValue).isEqualTo(expectedAxis.styleValue)
- }
- }
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index e1c7417..c068efb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -35,7 +35,6 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -930,6 +929,15 @@
assertNotSame(firstFpLocation, mAuthController.getFingerprintSensorLocation());
}
+ @Test
+ public void testCloseDialog_whenGlobalActionsMenuShown() throws Exception {
+ showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */);
+ mAuthController.handleShowGlobalActionsMenu();
+ verify(mReceiver).onDialogDismissed(
+ eq(BiometricPrompt.DISMISSED_REASON_USER_CANCEL),
+ eq(null) /* credentialAttestation */);
+ }
+
private void showDialog(int[] sensorIds, boolean credentialAllowed) {
mAuthController.showAuthenticationDialog(createTestPromptInfo(),
mReceiver /* receiver */,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt
index 5a613aa..590989d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt
@@ -45,6 +45,7 @@
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.Mock
import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
@@ -234,6 +235,36 @@
}
@Test
+ fun dialogPositiveButtonWhenCalledOnCompleteSettingIsTrue() {
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ doAnswer { assertThat(secureSettings.getBool(SETTING_ACTION, false)).isTrue() }
+ .`when`(completedRunnable)
+ .invoke()
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ clickButton(DialogInterface.BUTTON_POSITIVE)
+
+ verify(completedRunnable).invoke()
+ }
+
+ @Test
+ fun dialogPositiveCancelKeyguardStillCallsOnComplete() {
+ `when`(activityStarter.dismissKeyguardThenExecute(any(), nullable(), anyBoolean()))
+ .thenAnswer { (it.arguments[1] as Runnable).run() }
+ sharedPreferences.putAttempts(0)
+ secureSettings.putBool(SETTING_SHOW, true)
+ secureSettings.putBool(SETTING_ACTION, false)
+
+ underTest.maybeShowDialog(context, completedRunnable)
+ clickButton(DialogInterface.BUTTON_POSITIVE)
+
+ verify(completedRunnable).invoke()
+ }
+
+ @Test
fun dialogCancelDoesntChangeSetting() {
sharedPreferences.putAttempts(0)
secureSettings.putBool(SETTING_SHOW, true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index aa17d49..ed73797 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -20,7 +20,9 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -63,6 +65,7 @@
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@@ -295,6 +298,7 @@
// Inform the overlay service of dream starting.
client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
true /*shouldShowComplication*/);
+ mMainExecutor.runAllReady();
assertThat(mService.shouldShowComplications()).isTrue();
}
@@ -338,6 +342,48 @@
}
@Test
+ public void testImmediateEndDream() throws Exception {
+ final IDreamOverlayClient client = getClient();
+
+ // Start the dream, but don't execute any Runnables put on the executor yet. We delay
+ // executing Runnables as the timing isn't guaranteed and we want to verify that the overlay
+ // starts and finishes in the proper order even if Runnables are delayed.
+ client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
+ // Immediately end the dream.
+ client.endDream();
+ // Run any scheduled Runnables.
+ mMainExecutor.runAllReady();
+
+ // The overlay starts then finishes.
+ InOrder inOrder = inOrder(mWindowManager);
+ inOrder.verify(mWindowManager).addView(mViewCaptor.capture(), any());
+ inOrder.verify(mWindowManager).removeView(mViewCaptor.getValue());
+ }
+
+ @Test
+ public void testEndDreamDuringStartDream() throws Exception {
+ final IDreamOverlayClient client = getClient();
+
+ // Schedule the endDream call in the middle of the startDream implementation, as any
+ // ordering is possible.
+ doAnswer(invocation -> {
+ client.endDream();
+ return null;
+ }).when(mStateController).setOverlayActive(true);
+
+ // Start the dream.
+ client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
+ mMainExecutor.runAllReady();
+
+ // The overlay starts then finishes.
+ InOrder inOrder = inOrder(mWindowManager);
+ inOrder.verify(mWindowManager).addView(mViewCaptor.capture(), any());
+ inOrder.verify(mWindowManager).removeView(mViewCaptor.getValue());
+ }
+
+ @Test
public void testDestroy() throws RemoteException {
final IDreamOverlayClient client = getClient();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java
index b3329eb..0e16b47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java
@@ -17,7 +17,6 @@
package com.android.systemui.dreams.complication;
import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -36,7 +35,7 @@
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.shared.condition.Monitor;
import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.settings.FakeSettings;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
@@ -57,8 +56,7 @@
private Context mContext;
@Mock
private DreamBackend mDreamBackend;
- @Mock
- private SecureSettings mSecureSettings;
+ private FakeSettings mSecureSettings;
@Mock
private DreamOverlayStateController mDreamOverlayStateController;
@Captor
@@ -74,6 +72,7 @@
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mDreamBackend.getEnabledComplications()).thenReturn(new HashSet<>());
+ mSecureSettings = new FakeSettings();
mMonitor = SelfExecutingMonitor.createInstance();
mController = new ComplicationTypesUpdater(mDreamBackend, mExecutor,
@@ -100,19 +99,15 @@
when(mDreamBackend.getEnabledComplications()).thenReturn(new HashSet<>(Arrays.asList(
DreamBackend.COMPLICATION_TYPE_TIME, DreamBackend.COMPLICATION_TYPE_WEATHER,
DreamBackend.COMPLICATION_TYPE_AIR_QUALITY)));
- final ContentObserver settingsObserver = captureSettingsObserver();
- settingsObserver.onChange(false);
+
+ // Update the setting to trigger any content observers
+ mSecureSettings.putBoolForUser(
+ Settings.Secure.SCREENSAVER_COMPLICATIONS_ENABLED, true,
+ UserHandle.myUserId());
mExecutor.runAllReady();
verify(mDreamOverlayStateController).setAvailableComplicationTypes(
Complication.COMPLICATION_TYPE_TIME | Complication.COMPLICATION_TYPE_WEATHER
| Complication.COMPLICATION_TYPE_AIR_QUALITY);
}
-
- private ContentObserver captureSettingsObserver() {
- verify(mSecureSettings).registerContentObserverForUser(
- eq(Settings.Secure.SCREENSAVER_COMPLICATIONS_ENABLED),
- mSettingsObserverCaptor.capture(), eq(UserHandle.myUserId()));
- return mSettingsObserverCaptor.getValue();
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
index 984f4be..1044131 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
@@ -192,6 +192,7 @@
)
underTest.previewManager =
KeyguardRemotePreviewManager(
+ applicationScope = testScope.backgroundScope,
previewRendererFactory = previewRendererFactory,
mainDispatcher = testDispatcher,
backgroundHandler = backgroundHandler,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt
index e66be08..2ab1b99 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt
@@ -93,7 +93,7 @@
}
@Test
- fun shouldUpdateSideFps() = runTest {
+ fun shouldUpdateSideFps_show() = runTest {
var count = 0
val job = underTest.shouldUpdateSideFps.onEach { count++ }.launchIn(this)
repository.setPrimaryShow(true)
@@ -104,6 +104,18 @@
}
@Test
+ fun shouldUpdateSideFps_hide() = runTest {
+ repository.setPrimaryShow(true)
+ var count = 0
+ val job = underTest.shouldUpdateSideFps.onEach { count++ }.launchIn(this)
+ repository.setPrimaryShow(false)
+ // Run the tasks that are pending at this point of virtual time.
+ runCurrent()
+ assertThat(count).isEqualTo(1)
+ job.cancel()
+ }
+
+ @Test
fun sideFpsShowing() = runTest {
var sideFpsIsShowing = false
val job = underTest.sideFpsShowing.onEach { sideFpsIsShowing = it }.launchIn(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index fd353af..df13fdd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -94,7 +94,6 @@
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -1763,7 +1762,7 @@
fun tapContentView_showOverLockscreen_openActivity() {
// WHEN we are on lockscreen and this activity can show over lockscreen
whenever(keyguardStateController.isShowing).thenReturn(true)
- whenever(activityIntentHelper.wouldShowOverLockscreen(any(), any())).thenReturn(true)
+ whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any())).thenReturn(true)
val clickIntent = mock(Intent::class.java)
val pendingIntent = mock(PendingIntent::class.java)
@@ -1774,16 +1773,20 @@
player.bindPlayer(data, KEY)
verify(viewHolder.player).setOnClickListener(captor.capture())
- // THEN it shows without dismissing keyguard first
+ // THEN it sends the PendingIntent without dismissing keyguard first,
+ // and does not use the Intent directly (see b/271845008)
captor.value.onClick(viewHolder.player)
- verify(activityStarter).startActivity(eq(clickIntent), eq(true), nullable(), eq(true))
+ verify(pendingIntent).send()
+ verify(pendingIntent, never()).getIntent()
+ verify(activityStarter, never()).postStartActivityDismissingKeyguard(eq(clickIntent), any())
}
@Test
fun tapContentView_noShowOverLockscreen_dismissKeyguard() {
// WHEN we are on lockscreen and the activity cannot show over lockscreen
whenever(keyguardStateController.isShowing).thenReturn(true)
- whenever(activityIntentHelper.wouldShowOverLockscreen(any(), any())).thenReturn(false)
+ whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any()))
+ .thenReturn(false)
val clickIntent = mock(Intent::class.java)
val pendingIntent = mock(PendingIntent::class.java)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 56e060d..17d8799 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -16,12 +16,13 @@
package com.android.systemui.media.dialog;
-import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_GO_TO_APP;
-import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_NONE;
import static android.media.RouteListingPreference.Item.SUBTEXT_AD_ROUTING_DISALLOWED;
import static android.media.RouteListingPreference.Item.SUBTEXT_CUSTOM;
import static android.media.RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQUIRED;
+import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP;
+import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt
new file mode 100644
index 0000000..ceacaf9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.data.repository
+
+import android.content.Context
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
+import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import com.android.systemui.multishade.shared.model.ShadeConfig
+import com.android.systemui.multishade.shared.model.ShadeId
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class MultiShadeRepositoryTest : SysuiTestCase() {
+
+ private lateinit var inputProxy: MultiShadeInputProxy
+
+ @Before
+ fun setUp() {
+ inputProxy = MultiShadeInputProxy()
+ }
+
+ @Test
+ fun proxiedInput() = runTest {
+ val underTest = create()
+ val latest: ProxiedInputModel? by collectLastValue(underTest.proxiedInput)
+
+ assertWithMessage("proxiedInput should start with null").that(latest).isNull()
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
+ assertThat(latest).isEqualTo(ProxiedInputModel.OnTap)
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 100f))
+ assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 100f))
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 120f))
+ assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 120f))
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
+ assertThat(latest).isEqualTo(ProxiedInputModel.OnDragEnd)
+ }
+
+ @Test
+ fun shadeConfig_dualShadeEnabled() = runTest {
+ overrideResource(R.bool.dual_shade_enabled, true)
+ val underTest = create()
+ val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig)
+
+ assertThat(shadeConfig).isInstanceOf(ShadeConfig.DualShadeConfig::class.java)
+ }
+
+ @Test
+ fun shadeConfig_dualShadeNotEnabled() = runTest {
+ overrideResource(R.bool.dual_shade_enabled, false)
+ val underTest = create()
+ val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig)
+
+ assertThat(shadeConfig).isInstanceOf(ShadeConfig.SingleShadeConfig::class.java)
+ }
+
+ @Test
+ fun forceCollapseAll() = runTest {
+ val underTest = create()
+ val forceCollapseAll: Boolean? by collectLastValue(underTest.forceCollapseAll)
+
+ assertWithMessage("forceCollapseAll should start as false!")
+ .that(forceCollapseAll)
+ .isFalse()
+
+ underTest.setForceCollapseAll(true)
+ assertThat(forceCollapseAll).isTrue()
+
+ underTest.setForceCollapseAll(false)
+ assertThat(forceCollapseAll).isFalse()
+ }
+
+ @Test
+ fun shadeInteraction() = runTest {
+ val underTest = create()
+ val shadeInteraction: MultiShadeInteractionModel? by
+ collectLastValue(underTest.shadeInteraction)
+
+ assertWithMessage("shadeInteraction should start as null!").that(shadeInteraction).isNull()
+
+ underTest.setShadeInteraction(
+ MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false)
+ )
+ assertThat(shadeInteraction)
+ .isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false))
+
+ underTest.setShadeInteraction(
+ MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true)
+ )
+ assertThat(shadeInteraction)
+ .isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true))
+
+ underTest.setShadeInteraction(null)
+ assertThat(shadeInteraction).isNull()
+ }
+
+ @Test
+ fun expansion() = runTest {
+ val underTest = create()
+ val leftExpansion: Float? by
+ collectLastValue(underTest.getShade(ShadeId.LEFT).map { it.expansion })
+ val rightExpansion: Float? by
+ collectLastValue(underTest.getShade(ShadeId.RIGHT).map { it.expansion })
+ val singleExpansion: Float? by
+ collectLastValue(underTest.getShade(ShadeId.SINGLE).map { it.expansion })
+
+ assertWithMessage("expansion should start as 0!").that(leftExpansion).isZero()
+ assertWithMessage("expansion should start as 0!").that(rightExpansion).isZero()
+ assertWithMessage("expansion should start as 0!").that(singleExpansion).isZero()
+
+ underTest.setExpansion(
+ shadeId = ShadeId.LEFT,
+ 0.4f,
+ )
+ assertThat(leftExpansion).isEqualTo(0.4f)
+ assertThat(rightExpansion).isEqualTo(0f)
+ assertThat(singleExpansion).isEqualTo(0f)
+
+ underTest.setExpansion(
+ shadeId = ShadeId.RIGHT,
+ 0.73f,
+ )
+ assertThat(leftExpansion).isEqualTo(0.4f)
+ assertThat(rightExpansion).isEqualTo(0.73f)
+ assertThat(singleExpansion).isEqualTo(0f)
+
+ underTest.setExpansion(
+ shadeId = ShadeId.LEFT,
+ 0.1f,
+ )
+ underTest.setExpansion(
+ shadeId = ShadeId.SINGLE,
+ 0.88f,
+ )
+ assertThat(leftExpansion).isEqualTo(0.1f)
+ assertThat(rightExpansion).isEqualTo(0.73f)
+ assertThat(singleExpansion).isEqualTo(0.88f)
+ }
+
+ private fun create(): MultiShadeRepository {
+ return create(
+ context = context,
+ inputProxy = inputProxy,
+ )
+ }
+
+ companion object {
+ fun create(
+ context: Context,
+ inputProxy: MultiShadeInputProxy,
+ ): MultiShadeRepository {
+ return MultiShadeRepository(
+ applicationContext = context,
+ inputProxy = inputProxy,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt
new file mode 100644
index 0000000..415e68f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.domain.interactor
+
+import android.content.Context
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
+import com.android.systemui.multishade.data.repository.MultiShadeRepositoryTest
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import com.android.systemui.multishade.shared.model.ShadeId
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class MultiShadeInteractorTest : SysuiTestCase() {
+
+ private lateinit var testScope: TestScope
+ private lateinit var inputProxy: MultiShadeInputProxy
+
+ @Before
+ fun setUp() {
+ testScope = TestScope()
+ inputProxy = MultiShadeInputProxy()
+ }
+
+ @Test
+ fun maxShadeExpansion() =
+ testScope.runTest {
+ val underTest = create()
+ val maxShadeExpansion: Float? by collectLastValue(underTest.maxShadeExpansion)
+ assertWithMessage("maxShadeExpansion must start with 0.0!")
+ .that(maxShadeExpansion)
+ .isEqualTo(0f)
+
+ underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f)
+ assertThat(maxShadeExpansion).isEqualTo(0.441f)
+
+ underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f)
+ assertThat(maxShadeExpansion).isEqualTo(0.442f)
+
+ underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f)
+ assertThat(maxShadeExpansion).isEqualTo(0.441f)
+
+ underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f)
+ assertThat(maxShadeExpansion).isEqualTo(0f)
+ }
+
+ @Test
+ fun isVisible_dualShadeConfig() =
+ testScope.runTest {
+ overrideResource(R.bool.dual_shade_enabled, true)
+ val underTest = create()
+ val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT))
+ val isRightShadeVisible: Boolean? by
+ collectLastValue(underTest.isVisible(ShadeId.RIGHT))
+ val isSingleShadeVisible: Boolean? by
+ collectLastValue(underTest.isVisible(ShadeId.SINGLE))
+
+ assertThat(isLeftShadeVisible).isTrue()
+ assertThat(isRightShadeVisible).isTrue()
+ assertThat(isSingleShadeVisible).isFalse()
+ }
+
+ @Test
+ fun isVisible_singleShadeConfig() =
+ testScope.runTest {
+ overrideResource(R.bool.dual_shade_enabled, false)
+ val underTest = create()
+ val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT))
+ val isRightShadeVisible: Boolean? by
+ collectLastValue(underTest.isVisible(ShadeId.RIGHT))
+ val isSingleShadeVisible: Boolean? by
+ collectLastValue(underTest.isVisible(ShadeId.SINGLE))
+
+ assertThat(isLeftShadeVisible).isFalse()
+ assertThat(isRightShadeVisible).isFalse()
+ assertThat(isSingleShadeVisible).isTrue()
+ }
+
+ @Test
+ fun isNonProxiedInputAllowed() =
+ testScope.runTest {
+ val underTest = create()
+ val isLeftShadeNonProxiedInputAllowed: Boolean? by
+ collectLastValue(underTest.isNonProxiedInputAllowed(ShadeId.LEFT))
+ assertWithMessage("isNonProxiedInputAllowed should start as true!")
+ .that(isLeftShadeNonProxiedInputAllowed)
+ .isTrue()
+
+ // Need to collect proxied input so the flows become hot as the gesture cancelation code
+ // logic sits in side the proxiedInput flow for each shade.
+ collectLastValue(underTest.proxiedInput(ShadeId.LEFT))
+ collectLastValue(underTest.proxiedInput(ShadeId.RIGHT))
+
+ // Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on
+ // the
+ // same shade.
+ inputProxy.onProxiedInput(
+ ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f)
+ )
+ assertThat(isLeftShadeNonProxiedInputAllowed).isFalse()
+
+ // Registering the end of the proxied interaction re-allows it.
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
+ assertThat(isLeftShadeNonProxiedInputAllowed).isTrue()
+
+ // Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade,
+ // disallowing non-proxied input on the LEFT shade.
+ inputProxy.onProxiedInput(
+ ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f)
+ )
+ assertThat(isLeftShadeNonProxiedInputAllowed).isFalse()
+
+ // Registering the end of the interaction on the RIGHT shade re-allows it.
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
+ assertThat(isLeftShadeNonProxiedInputAllowed).isTrue()
+ }
+
+ @Test
+ fun isForceCollapsed_whenOtherShadeInteractionUnderway() =
+ testScope.runTest {
+ val underTest = create()
+ val isLeftShadeForceCollapsed: Boolean? by
+ collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
+ val isRightShadeForceCollapsed: Boolean? by
+ collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
+ val isSingleShadeForceCollapsed: Boolean? by
+ collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
+
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isLeftShadeForceCollapsed)
+ .isFalse()
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isRightShadeForceCollapsed)
+ .isFalse()
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isSingleShadeForceCollapsed)
+ .isFalse()
+
+ // Registering the start of an interaction on the RIGHT shade force-collapses the LEFT
+ // shade.
+ underTest.onUserInteractionStarted(ShadeId.RIGHT)
+ assertThat(isLeftShadeForceCollapsed).isTrue()
+ assertThat(isRightShadeForceCollapsed).isFalse()
+ assertThat(isSingleShadeForceCollapsed).isFalse()
+
+ // Registering the end of the interaction on the RIGHT shade re-allows it.
+ underTest.onUserInteractionEnded(ShadeId.RIGHT)
+ assertThat(isLeftShadeForceCollapsed).isFalse()
+ assertThat(isRightShadeForceCollapsed).isFalse()
+ assertThat(isSingleShadeForceCollapsed).isFalse()
+
+ // Registering the start of an interaction on the LEFT shade force-collapses the RIGHT
+ // shade.
+ underTest.onUserInteractionStarted(ShadeId.LEFT)
+ assertThat(isLeftShadeForceCollapsed).isFalse()
+ assertThat(isRightShadeForceCollapsed).isTrue()
+ assertThat(isSingleShadeForceCollapsed).isFalse()
+
+ // Registering the end of the interaction on the LEFT shade re-allows it.
+ underTest.onUserInteractionEnded(ShadeId.LEFT)
+ assertThat(isLeftShadeForceCollapsed).isFalse()
+ assertThat(isRightShadeForceCollapsed).isFalse()
+ assertThat(isSingleShadeForceCollapsed).isFalse()
+ }
+
+ @Test
+ fun collapseAll() =
+ testScope.runTest {
+ val underTest = create()
+ val isLeftShadeForceCollapsed: Boolean? by
+ collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
+ val isRightShadeForceCollapsed: Boolean? by
+ collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
+ val isSingleShadeForceCollapsed: Boolean? by
+ collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
+
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isLeftShadeForceCollapsed)
+ .isFalse()
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isRightShadeForceCollapsed)
+ .isFalse()
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isSingleShadeForceCollapsed)
+ .isFalse()
+
+ underTest.collapseAll()
+ assertThat(isLeftShadeForceCollapsed).isTrue()
+ assertThat(isRightShadeForceCollapsed).isTrue()
+ assertThat(isSingleShadeForceCollapsed).isTrue()
+
+ // Receiving proxied input on that's not a tap gesture, on the left-hand side resets the
+ // "collapse all". Note that now the RIGHT shade is force-collapsed because we're
+ // interacting with the LEFT shade.
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 0f))
+ assertThat(isLeftShadeForceCollapsed).isFalse()
+ assertThat(isRightShadeForceCollapsed).isTrue()
+ assertThat(isSingleShadeForceCollapsed).isFalse()
+ }
+
+ @Test
+ fun onTapOutside_collapsesAll() =
+ testScope.runTest {
+ val underTest = create()
+ val isLeftShadeForceCollapsed: Boolean? by
+ collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
+ val isRightShadeForceCollapsed: Boolean? by
+ collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
+ val isSingleShadeForceCollapsed: Boolean? by
+ collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
+
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isLeftShadeForceCollapsed)
+ .isFalse()
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isRightShadeForceCollapsed)
+ .isFalse()
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isSingleShadeForceCollapsed)
+ .isFalse()
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
+ assertThat(isLeftShadeForceCollapsed).isTrue()
+ assertThat(isRightShadeForceCollapsed).isTrue()
+ assertThat(isSingleShadeForceCollapsed).isTrue()
+ }
+
+ @Test
+ fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() =
+ testScope.runTest {
+ val underTest = create()
+ val proxiedInput: ProxiedInputModel? by
+ collectLastValue(underTest.proxiedInput(ShadeId.RIGHT))
+ underTest.onUserInteractionStarted(shadeId = ShadeId.RIGHT)
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
+ assertThat(proxiedInput).isNull()
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f))
+ assertThat(proxiedInput).isNull()
+
+ underTest.onUserInteractionEnded(shadeId = ShadeId.RIGHT)
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
+ assertThat(proxiedInput).isNotNull()
+ }
+
+ private fun create(): MultiShadeInteractor {
+ return create(
+ testScope = testScope,
+ context = context,
+ inputProxy = inputProxy,
+ )
+ }
+
+ companion object {
+ fun create(
+ testScope: TestScope,
+ context: Context,
+ inputProxy: MultiShadeInputProxy,
+ ): MultiShadeInteractor {
+ return MultiShadeInteractor(
+ applicationScope = testScope.backgroundScope,
+ repository =
+ MultiShadeRepositoryTest.create(
+ context = context,
+ inputProxy = inputProxy,
+ ),
+ inputProxy = inputProxy,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt
new file mode 100644
index 0000000..0484515
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
+import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class MultiShadeViewModelTest : SysuiTestCase() {
+
+ private lateinit var testScope: TestScope
+ private lateinit var inputProxy: MultiShadeInputProxy
+
+ @Before
+ fun setUp() {
+ testScope = TestScope()
+ inputProxy = MultiShadeInputProxy()
+ }
+
+ @Test
+ fun scrim_whenDualShadeCollapsed() =
+ testScope.runTest {
+ val alpha = 0.5f
+ overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
+ overrideResource(R.bool.dual_shade_enabled, true)
+
+ val underTest = create()
+ val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
+ val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
+
+ assertThat(scrimAlpha).isZero()
+ assertThat(isScrimEnabled).isFalse()
+ }
+
+ @Test
+ fun scrim_whenDualShadeExpanded() =
+ testScope.runTest {
+ val alpha = 0.5f
+ overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
+ overrideResource(R.bool.dual_shade_enabled, true)
+ val underTest = create()
+ val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
+ val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
+ assertThat(scrimAlpha).isZero()
+ assertThat(isScrimEnabled).isFalse()
+
+ underTest.leftShade.onExpansionChanged(0.5f)
+ assertThat(scrimAlpha).isEqualTo(alpha * 0.5f)
+ assertThat(isScrimEnabled).isTrue()
+
+ underTest.rightShade.onExpansionChanged(1f)
+ assertThat(scrimAlpha).isEqualTo(alpha * 1f)
+ assertThat(isScrimEnabled).isTrue()
+ }
+
+ @Test
+ fun scrim_whenSingleShadeCollapsed() =
+ testScope.runTest {
+ val alpha = 0.5f
+ overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
+ overrideResource(R.bool.dual_shade_enabled, false)
+
+ val underTest = create()
+ val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
+ val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
+
+ assertThat(scrimAlpha).isZero()
+ assertThat(isScrimEnabled).isFalse()
+ }
+
+ @Test
+ fun scrim_whenSingleShadeExpanded() =
+ testScope.runTest {
+ val alpha = 0.5f
+ overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
+ overrideResource(R.bool.dual_shade_enabled, false)
+ val underTest = create()
+ val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
+ val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
+
+ underTest.singleShade.onExpansionChanged(0.95f)
+
+ assertThat(scrimAlpha).isZero()
+ assertThat(isScrimEnabled).isFalse()
+ }
+
+ private fun create(): MultiShadeViewModel {
+ return MultiShadeViewModel(
+ viewModelScope = testScope.backgroundScope,
+ interactor =
+ MultiShadeInteractorTest.create(
+ testScope = testScope,
+ context = context,
+ inputProxy = inputProxy,
+ ),
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt
new file mode 100644
index 0000000..e32aac5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
+import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
+import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import com.android.systemui.multishade.shared.model.ShadeId
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ShadeViewModelTest : SysuiTestCase() {
+
+ private lateinit var testScope: TestScope
+ private lateinit var inputProxy: MultiShadeInputProxy
+ private var interactor: MultiShadeInteractor? = null
+
+ @Before
+ fun setUp() {
+ testScope = TestScope()
+ inputProxy = MultiShadeInputProxy()
+ }
+
+ @Test
+ fun isVisible_dualShadeConfig() =
+ testScope.runTest {
+ overrideResource(R.bool.dual_shade_enabled, true)
+ val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible)
+ val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible)
+ val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible)
+
+ assertThat(isLeftShadeVisible).isTrue()
+ assertThat(isRightShadeVisible).isTrue()
+ assertThat(isSingleShadeVisible).isFalse()
+ }
+
+ @Test
+ fun isVisible_singleShadeConfig() =
+ testScope.runTest {
+ overrideResource(R.bool.dual_shade_enabled, false)
+ val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible)
+ val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible)
+ val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible)
+
+ assertThat(isLeftShadeVisible).isFalse()
+ assertThat(isRightShadeVisible).isFalse()
+ assertThat(isSingleShadeVisible).isTrue()
+ }
+
+ @Test
+ fun isSwipingEnabled() =
+ testScope.runTest {
+ val underTest = create(ShadeId.LEFT)
+ val isSwipingEnabled: Boolean? by collectLastValue(underTest.isSwipingEnabled)
+ assertWithMessage("isSwipingEnabled should start as true!")
+ .that(isSwipingEnabled)
+ .isTrue()
+
+ // Need to collect proxied input so the flows become hot as the gesture cancelation code
+ // logic sits in side the proxiedInput flow for each shade.
+ collectLastValue(underTest.proxiedInput)
+ collectLastValue(create(ShadeId.RIGHT).proxiedInput)
+
+ // Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on
+ // the
+ // same shade.
+ inputProxy.onProxiedInput(
+ ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f)
+ )
+ assertThat(isSwipingEnabled).isFalse()
+
+ // Registering the end of the proxied interaction re-allows it.
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
+ assertThat(isSwipingEnabled).isTrue()
+
+ // Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade,
+ // disallowing non-proxied input on the LEFT shade.
+ inputProxy.onProxiedInput(
+ ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f)
+ )
+ assertThat(isSwipingEnabled).isFalse()
+
+ // Registering the end of the interaction on the RIGHT shade re-allows it.
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
+ assertThat(isSwipingEnabled).isTrue()
+ }
+
+ @Test
+ fun isForceCollapsed_whenOtherShadeInteractionUnderway() =
+ testScope.runTest {
+ val leftShade = create(ShadeId.LEFT)
+ val rightShade = create(ShadeId.RIGHT)
+ val isLeftShadeForceCollapsed: Boolean? by collectLastValue(leftShade.isForceCollapsed)
+ val isRightShadeForceCollapsed: Boolean? by
+ collectLastValue(rightShade.isForceCollapsed)
+ val isSingleShadeForceCollapsed: Boolean? by
+ collectLastValue(create(ShadeId.SINGLE).isForceCollapsed)
+
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isLeftShadeForceCollapsed)
+ .isFalse()
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isRightShadeForceCollapsed)
+ .isFalse()
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isSingleShadeForceCollapsed)
+ .isFalse()
+
+ // Registering the start of an interaction on the RIGHT shade force-collapses the LEFT
+ // shade.
+ rightShade.onDragStarted()
+ assertThat(isLeftShadeForceCollapsed).isTrue()
+ assertThat(isRightShadeForceCollapsed).isFalse()
+ assertThat(isSingleShadeForceCollapsed).isFalse()
+
+ // Registering the end of the interaction on the RIGHT shade re-allows it.
+ rightShade.onDragEnded()
+ assertThat(isLeftShadeForceCollapsed).isFalse()
+ assertThat(isRightShadeForceCollapsed).isFalse()
+ assertThat(isSingleShadeForceCollapsed).isFalse()
+
+ // Registering the start of an interaction on the LEFT shade force-collapses the RIGHT
+ // shade.
+ leftShade.onDragStarted()
+ assertThat(isLeftShadeForceCollapsed).isFalse()
+ assertThat(isRightShadeForceCollapsed).isTrue()
+ assertThat(isSingleShadeForceCollapsed).isFalse()
+
+ // Registering the end of the interaction on the LEFT shade re-allows it.
+ leftShade.onDragEnded()
+ assertThat(isLeftShadeForceCollapsed).isFalse()
+ assertThat(isRightShadeForceCollapsed).isFalse()
+ assertThat(isSingleShadeForceCollapsed).isFalse()
+ }
+
+ @Test
+ fun onTapOutside_collapsesAll() =
+ testScope.runTest {
+ val isLeftShadeForceCollapsed: Boolean? by
+ collectLastValue(create(ShadeId.LEFT).isForceCollapsed)
+ val isRightShadeForceCollapsed: Boolean? by
+ collectLastValue(create(ShadeId.RIGHT).isForceCollapsed)
+ val isSingleShadeForceCollapsed: Boolean? by
+ collectLastValue(create(ShadeId.SINGLE).isForceCollapsed)
+
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isLeftShadeForceCollapsed)
+ .isFalse()
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isRightShadeForceCollapsed)
+ .isFalse()
+ assertWithMessage("isForceCollapsed should start as false!")
+ .that(isSingleShadeForceCollapsed)
+ .isFalse()
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
+ assertThat(isLeftShadeForceCollapsed).isTrue()
+ assertThat(isRightShadeForceCollapsed).isTrue()
+ assertThat(isSingleShadeForceCollapsed).isTrue()
+ }
+
+ @Test
+ fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() =
+ testScope.runTest {
+ val underTest = create(ShadeId.RIGHT)
+ val proxiedInput: ProxiedInputModel? by collectLastValue(underTest.proxiedInput)
+ underTest.onDragStarted()
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
+ assertThat(proxiedInput).isNull()
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f))
+ assertThat(proxiedInput).isNull()
+
+ underTest.onDragEnded()
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
+ assertThat(proxiedInput).isNotNull()
+ }
+
+ private fun create(
+ shadeId: ShadeId,
+ ): ShadeViewModel {
+ return ShadeViewModel(
+ viewModelScope = testScope.backgroundScope,
+ shadeId = shadeId,
+ interactor = interactor
+ ?: MultiShadeInteractorTest.create(
+ testScope = testScope,
+ context = context,
+ inputProxy = inputProxy,
+ )
+ .also { interactor = it },
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 51492eb..0dc2d17 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -29,6 +29,8 @@
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.dock.DockManager
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -112,6 +114,9 @@
.thenReturn(keyguardSecurityContainerController)
whenever(keyguardTransitionInteractor.lockscreenToDreamingTransition)
.thenReturn(emptyFlow<TransitionStep>())
+
+ val featureFlags = FakeFeatureFlags();
+ featureFlags.set(Flags.TRACKPAD_GESTURE_BACK, false)
underTest =
NotificationShadeWindowViewController(
lockscreenShadeTransitionController,
@@ -138,6 +143,7 @@
udfpsOverlayInteractor,
keyguardTransitionInteractor,
primaryBouncerToGoneTransitionViewModel,
+ featureFlags,
)
underTest.setupExpandedStatusBar()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
index 2f528a8..2797440 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
@@ -43,6 +43,8 @@
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.dock.DockManager;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
@@ -132,6 +134,8 @@
when(mKeyguardTransitionInteractor.getLockscreenToDreamingTransition())
.thenReturn(emptyFlow());
+ FakeFeatureFlags featureFlags = new FakeFeatureFlags();
+ featureFlags.set(Flags.TRACKPAD_GESTURE_BACK, false);
mController = new NotificationShadeWindowViewController(
mLockscreenShadeTransitionController,
new FalsingCollectorFake(),
@@ -156,7 +160,8 @@
mAlternateBouncerInteractor,
mUdfpsOverlayInteractor,
mKeyguardTransitionInteractor,
- mPrimaryBouncerToGoneTransitionViewModel
+ mPrimaryBouncerToGoneTransitionViewModel,
+ featureFlags
);
mController.setupExpandedStatusBar();
mController.setDragDownHelper(mDragDownHelper);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
index 64e58d0..0e2a3ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
@@ -134,7 +134,7 @@
TransitionInfoBuilder(@WindowManager.TransitionType int type) {
mInfo = new TransitionInfo(type, 0 /* flags */);
- mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0);
+ mInfo.addRootLeash(0, createMockSurface(true /* valid */), 0, 0);
}
TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
@@ -144,6 +144,7 @@
change.setMode(mode);
change.setFlags(flags);
change.setTaskInfo(taskInfo);
+ change.setDisplayId(0, 0);
mInfo.addChange(change);
return this;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
index a280510..58b44ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
@@ -24,6 +24,7 @@
import android.testing.TestableLooper
import android.view.View
import android.view.ViewGroup
+import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dreams.smartspace.DreamSmartspaceController
@@ -46,6 +47,7 @@
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
+import org.mockito.Mockito.anyInt
import org.mockito.MockitoAnnotations
import org.mockito.Spy
@@ -69,12 +71,21 @@
private lateinit var viewComponent: SmartspaceViewComponent
@Mock
+ private lateinit var weatherViewComponent: SmartspaceViewComponent
+
+ @Spy
+ private var weatherSmartspaceView: SmartspaceView = TestView(context)
+
+ @Mock
private lateinit var targetFilter: SmartspaceTargetFilter
@Mock
private lateinit var plugin: BcSmartspaceDataPlugin
@Mock
+ private lateinit var weatherPlugin: BcSmartspaceDataPlugin
+
+ @Mock
private lateinit var precondition: SmartspacePrecondition
@Spy
@@ -88,6 +99,9 @@
private lateinit var controller: DreamSmartspaceController
+ // TODO(b/272811280): Remove usage of real view
+ private val fakeParent = FrameLayout(context)
+
/**
* A class which implements SmartspaceView and extends View. This is mocked to provide the right
* object inheritance and interface implementation used in DreamSmartspaceController
@@ -121,13 +135,17 @@
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- `when`(viewComponentFactory.create(any(), eq(plugin), any()))
+ `when`(viewComponentFactory.create(any(), eq(plugin), any(), eq(null)))
.thenReturn(viewComponent)
`when`(viewComponent.getView()).thenReturn(smartspaceView)
+ `when`(viewComponentFactory.create(any(), eq(weatherPlugin), any(), any()))
+ .thenReturn(weatherViewComponent)
+ `when`(weatherViewComponent.getView()).thenReturn(weatherSmartspaceView)
`when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(session)
controller = DreamSmartspaceController(context, smartspaceManager, execution, uiExecutor,
- viewComponentFactory, precondition, Optional.of(targetFilter), Optional.of(plugin))
+ viewComponentFactory, precondition, Optional.of(targetFilter), Optional.of(plugin),
+ Optional.of(weatherPlugin))
}
/**
@@ -168,11 +186,11 @@
`when`(precondition.conditionsMet()).thenReturn(true)
controller.buildAndConnectView(Mockito.mock(ViewGroup::class.java))
- var stateChangeListener = withArgCaptor<View.OnAttachStateChangeListener> {
- verify(viewComponentFactory).create(any(), eq(plugin), capture())
+ val stateChangeListener = withArgCaptor<View.OnAttachStateChangeListener> {
+ verify(viewComponentFactory).create(any(), eq(plugin), capture(), eq(null))
}
- var mockView = Mockito.mock(TestView::class.java)
+ val mockView = Mockito.mock(TestView::class.java)
`when`(precondition.conditionsMet()).thenReturn(true)
stateChangeListener.onViewAttachedToWindow(mockView)
@@ -183,4 +201,74 @@
verify(session).close()
}
+
+ /**
+ * Ensures session is created when weather smartspace view is created and attached.
+ */
+ @Test
+ fun testConnectOnWeatherViewCreate() {
+ `when`(precondition.conditionsMet()).thenReturn(true)
+
+ val customView = Mockito.mock(TestView::class.java)
+ val weatherView = controller.buildAndConnectWeatherView(fakeParent, customView)
+ val weatherSmartspaceView = weatherView as SmartspaceView
+ fakeParent.addView(weatherView)
+
+ // Then weather view is created with custom view and the default weatherPlugin.getView
+ // should not be called
+ verify(viewComponentFactory).create(eq(fakeParent), eq(weatherPlugin), any(),
+ eq(customView))
+ verify(weatherPlugin, Mockito.never()).getView(fakeParent)
+
+ // And then session is created
+ controller.stateChangeListener.onViewAttachedToWindow(weatherView)
+ verify(smartspaceManager).createSmartspaceSession(any())
+ verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
+ verify(weatherSmartspaceView).setDozeAmount(0f)
+ }
+
+ /**
+ * Ensures weather plugin registers target listener when it is added from the controller.
+ */
+ @Test
+ fun testAddListenerInController_registersListenerForWeatherPlugin() {
+ val customView = Mockito.mock(TestView::class.java)
+ `when`(precondition.conditionsMet()).thenReturn(true)
+
+ // Given a session is created
+ val weatherView = controller.buildAndConnectWeatherView(fakeParent, customView)
+ controller.stateChangeListener.onViewAttachedToWindow(weatherView)
+ verify(smartspaceManager).createSmartspaceSession(any())
+
+ // When a listener is added
+ controller.addListenerForWeatherPlugin(listener)
+
+ // Then the listener is registered to the weather plugin only
+ verify(weatherPlugin).registerListener(listener)
+ verify(plugin, Mockito.never()).registerListener(any())
+ }
+
+ /**
+ * Ensures session is closed and weather plugin unregisters the notifier when weather smartspace
+ * view is detached.
+ */
+ @Test
+ fun testDisconnect_emitsEmptyListAndRemovesNotifier() {
+ `when`(precondition.conditionsMet()).thenReturn(true)
+
+ // Given a session is created
+ val customView = Mockito.mock(TestView::class.java)
+ val weatherView = controller.buildAndConnectWeatherView(fakeParent, customView)
+ controller.stateChangeListener.onViewAttachedToWindow(weatherView)
+ verify(smartspaceManager).createSmartspaceSession(any())
+
+ // When view is detached
+ controller.stateChangeListener.onViewDetachedFromWindow(weatherView)
+ // Then the session is closed
+ verify(session).close()
+
+ // And the listener receives an empty list of targets and unregisters the notifier
+ verify(weatherPlugin).onTargetsAvailable(emptyList())
+ verify(weatherPlugin).registerSmartspaceEventNotifier(null)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index cb4f119..4bb14a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -22,6 +22,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.logcatLogBuffer
+import com.android.systemui.flags.Flags
import com.android.systemui.statusbar.NotificationRemoteInputManager
import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
@@ -58,6 +59,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyString
+import org.mockito.BDDMockito.clearInvocations
import org.mockito.BDDMockito.given
import org.mockito.Mockito.never
import org.mockito.Mockito.times
@@ -166,6 +168,12 @@
mGroupChild1 = mHelper.createChildNotification(GROUP_ALERT_ALL, 1, "child", 350)
mGroupChild2 = mHelper.createChildNotification(GROUP_ALERT_ALL, 2, "child", 250)
mGroupChild3 = mHelper.createChildNotification(GROUP_ALERT_ALL, 3, "child", 150)
+
+ // Set the default FSI decision
+ setShouldFullScreen(any(), FullScreenIntentDecision.NO_FULL_SCREEN_INTENT)
+
+ // Run tests with default feature flag state
+ whenever(mFlags.fsiOnDNDUpdate()).thenReturn(Flags.FSI_ON_DND_UPDATE.default)
}
@Test
@@ -810,6 +818,39 @@
}
@Test
+ fun onEntryAdded_whenLaunchingFSI_doesLogDecision() {
+ // GIVEN A new notification can FSI
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(
+ mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ }
+
+ @Test
+ fun onEntryAdded_whenNotLaunchingFSI_doesLogDecision() {
+ // GIVEN A new notification can't FSI
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FULL_SCREEN_INTENT)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(
+ mEntry, FullScreenIntentDecision.NO_FULL_SCREEN_INTENT)
+ }
+
+ @Test
+ fun onEntryAdded_whenNotLaunchingFSIBecauseOfDnd_doesLogDecision() {
+ // GIVEN A new notification can't FSI because of DND
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(
+ mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ }
+
+ @Test
fun testOnRankingApplied_noFSIOnUpdateWhenFlagOff() {
// Ensure the feature flag is off
whenever(mFlags.fsiOnDNDUpdate()).thenReturn(false)
@@ -818,13 +859,22 @@
setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
mCollectionListener.onEntryAdded(mEntry)
+ // Verify that this causes a log
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(
+ mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ clearInvocations(mNotificationInterruptStateProvider)
+
// and it is then updated to allow full screen
setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
mCollectionListener.onRankingApplied()
// THEN it should not full screen because the feature is off
- verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+
+ // VERIFY that no additional logging happens either
+ verify(mNotificationInterruptStateProvider, never())
+ .logFullScreenIntentDecision(any(), any())
}
@Test
@@ -836,8 +886,11 @@
setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
mCollectionListener.onEntryAdded(mEntry)
- // at this point, it should not have full screened
- verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+ // at this point, it should not have full screened, but should have logged
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(mEntry,
+ FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ clearInvocations(mNotificationInterruptStateProvider)
// and it is then updated to allow full screen AND HUN
setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
@@ -847,10 +900,110 @@
mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
- // THEN it should full screen but it should NOT HUN
+ // THEN it should full screen and log but it should NOT HUN
verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
verify(mHeadsUpManager, never()).showNotification(any())
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(mEntry,
+ FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ clearInvocations(mNotificationInterruptStateProvider)
+
+ // WHEN ranking updates again and the pipeline reruns
+ clearInvocations(mLaunchFullScreenIntentProvider)
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // VERIFY that the FSI does not launch again or log
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mNotificationInterruptStateProvider, never())
+ .logFullScreenIntentDecision(any(), any())
+ }
+
+ @Test
+ fun testOnRankingApplied_withOnlyDndSuppressionAllowsFsiLater() {
+ // Turn on the feature
+ whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
+
+ // GIVEN that mEntry was previously suppressed from full-screen only by DND
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // at this point, it should not have full screened, but should have logged
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(mEntry,
+ FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ clearInvocations(mNotificationInterruptStateProvider)
+
+ // ranking is applied with only DND blocking FSI
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN it should still not yet full screen or HUN
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+ verify(mHeadsUpManager, never()).showNotification(any())
+
+ // Same decision as before; is not logged
+ verify(mNotificationInterruptStateProvider, never())
+ .logFullScreenIntentDecision(any(), any())
+ clearInvocations(mNotificationInterruptStateProvider)
+
+ // and it is then updated to allow full screen AND HUN
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ setShouldHeadsUp(mEntry)
+ whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN it should full screen and log but it should NOT HUN
+ verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+ verify(mHeadsUpManager, never()).showNotification(any())
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(mEntry,
+ FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ clearInvocations(mNotificationInterruptStateProvider)
+ }
+
+ @Test
+ fun testOnRankingApplied_newNonFullScreenAnswerInvalidatesCandidate() {
+ // Turn on the feature
+ whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
+
+ // GIVEN that mEntry was previously suppressed from full-screen only by DND
+ whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // at this point, it should not have full screened
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+
+ // now some other condition blocks FSI in addition to DND
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_BY_DND)
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN it should NOT full screen or HUN
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+ verify(mHeadsUpManager, never()).showNotification(any())
+
+ // NOW the DND logic changes and FSI and HUN are available
+ clearInvocations(mLaunchFullScreenIntentProvider)
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ setShouldHeadsUp(mEntry)
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // VERIFY that the FSI didn't happen, but that we do HUN
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ finishBind(mEntry)
+ verify(mHeadsUpManager).showNotification(mEntry)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 07d0dbd..8acf507 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -36,9 +36,12 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
@@ -76,6 +79,10 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
/**
* Tests for the interruption state provider which understands whether the system & notification
* is in a state allowing a particular notification to hun, pulse, or bubble.
@@ -560,7 +567,7 @@
.isFalse();
verify(mLogger, never()).logFullscreen(any(), any());
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
- verify(mLogger).logNoFullscreen(entry, "Suppressed by DND");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_SUPPRESSED_ONLY_BY_DND");
}
@Test
@@ -579,7 +586,7 @@
.isFalse();
verify(mLogger, never()).logFullscreen(any(), any());
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
- verify(mLogger).logNoFullscreen(entry, "Suppressed by DND");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_SUPPRESSED_BY_DND");
}
@Test
@@ -599,7 +606,7 @@
.isEqualTo(FullScreenIntentDecision.NO_FSI_NOT_IMPORTANT_ENOUGH);
assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.isFalse();
- verify(mLogger).logNoFullscreen(entry, "Not important enough");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_NOT_IMPORTANT_ENOUGH");
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
verify(mLogger, never()).logFullscreen(any(), any());
}
@@ -622,7 +629,8 @@
assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.isFalse();
verify(mLogger, never()).logNoFullscreen(any(), any());
- verify(mLogger).logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN");
+ verify(mLogger).logNoFullscreenWarning(entry,
+ "NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR: GroupAlertBehavior will prevent HUN");
verify(mLogger, never()).logFullscreen(any(), any());
assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1);
@@ -652,7 +660,7 @@
.isTrue();
verify(mLogger, never()).logNoFullscreen(any(), any());
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
- verify(mLogger).logFullscreen(entry, "Device is not interactive");
+ verify(mLogger).logFullscreen(entry, "FSI_DEVICE_NOT_INTERACTIVE");
}
@Test
@@ -674,7 +682,7 @@
.isTrue();
verify(mLogger, never()).logNoFullscreen(any(), any());
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
- verify(mLogger).logFullscreen(entry, "Device is dreaming");
+ verify(mLogger).logFullscreen(entry, "FSI_DEVICE_IS_DREAMING");
}
@Test
@@ -696,7 +704,7 @@
.isTrue();
verify(mLogger, never()).logNoFullscreen(any(), any());
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
- verify(mLogger).logFullscreen(entry, "Keyguard is showing");
+ verify(mLogger).logFullscreen(entry, "FSI_KEYGUARD_SHOWING");
}
@Test
@@ -717,7 +725,7 @@
.isEqualTo(FullScreenIntentDecision.NO_FSI_EXPECTED_TO_HUN);
assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.isFalse();
- verify(mLogger).logNoFullscreen(entry, "Expected to HUN");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_EXPECTED_TO_HUN");
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
verify(mLogger, never()).logFullscreen(any(), any());
}
@@ -737,7 +745,7 @@
.isTrue();
verify(mLogger, never()).logNoFullscreen(any(), any());
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
- verify(mLogger).logFullscreen(entry, "Expected not to HUN");
+ verify(mLogger).logFullscreen(entry, "FSI_EXPECTED_NOT_TO_HUN");
}
@Test
@@ -756,7 +764,7 @@
.isEqualTo(FullScreenIntentDecision.NO_FSI_EXPECTED_TO_HUN);
assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.isFalse();
- verify(mLogger).logNoFullscreen(entry, "Expected to HUN");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_EXPECTED_TO_HUN");
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
verify(mLogger, never()).logFullscreen(any(), any());
}
@@ -802,7 +810,7 @@
.isEqualTo(FullScreenIntentDecision.NO_FSI_EXPECTED_TO_HUN);
assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.isFalse();
- verify(mLogger).logNoFullscreen(entry, "Expected to HUN");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_EXPECTED_TO_HUN");
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
verify(mLogger, never()).logFullscreen(any(), any());
}
@@ -848,12 +856,37 @@
.isEqualTo(FullScreenIntentDecision.NO_FSI_EXPECTED_TO_HUN);
assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.isFalse();
- verify(mLogger).logNoFullscreen(entry, "Expected to HUN");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_EXPECTED_TO_HUN");
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
verify(mLogger, never()).logFullscreen(any(), any());
}
@Test
+ public void logFullScreenIntentDecision_shouldAlmostAlwaysLogOneTime() {
+ NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
+ Set<FullScreenIntentDecision> warnings = new HashSet<>(Arrays.asList(
+ FullScreenIntentDecision.NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR,
+ FullScreenIntentDecision.NO_FSI_NO_HUN_OR_KEYGUARD
+ ));
+ for (FullScreenIntentDecision decision : FullScreenIntentDecision.values()) {
+ clearInvocations(mLogger);
+ boolean expectedToLog = decision != FullScreenIntentDecision.NO_FULL_SCREEN_INTENT;
+ boolean isWarning = warnings.contains(decision);
+ mNotifInterruptionStateProvider.logFullScreenIntentDecision(entry, decision);
+ if (decision.shouldLaunch) {
+ verify(mLogger).logFullscreen(eq(entry), contains(decision.name()));
+ } else if (expectedToLog) {
+ if (isWarning) {
+ verify(mLogger).logNoFullscreenWarning(eq(entry), contains(decision.name()));
+ } else {
+ verify(mLogger).logNoFullscreen(eq(entry), contains(decision.name()));
+ }
+ }
+ verifyNoMoreInteractions(mLogger);
+ }
+ }
+
+ @Test
public void testShouldHeadsUp_snoozed_unlocked_withStrictRules() throws Exception {
when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(true);
NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index f6e5959..542b688 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -18,17 +18,16 @@
import android.content.Intent
import android.telephony.CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL
-import android.telephony.CellSignalStrengthCdma
import android.telephony.NetworkRegistrationInfo
import android.telephony.ServiceState
import android.telephony.ServiceState.STATE_IN_SERVICE
import android.telephony.ServiceState.STATE_OUT_OF_SERVICE
-import android.telephony.SignalStrength
import android.telephony.TelephonyCallback
import android.telephony.TelephonyCallback.DataActivityListener
import android.telephony.TelephonyCallback.ServiceStateListener
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
import android.telephony.TelephonyManager
import android.telephony.TelephonyManager.DATA_ACTIVITY_DORMANT
import android.telephony.TelephonyManager.DATA_ACTIVITY_IN
@@ -68,6 +67,7 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.signalStrength
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
@@ -75,14 +75,12 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.After
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
@@ -99,7 +97,6 @@
@Mock private lateinit var logger: MobileInputLogger
@Mock private lateinit var tableLogger: TableLogBuffer
- private val scope = CoroutineScope(IMMEDIATE)
private val mobileMappings = FakeMobileMappingsProxy()
private val systemUiCarrierConfig =
SystemUiCarrierConfig(
@@ -107,6 +104,9 @@
createTestConfig(),
)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@@ -124,21 +124,16 @@
systemUiCarrierConfig,
fakeBroadcastDispatcher,
mobileMappings,
- IMMEDIATE,
+ testDispatcher,
logger,
tableLogger,
- scope,
+ testScope.backgroundScope,
)
}
- @After
- fun tearDown() {
- scope.cancel()
- }
-
@Test
fun emergencyOnly() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this)
@@ -154,18 +149,15 @@
@Test
fun emergencyOnly_toggles() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this)
val callback = getTelephonyCallbackForType<ServiceStateListener>()
- val serviceState = ServiceState()
- serviceState.isEmergencyOnly = true
- callback.onServiceStateChanged(serviceState)
+ callback.onServiceStateChanged(ServiceState().also { it.isEmergencyOnly = true })
assertThat(latest).isTrue()
- serviceState.isEmergencyOnly = false
- callback.onServiceStateChanged(serviceState)
+ callback.onServiceStateChanged(ServiceState().also { it.isEmergencyOnly = false })
assertThat(latest).isFalse()
@@ -174,7 +166,7 @@
@Test
fun cdmaLevelUpdates() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Int? = null
val job = underTest.cdmaLevel.onEach { latest = it }.launchIn(this)
@@ -194,7 +186,7 @@
@Test
fun gsmLevelUpdates() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Int? = null
val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this)
@@ -214,7 +206,7 @@
@Test
fun isGsm() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isGsm.onEach { latest = it }.launchIn(this)
@@ -234,7 +226,7 @@
@Test
fun dataConnectionState_connected() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -249,7 +241,7 @@
@Test
fun dataConnectionState_connecting() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -264,7 +256,7 @@
@Test
fun dataConnectionState_disconnected() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -279,7 +271,7 @@
@Test
fun dataConnectionState_disconnecting() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -294,7 +286,7 @@
@Test
fun dataConnectionState_suspended() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -309,7 +301,7 @@
@Test
fun dataConnectionState_handoverInProgress() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -324,7 +316,7 @@
@Test
fun dataConnectionState_unknown() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -339,7 +331,7 @@
@Test
fun dataConnectionState_invalid() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -354,7 +346,7 @@
@Test
fun dataActivity() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataActivityModel? = null
val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this)
@@ -368,7 +360,7 @@
@Test
fun carrierNetworkChange() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.carrierNetworkChangeActive.onEach { latest = it }.launchIn(this)
@@ -382,7 +374,7 @@
@Test
fun networkType_default() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: ResolvedNetworkType? = null
val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
@@ -395,7 +387,7 @@
@Test
fun networkType_unknown_hasCorrectKey() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: ResolvedNetworkType? = null
val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
@@ -413,14 +405,19 @@
@Test
fun networkType_updatesUsingDefault() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: ResolvedNetworkType? = null
val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+ val overrideType = OVERRIDE_NETWORK_TYPE_NONE
val type = NETWORK_TYPE_LTE
val expected = DefaultNetworkType(mobileMappings.toIconKey(type))
- val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) }
+ val ti =
+ mock<TelephonyDisplayInfo>().also {
+ whenever(it.overrideNetworkType).thenReturn(overrideType)
+ whenever(it.networkType).thenReturn(type)
+ }
callback.onDisplayInfoChanged(ti)
assertThat(latest).isEqualTo(expected)
@@ -430,7 +427,7 @@
@Test
fun networkType_updatesUsingOverride() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: ResolvedNetworkType? = null
val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
@@ -450,16 +447,38 @@
}
@Test
+ fun networkType_unknownNetworkWithOverride_usesOverrideKey() =
+ testScope.runTest {
+ var latest: ResolvedNetworkType? = null
+ val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+ val unknown = NETWORK_TYPE_UNKNOWN
+ val type = OVERRIDE_NETWORK_TYPE_LTE_CA
+ val expected = OverrideNetworkType(mobileMappings.toIconKeyOverride(type))
+ val ti =
+ mock<TelephonyDisplayInfo>().also {
+ whenever(it.networkType).thenReturn(unknown)
+ whenever(it.overrideNetworkType).thenReturn(type)
+ }
+ callback.onDisplayInfoChanged(ti)
+
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
fun dataEnabled_initial_false() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
assertThat(underTest.dataEnabled.value).isFalse()
}
@Test
- fun `is data enabled - tracks telephony callback`() =
- runBlocking(IMMEDIATE) {
+ fun isDataEnabled_tracksTelephonyCallback() =
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
@@ -479,7 +498,7 @@
@Test
fun numberOfLevels_isDefault() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Int? = null
val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
@@ -489,51 +508,68 @@
}
@Test
- fun `roaming - cdma - queries telephony manager`() =
- runBlocking(IMMEDIATE) {
+ fun roaming_cdma_queriesTelephonyManager() =
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
val cb = getTelephonyCallbackForType<ServiceStateListener>()
- val serviceState = ServiceState()
- serviceState.roaming = false
-
- // CDMA roaming is off, GSM roaming is off
+ // CDMA roaming is off, GSM roaming is on
whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
- cb.onServiceStateChanged(serviceState)
+ cb.onServiceStateChanged(ServiceState().also { it.roaming = true })
assertThat(latest).isFalse()
- // CDMA roaming is off, GSM roaming is on
+ // CDMA roaming is on, GSM roaming is off
whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_ON)
- cb.onServiceStateChanged(serviceState)
+ cb.onServiceStateChanged(ServiceState().also { it.roaming = false })
assertThat(latest).isTrue()
job.cancel()
}
+ /**
+ * [TelephonyManager.getCdmaEnhancedRoamingIndicatorDisplayNumber] returns -1 if the service is
+ * not running or if there is an error while retrieving the cdma ERI
+ */
@Test
- fun `roaming - gsm - queries service state`() =
- runBlocking(IMMEDIATE) {
+ fun cdmaRoaming_ignoresNegativeOne() =
+ testScope.runTest {
var latest: Boolean? = null
- val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+ val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
val serviceState = ServiceState()
serviceState.roaming = false
val cb = getTelephonyCallbackForType<ServiceStateListener>()
- // CDMA roaming is off, GSM roaming is off
- whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
+ // CDMA roaming is unavailable (-1), GSM roaming is off
+ whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(-1)
cb.onServiceStateChanged(serviceState)
assertThat(latest).isFalse()
+ job.cancel()
+ }
+
+ @Test
+ fun roaming_gsm_queriesServiceState() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+
+ val cb = getTelephonyCallbackForType<ServiceStateListener>()
+
+ // CDMA roaming is off, GSM roaming is off
+ whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
+ cb.onServiceStateChanged(ServiceState().also { it.roaming = false })
+
+ assertThat(latest).isFalse()
+
// CDMA roaming is off, GSM roaming is on
- serviceState.roaming = true
- cb.onServiceStateChanged(serviceState)
+ cb.onServiceStateChanged(ServiceState().also { it.roaming = true })
assertThat(latest).isTrue()
@@ -541,8 +577,8 @@
}
@Test
- fun `activity - updates from callback`() =
- runBlocking(IMMEDIATE) {
+ fun activity_updatesFromCallback() =
+ testScope.runTest {
var latest: DataActivityModel? = null
val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this)
@@ -578,8 +614,8 @@
}
@Test
- fun `network name - default`() =
- runBlocking(IMMEDIATE) {
+ fun networkName_default() =
+ testScope.runTest {
var latest: NetworkNameModel? = null
val job = underTest.networkName.onEach { latest = it }.launchIn(this)
@@ -589,8 +625,8 @@
}
@Test
- fun `network name - uses broadcast info - returns derived`() =
- runBlocking(IMMEDIATE) {
+ fun networkName_usesBroadcastInfo_returnsDerived() =
+ testScope.runTest {
var latest: NetworkNameModel? = null
val job = underTest.networkName.onEach { latest = it }.launchIn(this)
@@ -606,8 +642,8 @@
}
@Test
- fun `network name - broadcast not for this sub id - keeps old value`() =
- runBlocking(IMMEDIATE) {
+ fun networkName_broadcastNotForThisSubId_keepsOldValue() =
+ testScope.runTest {
var latest: NetworkNameModel? = null
val job = underTest.networkName.onEach { latest = it }.launchIn(this)
@@ -631,8 +667,8 @@
}
@Test
- fun `network name - broadcast has no data - updates to default`() =
- runBlocking(IMMEDIATE) {
+ fun networkName_broadcastHasNoData_updatesToDefault() =
+ testScope.runTest {
var latest: NetworkNameModel? = null
val job = underTest.networkName.onEach { latest = it }.launchIn(this)
@@ -658,8 +694,8 @@
}
@Test
- fun `operatorAlphaShort - tracked`() =
- runBlocking(IMMEDIATE) {
+ fun operatorAlphaShort_tracked() =
+ testScope.runTest {
var latest: String? = null
val job = underTest.operatorAlphaShort.onEach { latest = it }.launchIn(this)
@@ -680,33 +716,45 @@
}
@Test
- fun `connection model - isInService - not iwlan`() =
- runBlocking(IMMEDIATE) {
+ fun isInService_notIwlan() =
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isInService.onEach { latest = it }.launchIn(this)
- val serviceState = ServiceState()
- serviceState.voiceRegState = STATE_IN_SERVICE
- serviceState.dataRegState = STATE_IN_SERVICE
-
- getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+ getTelephonyCallbackForType<ServiceStateListener>()
+ .onServiceStateChanged(
+ ServiceState().also {
+ it.voiceRegState = STATE_IN_SERVICE
+ it.dataRegState = STATE_IN_SERVICE
+ }
+ )
assertThat(latest).isTrue()
- serviceState.voiceRegState = STATE_OUT_OF_SERVICE
- getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+ getTelephonyCallbackForType<ServiceStateListener>()
+ .onServiceStateChanged(
+ ServiceState().also {
+ it.dataRegState = STATE_IN_SERVICE
+ it.voiceRegState = STATE_OUT_OF_SERVICE
+ }
+ )
assertThat(latest).isTrue()
- serviceState.dataRegState = STATE_OUT_OF_SERVICE
- getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+ getTelephonyCallbackForType<ServiceStateListener>()
+ .onServiceStateChanged(
+ ServiceState().also {
+ it.voiceRegState = STATE_OUT_OF_SERVICE
+ it.dataRegState = STATE_OUT_OF_SERVICE
+ }
+ )
assertThat(latest).isFalse()
job.cancel()
}
@Test
- fun `connection model - isInService - is iwlan - voice out of service - data in service`() =
- runBlocking(IMMEDIATE) {
+ fun isInService_isIwlan_voiceOutOfService_dataInService() =
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isInService.onEach { latest = it }.launchIn(this)
@@ -730,8 +778,8 @@
}
@Test
- fun `number of levels - uses carrier config`() =
- runBlocking(IMMEDIATE) {
+ fun numberOfLevels_usesCarrierConfig() =
+ testScope.runTest {
var latest: Int? = null
val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
@@ -756,19 +804,6 @@
return MobileTelephonyHelpers.getTelephonyCallbackForType(telephonyManager)
}
- /** Convenience constructor for SignalStrength */
- private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
- val signalStrength = mock<SignalStrength>()
- whenever(signalStrength.isGsm).thenReturn(isGsm)
- whenever(signalStrength.level).thenReturn(gsmLevel)
- val cdmaStrength =
- mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
- whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
- .thenReturn(listOf(cdmaStrength))
-
- return signalStrength
- }
-
private fun spnIntent(
subId: Int = SUB_1_ID,
showSpn: Boolean = true,
@@ -785,7 +820,6 @@
}
companion object {
- private val IMMEDIATE = Dispatchers.Main.immediate
private const val SUB_1_ID = 1
private val DEFAULT_NAME = NetworkNameModel.Default("default name")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt
new file mode 100644
index 0000000..bbf04ed2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import android.telephony.ServiceState
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.CarrierNetworkListener
+import android.telephony.TelephonyCallback.DataActivityListener
+import android.telephony.TelephonyCallback.DataConnectionStateListener
+import android.telephony.TelephonyCallback.DataEnabledListener
+import android.telephony.TelephonyCallback.DisplayInfoListener
+import android.telephony.TelephonyCallback.ServiceStateListener
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
+import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.getTelephonyCallbackForType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.signalStrength
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+/**
+ * Test class to stress test the TelephonyCallbacks that we listen to. In particular, the callbacks
+ * all come back in on a single listener (for reasons defined in the system). This test is built to
+ * ensure that we don't miss any important callbacks.
+ *
+ * Kind of like an interaction test case build just for [TelephonyCallback]
+ *
+ * The list of telephony callbacks we use is: [TelephonyCallback.CarrierNetworkListener]
+ * [TelephonyCallback.DataActivityListener] [TelephonyCallback.DataConnectionStateListener]
+ * [TelephonyCallback.DataEnabledListener] [TelephonyCallback.DisplayInfoListener]
+ * [TelephonyCallback.ServiceStateListener] [TelephonyCallback.SignalStrengthsListener]
+ *
+ * Because each of these callbacks comes in on the same callbackFlow, collecting on a field backed
+ * by only a single callback can immediately create backpressure on the other fields related to a
+ * mobile connection.
+ *
+ * This test should be designed to test _at least_ each individual callback in a smoke-test fashion.
+ * The way we will achieve this is as follows:
+ * 1. Start up a listener (A) collecting on a field which is _not under test_
+ * 2. Send a single event to a telephony callback which supports the field under test (B)
+ * 3. Send many (may be as few as 2) events to the callback backing A to ensure we start seeing
+ * backpressure on other fields NOTE: poor handling of backpressure here would normally cause B
+ * to get dropped
+ * 4. Start up a new collector for B
+ * 5. Assert that B has the state sent in step #2
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileConnectionTelephonySmokeTests : SysuiTestCase() {
+ private lateinit var underTest: MobileConnectionRepositoryImpl
+ private lateinit var connectionsRepo: FakeMobileConnectionsRepository
+
+ @Mock private lateinit var telephonyManager: TelephonyManager
+ @Mock private lateinit var logger: MobileInputLogger
+ @Mock private lateinit var tableLogger: TableLogBuffer
+
+ private val mobileMappings = FakeMobileMappingsProxy()
+ private val systemUiCarrierConfig =
+ SystemUiCarrierConfig(
+ SUB_1_ID,
+ SystemUiCarrierConfigTest.createTestConfig(),
+ )
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
+
+ connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogger)
+
+ underTest =
+ MobileConnectionRepositoryImpl(
+ context,
+ SUB_1_ID,
+ DEFAULT_NAME,
+ SEP,
+ telephonyManager,
+ systemUiCarrierConfig,
+ fakeBroadcastDispatcher,
+ mobileMappings,
+ testDispatcher,
+ logger,
+ tableLogger,
+ testScope.backgroundScope,
+ )
+ }
+
+ @Test
+ fun carrierNetworkChangeListener_noisyActivity() =
+ testScope.runTest {
+ var latest: Boolean? = null
+
+ // Start collecting data activity; don't care about the result
+ val activityJob = underTest.dataActivityDirection.launchIn(this)
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+
+ val callback = getTelephonyCallbackForType<CarrierNetworkListener>()
+ callback.onCarrierNetworkChange(true)
+
+ flipActivity(100, activityCallback)
+
+ val job = underTest.carrierNetworkChangeActive.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isTrue()
+
+ activityJob.cancel()
+ job.cancel()
+ }
+
+ @Test
+ fun dataActivityLate_noisyDisplayInfo() =
+ testScope.runTest {
+ var latest: DataActivityModel? = null
+
+ // start collecting displayInfo; don't care about the result
+ val displayInfoJob = underTest.resolvedNetworkType.launchIn(this)
+
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+ activityCallback.onDataActivity(DATA_ACTIVITY_INOUT)
+
+ val displayInfoCallback = getTelephonyCallbackForType<DisplayInfoListener>()
+ val type1 = NETWORK_TYPE_UNKNOWN
+ val type2 = NETWORK_TYPE_LTE
+ val t1 =
+ mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type1) }
+ val t2 =
+ mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type2) }
+
+ flipDisplayInfo(100, listOf(t1, t2), displayInfoCallback)
+
+ val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest)
+ .isEqualTo(
+ DataActivityModel(
+ hasActivityIn = true,
+ hasActivityOut = true,
+ )
+ )
+
+ displayInfoJob.cancel()
+ job.cancel()
+ }
+
+ @Test
+ fun dataConnectionStateListener_noisyActivity() =
+ testScope.runTest {
+ var latest: DataConnectionState? = null
+
+ // Start collecting data activity; don't care about the result
+ val activityJob = underTest.dataActivityDirection.launchIn(this)
+
+ val connectionCallback = getTelephonyCallbackForType<DataConnectionStateListener>()
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+
+ connectionCallback.onDataConnectionStateChanged(
+ TelephonyManager.DATA_CONNECTED,
+ 200 /* unused */
+ )
+
+ // Send a bunch of events that we don't care about, to overrun the replay buffer
+ flipActivity(100, activityCallback)
+
+ val connectionJob = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(DataConnectionState.Connected)
+
+ activityJob.cancel()
+ connectionJob.cancel()
+ }
+
+ @Test
+ fun dataEnabledLate_noisyActivity() =
+ testScope.runTest {
+ var latest: Boolean? = null
+
+ // Start collecting data activity; don't care about the result
+ val activityJob = underTest.dataActivityDirection.launchIn(this)
+
+ val enabledCallback = getTelephonyCallbackForType<DataEnabledListener>()
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+
+ enabledCallback.onDataEnabledChanged(true, 1 /* unused */)
+
+ // Send a bunch of events that we don't care about, to overrun the replay buffer
+ flipActivity(100, activityCallback)
+
+ val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isTrue()
+
+ activityJob.cancel()
+ job.cancel()
+ }
+
+ @Test
+ fun displayInfoLate_noisyActivity() =
+ testScope.runTest {
+ var latest: ResolvedNetworkType? = null
+
+ // Start collecting data activity; don't care about the result
+ val activityJob = underTest.dataActivityDirection.launchIn(this)
+
+ val displayInfoCallback = getTelephonyCallbackForType<DisplayInfoListener>()
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+
+ val type = NETWORK_TYPE_LTE
+ val expected = ResolvedNetworkType.DefaultNetworkType(mobileMappings.toIconKey(type))
+ val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) }
+ displayInfoCallback.onDisplayInfoChanged(ti)
+
+ // Send a bunch of events that we don't care about, to overrun the replay buffer
+ flipActivity(100, activityCallback)
+
+ val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(expected)
+
+ activityJob.cancel()
+ job.cancel()
+ }
+
+ @Test
+ fun serviceStateListener_noisyActivity() =
+ testScope.runTest {
+ var latest: Boolean? = null
+
+ // Start collecting data activity; don't care about the result
+ val activityJob = underTest.dataActivityDirection.launchIn(this)
+
+ val serviceStateCallback = getTelephonyCallbackForType<ServiceStateListener>()
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+
+ // isEmergencyOnly comes in
+ val serviceState = ServiceState()
+ serviceState.isEmergencyOnly = true
+ serviceStateCallback.onServiceStateChanged(serviceState)
+
+ flipActivity(100, activityCallback)
+
+ val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isTrue()
+
+ activityJob.cancel()
+ job.cancel()
+ }
+
+ @Test
+ fun signalStrengthsListenerLate_noisyActivity() =
+ testScope.runTest {
+ var latest: Int? = null
+
+ // Start collecting data activity; don't care about the result
+ val activityJob = underTest.dataActivityDirection.launchIn(this)
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
+ val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
+ callback.onSignalStrengthsChanged(strength)
+
+ flipActivity(100, activityCallback)
+
+ val job = underTest.cdmaLevel.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(2)
+
+ activityJob.cancel()
+ job.cancel()
+ }
+
+ private fun flipActivity(
+ times: Int,
+ callback: DataActivityListener,
+ ) {
+ repeat(times) { index -> callback.onDataActivity(index % 4) }
+ }
+
+ private fun flipDisplayInfo(
+ times: Int,
+ infos: List<TelephonyDisplayInfo>,
+ callback: DisplayInfoListener,
+ ) {
+ val len = infos.size
+ repeat(times) { index -> callback.onDisplayInfoChanged(infos[index % len]) }
+ }
+
+ private inline fun <reified T> getTelephonyCallbackForType(): T {
+ return getTelephonyCallbackForType(telephonyManager)
+ }
+
+ companion object {
+ private const val SUB_1_ID = 1
+
+ private val DEFAULT_NAME = NetworkNameModel.Default("default name")
+ private const val SEP = "-"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileTelephonyHelpers.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileTelephonyHelpers.kt
index 621f793..d07b96f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileTelephonyHelpers.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileTelephonyHelpers.kt
@@ -16,10 +16,14 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.SignalStrength
import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.mockito.Mockito.verify
@@ -31,6 +35,19 @@
return callbackCaptor.allValues
}
+ /** Convenience constructor for SignalStrength */
+ fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
+ val signalStrength = mock<SignalStrength>()
+ whenever(signalStrength.isGsm).thenReturn(isGsm)
+ whenever(signalStrength.level).thenReturn(gsmLevel)
+ val cdmaStrength =
+ mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
+ whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
+ .thenReturn(listOf(cdmaStrength))
+
+ return signalStrength
+ }
+
inline fun <reified T> getTelephonyCallbackForType(mockTelephonyManager: TelephonyManager): T {
val cbs = getTelephonyCallbacks(mockTelephonyManager).filterIsInstance<T>()
assertThat(cbs.size).isEqualTo(1)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt
new file mode 100644
index 0000000..71bd511
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.surfaceeffects.turbulencenoise
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TurbulenceNoiseShaderTest : SysuiTestCase() {
+
+ private lateinit var turbulenceNoiseShader: TurbulenceNoiseShader
+
+ @Test
+ fun compliesSimplexNoise() {
+ turbulenceNoiseShader = TurbulenceNoiseShader()
+ }
+
+ @Test
+ fun compliesFractalNoise() {
+ turbulenceNoiseShader = TurbulenceNoiseShader(useFractal = true)
+ }
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 607439b..bd67889 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -23,6 +23,7 @@
import android.hardware.display.DisplayManagerInternal;
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
+import android.hardware.input.InputManagerGlobal;
import android.hardware.input.VirtualKeyEvent;
import android.hardware.input.VirtualMouseButtonEvent;
import android.hardware.input.VirtualMouseRelativeEvent;
@@ -686,7 +687,7 @@
mListener = new InputManager.InputDeviceListener() {
@Override
public void onInputDeviceAdded(int deviceId) {
- final InputDevice device = InputManager.getInstance().getInputDevice(
+ final InputDevice device = InputManagerGlobal.getInstance().getInputDevice(
deviceId);
Objects.requireNonNull(device, "Newly added input device was null.");
if (!device.getName().equals(deviceName)) {
diff --git a/services/core/java/com/android/server/OWNERS b/services/core/java/com/android/server/OWNERS
index c441859..409f054 100644
--- a/services/core/java/com/android/server/OWNERS
+++ b/services/core/java/com/android/server/OWNERS
@@ -35,7 +35,7 @@
per-file NetIdManager.java = file:/services/core/java/com/android/server/net/OWNERS
per-file PackageWatchdog.java, RescueParty.java = file:/services/core/java/com/android/server/rollback/OWNERS
per-file PinnerService.java = file:/apct-tests/perftests/OWNERS
-per-file RescueParty.java = fdunlap@google.com, shuc@google.com
+per-file RescueParty.java = fdunlap@google.com, shuc@google.com, ancr@google.com, harshitmahajan@google.com
per-file SystemClockTime.java = file:/services/core/java/com/android/server/timedetector/OWNERS
per-file SystemTimeZone.java = file:/services/core/java/com/android/server/timezonedetector/OWNERS
per-file TelephonyRegistry.java = file:/telephony/OWNERS
diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java
index d1e0f16..3de65f9 100644
--- a/services/core/java/com/android/server/RescueParty.java
+++ b/services/core/java/com/android/server/RescueParty.java
@@ -79,6 +79,7 @@
static final String PROP_ATTEMPTING_FACTORY_RESET = "sys.attempting_factory_reset";
static final String PROP_ATTEMPTING_REBOOT = "sys.attempting_reboot";
static final String PROP_MAX_RESCUE_LEVEL_ATTEMPTED = "sys.max_rescue_level_attempted";
+ static final String PROP_LAST_FACTORY_RESET_TIME_MS = "persist.sys.last_factory_reset";
@VisibleForTesting
static final int LEVEL_NONE = 0;
@VisibleForTesting
@@ -105,10 +106,11 @@
@VisibleForTesting
static final String NAMESPACE_TO_PACKAGE_MAPPING_FLAG =
"namespace_to_package_mapping";
+ @VisibleForTesting
+ static final long FACTORY_RESET_THROTTLE_DURATION_MS = TimeUnit.MINUTES.toMillis(10);
private static final String NAME = "rescue-party-observer";
-
private static final String PROP_DISABLE_RESCUE = "persist.sys.disable_rescue";
private static final String PROP_VIRTUAL_DEVICE = "ro.hardware.virtual_device";
private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG =
@@ -327,8 +329,8 @@
}
}
- private static int getMaxRescueLevel(boolean mayPerformFactoryReset) {
- if (!mayPerformFactoryReset
+ private static int getMaxRescueLevel(boolean mayPerformReboot) {
+ if (!mayPerformReboot
|| SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
}
@@ -339,11 +341,11 @@
* Get the rescue level to perform if this is the n-th attempt at mitigating failure.
*
* @param mitigationCount: the mitigation attempt number (1 = first attempt etc.)
- * @param mayPerformFactoryReset: whether or not a factory reset may be performed for the given
- * failure.
+ * @param mayPerformReboot: whether or not a reboot and factory reset may be performed
+ * for the given failure.
* @return the rescue level for the n-th mitigation attempt.
*/
- private static int getRescueLevel(int mitigationCount, boolean mayPerformFactoryReset) {
+ private static int getRescueLevel(int mitigationCount, boolean mayPerformReboot) {
if (mitigationCount == 1) {
return LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS;
} else if (mitigationCount == 2) {
@@ -351,9 +353,9 @@
} else if (mitigationCount == 3) {
return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
} else if (mitigationCount == 4) {
- return Math.min(getMaxRescueLevel(mayPerformFactoryReset), LEVEL_WARM_REBOOT);
+ return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_WARM_REBOOT);
} else if (mitigationCount >= 5) {
- return Math.min(getMaxRescueLevel(mayPerformFactoryReset), LEVEL_FACTORY_RESET);
+ return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_FACTORY_RESET);
} else {
Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount);
return LEVEL_NONE;
@@ -450,6 +452,8 @@
break;
}
SystemProperties.set(PROP_ATTEMPTING_FACTORY_RESET, "true");
+ long now = System.currentTimeMillis();
+ SystemProperties.set(PROP_LAST_FACTORY_RESET_TIME_MS, Long.toString(now));
runnable = new Runnable() {
@Override
public void run() {
@@ -627,7 +631,7 @@
if (!isDisabled() && (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH
|| failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING)) {
return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
- mayPerformFactoryReset(failedPackage)));
+ mayPerformReboot(failedPackage)));
} else {
return PackageHealthObserverImpact.USER_IMPACT_NONE;
}
@@ -642,7 +646,7 @@
if (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH
|| failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING) {
final int level = getRescueLevel(mitigationCount,
- mayPerformFactoryReset(failedPackage));
+ mayPerformReboot(failedPackage));
executeRescueLevel(mContext,
failedPackage == null ? null : failedPackage.getPackageName(), level);
return true;
@@ -683,8 +687,9 @@
if (isDisabled()) {
return false;
}
+ boolean mayPerformReboot = !shouldThrottleReboot();
executeRescueLevel(mContext, /*failedPackage=*/ null,
- getRescueLevel(mitigationCount, true));
+ getRescueLevel(mitigationCount, mayPerformReboot));
return true;
}
@@ -698,14 +703,27 @@
* prompting a factory reset is an acceptable mitigation strategy for the package's
* failure, {@code false} otherwise.
*/
- private boolean mayPerformFactoryReset(@Nullable VersionedPackage failingPackage) {
+ private boolean mayPerformReboot(@Nullable VersionedPackage failingPackage) {
if (failingPackage == null) {
return false;
}
+ if (shouldThrottleReboot()) {
+ return false;
+ }
return isPersistentSystemApp(failingPackage.getPackageName());
}
+ /**
+ * Returns {@code true} if Rescue Party is allowed to attempt a reboot or factory reset.
+ * Will return {@code false} if a factory reset was already offered recently.
+ */
+ private boolean shouldThrottleReboot() {
+ Long lastResetTime = SystemProperties.getLong(PROP_LAST_FACTORY_RESET_TIME_MS, 0);
+ long now = System.currentTimeMillis();
+ return now < lastResetTime + FACTORY_RESET_THROTTLE_DURATION_MS;
+ }
+
private boolean isPersistentSystemApp(@NonNull String packageName) {
PackageManager pm = mContext.getPackageManager();
try {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index c5008fa..4a0a228 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -252,12 +252,6 @@
private static final boolean LOG_SERVICE_START_STOP = DEBUG_SERVICE;
- // How long we wait for a service to finish executing.
- static final int SERVICE_TIMEOUT = 20 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
-
- // How long we wait for a service to finish executing.
- static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;
-
// Foreground service types that always get immediate notification display,
// expressed in the same bitmask format that ServiceRecord.foregroundServiceType
// uses.
@@ -337,6 +331,13 @@
final ArrayMap<ForegroundServiceDelegation, ServiceRecord> mFgsDelegations = new ArrayMap<>();
/**
+ * A global counter for generating sequence numbers to uniquely identify bindService requests.
+ * It is purely for logging purposes.
+ */
+ @GuardedBy("mAm")
+ private long mBindServiceSeqCounter = 0;
+
+ /**
* Whether there is a rate limit that suppresses immediate re-deferral of new FGS
* notifications from each app. On by default, disabled only by shell command for
* test-suite purposes. To disable the behavior more generally, use the usual
@@ -4429,8 +4430,12 @@
try {
bumpServiceExecutingLocked(r, execInFg, "bind",
OomAdjuster.OOM_ADJ_REASON_BIND_SERVICE);
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+ Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, "requestServiceBinding="
+ + i.intent.getIntent() + ". bindSeq=" + mBindServiceSeqCounter);
+ }
r.app.getThread().scheduleBindService(r, i.intent.getIntent(), rebind,
- r.app.mState.getReportedProcState());
+ r.app.mState.getReportedProcState(), mBindServiceSeqCounter++);
if (!rebind) {
i.requested = true;
}
@@ -6598,13 +6603,15 @@
return;
}
final ProcessServiceRecord psr = proc.mServices;
- if (psr.numberOfExecutingServices() == 0 || proc.getThread() == null) {
+ if (psr.numberOfExecutingServices() == 0 || proc.getThread() == null
+ || proc.isKilled()) {
return;
}
final long now = SystemClock.uptimeMillis();
final long maxTime = now
- (psr.shouldExecServicesFg()
- ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
+ ? mAm.mConstants.SERVICE_TIMEOUT
+ : mAm.mConstants.SERVICE_BACKGROUND_TIMEOUT);
ServiceRecord timeout = null;
long nextTime = 0;
for (int i = psr.numberOfExecutingServices() - 1; i >= 0; i--) {
@@ -6635,8 +6642,8 @@
ActivityManagerService.SERVICE_TIMEOUT_MSG);
msg.obj = proc;
mAm.mHandler.sendMessageAtTime(msg, psr.shouldExecServicesFg()
- ? (nextTime + SERVICE_TIMEOUT) :
- (nextTime + SERVICE_BACKGROUND_TIMEOUT));
+ ? (nextTime + mAm.mConstants.SERVICE_TIMEOUT) :
+ (nextTime + mAm.mConstants.SERVICE_BACKGROUND_TIMEOUT));
}
}
@@ -6732,7 +6739,7 @@
ActivityManagerService.SERVICE_TIMEOUT_MSG);
msg.obj = proc;
mAm.mHandler.sendMessageDelayed(msg, proc.mServices.shouldExecServicesFg()
- ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
+ ? mAm.mConstants.SERVICE_TIMEOUT : mAm.mConstants.SERVICE_BACKGROUND_TIMEOUT);
}
void scheduleServiceForegroundTransitionTimeoutLocked(ServiceRecord r) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 5696004..4fa28a1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -248,7 +248,16 @@
private static final long DEFAULT_SERVICE_BIND_ALMOST_PERCEPTIBLE_TIMEOUT_MS = 15 * 1000;
- // Flag stored in the DeviceConfig API.
+ /**
+ * Default value to {@link #SERVICE_TIMEOUT}.
+ */
+ private static final long DEFAULT_SERVICE_TIMEOUT = 20 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
+
+ /**
+ * Default value to {@link #SERVICE_BACKGROUND_TIMEOUT}.
+ */
+ private static final long DEFAULT_SERVICE_BACKGROUND_TIMEOUT = DEFAULT_SERVICE_TIMEOUT * 10;
+
/**
* Maximum number of cached processes.
*/
@@ -506,6 +515,12 @@
// to restart less than this amount of time from the last one.
public long SERVICE_MIN_RESTART_TIME_BETWEEN = DEFAULT_SERVICE_MIN_RESTART_TIME_BETWEEN;
+ // How long we wait for a service to finish executing.
+ long SERVICE_TIMEOUT = DEFAULT_SERVICE_TIMEOUT;
+
+ // How long we wait for a service to finish executing.
+ long SERVICE_BACKGROUND_TIMEOUT = DEFAULT_SERVICE_BACKGROUND_TIMEOUT;
+
// Maximum amount of time for there to be no activity on a service before
// we consider it non-essential and allow its process to go on the
// LRU background list.
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 8ae9550..7550196 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -19717,8 +19717,9 @@
final long identity = Binder.clearCallingIdentity();
try {
return superImpl.apply(code, new AttributionSource(shellUid,
- "com.android.shell", attributionSource.getAttributionTag(),
- attributionSource.getToken(), attributionSource.getNext()),
+ Process.INVALID_PID, "com.android.shell",
+ attributionSource.getAttributionTag(), attributionSource.getToken(),
+ /*renouncedPermissions*/ null, attributionSource.getNext()),
shouldCollectAsyncNotedOp, message, shouldCollectMessage,
skiProxyOperation);
} finally {
@@ -19769,8 +19770,9 @@
final long identity = Binder.clearCallingIdentity();
try {
return superImpl.apply(clientId, code, new AttributionSource(shellUid,
- "com.android.shell", attributionSource.getAttributionTag(),
- attributionSource.getToken(), attributionSource.getNext()),
+ Process.INVALID_PID, "com.android.shell",
+ attributionSource.getAttributionTag(), attributionSource.getToken(),
+ /*renouncedPermissions*/ null, attributionSource.getNext()),
startIfModeDefault, shouldCollectAsyncNotedOp, message,
shouldCollectMessage, skipProxyOperation, proxyAttributionFlags,
proxiedAttributionFlags, attributionChainId);
@@ -19794,8 +19796,9 @@
final long identity = Binder.clearCallingIdentity();
try {
superImpl.apply(clientId, code, new AttributionSource(shellUid,
- "com.android.shell", attributionSource.getAttributionTag(),
- attributionSource.getToken(), attributionSource.getNext()),
+ Process.INVALID_PID, "com.android.shell",
+ attributionSource.getAttributionTag(), attributionSource.getToken(),
+ /*renouncedPermissions*/ null, attributionSource.getNext()),
skipProxyOperation);
} finally {
Binder.restoreCallingIdentity(identity);
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index f236a96..d09ca5c 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -30,7 +30,6 @@
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.RequiresNoPermission;
-import android.app.AlarmManager;
import android.app.StatsManager;
import android.app.usage.NetworkStatsManager;
import android.bluetooth.BluetoothActivityEnergyInfo;
@@ -414,18 +413,6 @@
Slog.e(TAG, "Could not register INetworkManagement event observer " + e);
}
- final AlarmManager am = mContext.getSystemService(AlarmManager.class);
- mHandler.post(() -> {
- synchronized (mStats) {
- mStats.setLongPlugInAlarmInterface(new AlarmInterface(am, () -> {
- synchronized (mStats) {
- if (mStats.isOnBattery()) return;
- mStats.maybeResetWhilePluggedInLocked();
- }
- }));
- }
- });
-
synchronized (mPowerStatsLock) {
mPowerStatsInternal = LocalServices.getService(PowerStatsInternal.class);
if (mPowerStatsInternal != null) {
@@ -2529,32 +2516,6 @@
}
}
- final class AlarmInterface implements BatteryStatsImpl.AlarmInterface,
- AlarmManager.OnAlarmListener {
- private AlarmManager mAm;
- private Runnable mOnAlarm;
-
- AlarmInterface(AlarmManager am, Runnable onAlarm) {
- mAm = am;
- mOnAlarm = onAlarm;
- }
-
- @Override
- public void schedule(long rtcTimeMs, long windowLengthMs) {
- mAm.setWindow(AlarmManager.RTC, rtcTimeMs, windowLengthMs, TAG, this, mHandler);
- }
-
- @Override
- public void cancel() {
- mAm.cancel(this);
- }
-
- @Override
- public void onAlarm() {
- mOnAlarm.run();
- }
- }
-
private static native int nativeWaitWakeup(ByteBuffer outBuffer);
private void dumpHelp(PrintWriter pw) {
diff --git a/services/core/java/com/android/server/am/CoreSettingsObserver.java b/services/core/java/com/android/server/am/CoreSettingsObserver.java
index ddc9e91..844f175 100644
--- a/services/core/java/com/android/server/am/CoreSettingsObserver.java
+++ b/services/core/java/com/android/server/am/CoreSettingsObserver.java
@@ -25,6 +25,7 @@
import android.os.Bundle;
import android.provider.DeviceConfig;
import android.provider.Settings;
+import android.text.TextFlags;
import android.widget.WidgetFlags;
import com.android.internal.R;
@@ -162,6 +163,11 @@
DeviceConfig.NAMESPACE_WIDGET, WidgetFlags.MAGNIFIER_ASPECT_RATIO,
WidgetFlags.KEY_MAGNIFIER_ASPECT_RATIO, float.class,
WidgetFlags.MAGNIFIER_ASPECT_RATIO_DEFAULT));
+
+ sDeviceConfigEntries.add(new DeviceConfigEntry<Boolean>(
+ TextFlags.NAMESPACE, TextFlags.ENABLE_NEW_CONTEXT_MENU,
+ TextFlags.KEY_ENABLE_NEW_CONTEXT_MENU, boolean.class,
+ TextFlags.ENABLE_NEW_CONTEXT_MENU_DEFAULT));
// add other device configs here...
}
private static volatile boolean sDeviceConfigContextEntriesLoaded = false;
diff --git a/services/core/java/com/android/server/am/ProcessStatsService.java b/services/core/java/com/android/server/am/ProcessStatsService.java
index bac9253..8e93c1b 100644
--- a/services/core/java/com/android/server/am/ProcessStatsService.java
+++ b/services/core/java/com/android/server/am/ProcessStatsService.java
@@ -69,7 +69,7 @@
// define the encoding of that data in an integer.
static final int MAX_HISTORIC_STATES = 8; // Maximum number of historic states we will keep.
- static final String STATE_FILE_PREFIX = "state-"; // Prefix to use for state filenames.
+ static final String STATE_FILE_PREFIX = "state-v2-"; // Prefix to use for state filenames.
static final String STATE_FILE_SUFFIX = ".bin"; // Suffix to use for state filenames.
static final String STATE_FILE_CHECKIN_SUFFIX = ".ci"; // State files that have checked in.
static long WRITE_PERIOD = 30*60*1000; // Write file every 30 minutes or so.
@@ -462,6 +462,10 @@
File file = files[i];
String fileStr = file.getPath();
if (DEBUG) Slog.d(TAG, "Collecting: " + fileStr);
+ if (!file.getName().startsWith(STATE_FILE_PREFIX)) {
+ if (DEBUG) Slog.d(TAG, "Skipping: mismatching prefix");
+ continue;
+ }
if (!inclCheckedIn && fileStr.endsWith(STATE_FILE_CHECKIN_SUFFIX)) {
if (DEBUG) Slog.d(TAG, "Skipping: already checked in");
continue;
@@ -478,6 +482,14 @@
@GuardedBy("mFileLock")
private void trimHistoricStatesWriteLF() {
+ File[] files = mBaseDir.listFiles();
+ if (files != null) {
+ for (int i = 0; i < files.length; i++) {
+ if (!files[i].getName().startsWith(STATE_FILE_PREFIX)) {
+ files[i].delete();
+ }
+ }
+ }
ArrayList<String> filesArray = getCommittedFilesLF(MAX_HISTORIC_STATES, false, true);
if (filesArray == null) {
return;
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index db6944d0..3864200 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -2088,7 +2088,7 @@
}
@VisibleForTesting
- void onRefreshRateSettingChangedLocked(float min, float max) {
+ public void onRefreshRateSettingChangedLocked(float min, float max) {
boolean changeable = (max - min > 1f && max > 60f);
if (mRefreshRateChangeable != changeable) {
mRefreshRateChangeable = changeable;
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 4d03e44..7e990c6 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -96,7 +96,11 @@
*/
public abstract int getVirtualMousePointerDisplayId();
- /** Gets the current position of the mouse cursor. */
+ /**
+ * Gets the current position of the mouse cursor.
+ *
+ * Returns NaN-s as the coordinates if the cursor is not available.
+ */
public abstract PointF getCursorPosition();
/**
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index b2b22a0..efc4f11 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2863,9 +2863,6 @@
int getPointerDisplayId();
- /** Gets the x and y coordinates of the cursor's current position. */
- PointF getCursorPosition();
-
/**
* Notifies window manager that a {@link android.view.MotionEvent#ACTION_DOWN} pointer event
* occurred on a window that did not have focus.
@@ -3189,7 +3186,11 @@
@Override
public PointF getCursorPosition() {
- return mWindowManagerCallbacks.getCursorPosition();
+ final float[] p = mNative.getMouseCursorPosition();
+ if (p == null || p.length != 2) {
+ throw new IllegalStateException("Failed to get mouse cursor position");
+ }
+ return new PointF(p[0], p[1]);
}
@Override
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 22226e8..5395302d 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -224,6 +224,16 @@
/** Set whether stylus button reporting through motion events should be enabled. */
void setStylusButtonMotionEventsEnabled(boolean enabled);
+ /**
+ * Get the current position of the mouse cursor.
+ *
+ * If the mouse cursor is not currently shown, the coordinate values will be NaN-s.
+ *
+ * NOTE: This will grab the PointerController's lock, so we must be careful about calling this
+ * from the InputReader or Display threads, which may result in a deadlock.
+ */
+ float[] getMouseCursorPosition();
+
/** The native implementation of InputManagerService methods. */
class NativeImpl implements NativeInputManagerService {
/** Pointer to native input manager service object, used by native code. */
@@ -465,5 +475,8 @@
@Override
public native void setStylusButtonMotionEventsEnabled(boolean enabled);
+
+ @Override
+ public native float[] getMouseCursorPosition();
}
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 91f91f8..e32bf1b 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1488,16 +1488,19 @@
}
int change = isPackageDisappearing(imi.getPackageName());
- if (isPackageModified(imi.getPackageName())) {
- mAdditionalSubtypeMap.remove(imi.getId());
- AdditionalSubtypeUtils.save(mAdditionalSubtypeMap, mMethodMap,
- mSettings.getCurrentUserId());
- }
if (change == PACKAGE_TEMPORARY_CHANGE
|| change == PACKAGE_PERMANENT_CHANGE) {
Slog.i(TAG, "Input method uninstalled, disabling: "
+ imi.getComponent());
setInputMethodEnabledLocked(imi.getId(), false);
+ } else if (change == PACKAGE_UPDATING) {
+ Slog.i(TAG,
+ "Input method reinstalling, clearing additional subtypes: "
+ + imi.getComponent());
+ mAdditionalSubtypeMap.remove(imi.getId());
+ AdditionalSubtypeUtils.save(mAdditionalSubtypeMap,
+ mMethodMap,
+ mSettings.getCurrentUserId());
}
}
}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index afae08d..2b5f874 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -421,6 +421,8 @@
static class Injector {
protected Context mContext;
+ private ServiceThread mHandlerThread;
+ private Handler mHandler;
public Injector(Context context) {
mContext = context;
@@ -431,14 +433,20 @@
}
public ServiceThread getServiceThread() {
- ServiceThread handlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND,
- true /*allowIo*/);
- handlerThread.start();
- return handlerThread;
+ if (mHandlerThread == null) {
+ mHandlerThread = new ServiceThread(TAG,
+ Process.THREAD_PRIORITY_BACKGROUND,
+ true /*allowIo*/);
+ mHandlerThread.start();
+ }
+ return mHandlerThread;
}
public Handler getHandler(ServiceThread handlerThread) {
- return new Handler(handlerThread.getLooper());
+ if (mHandler == null) {
+ mHandler = new Handler(handlerThread.getLooper());
+ }
+ return mHandler;
}
public LockSettingsStorage getStorage() {
@@ -519,7 +527,8 @@
public RebootEscrowManager getRebootEscrowManager(RebootEscrowManager.Callbacks callbacks,
LockSettingsStorage storage) {
- return new RebootEscrowManager(mContext, callbacks, storage);
+ return new RebootEscrowManager(mContext, callbacks, storage,
+ getHandler(getServiceThread()));
}
public int binderGetCallingUid() {
diff --git a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java
index 9b42cfc..e1cd2c5 100644
--- a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java
+++ b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java
@@ -205,6 +205,8 @@
private final RebootEscrowKeyStoreManager mKeyStoreManager;
+ private final Handler mHandler;
+
PowerManager.WakeLock mWakeLock;
private ConnectivityManager.NetworkCallback mNetworkCallback;
@@ -399,19 +401,21 @@
}
}
- RebootEscrowManager(Context context, Callbacks callbacks, LockSettingsStorage storage) {
- this(new Injector(context, storage), callbacks, storage);
+ RebootEscrowManager(Context context, Callbacks callbacks, LockSettingsStorage storage,
+ Handler handler) {
+ this(new Injector(context, storage), callbacks, storage, handler);
}
@VisibleForTesting
RebootEscrowManager(Injector injector, Callbacks callbacks,
- LockSettingsStorage storage) {
+ LockSettingsStorage storage, Handler handler) {
mInjector = injector;
mCallbacks = callbacks;
mStorage = storage;
mUserManager = injector.getUserManager();
mEventLog = injector.getEventLog();
mKeyStoreManager = injector.getKeyStoreManager();
+ mHandler = handler;
}
/** Wrapper function to set error code serialized through handler, */
@@ -937,7 +941,7 @@
private void setRebootEscrowReady(boolean ready) {
if (mRebootEscrowReady != ready) {
- mRebootEscrowListener.onPreparedForReboot(ready);
+ mHandler.post(() -> mRebootEscrowListener.onPreparedForReboot(ready));
}
mRebootEscrowReady = ready;
}
diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java
index 3329f54..030c96e 100644
--- a/services/core/java/com/android/server/notification/ConditionProviders.java
+++ b/services/core/java/com/android/server/notification/ConditionProviders.java
@@ -261,11 +261,15 @@
}
}
- private Condition[] removeDuplicateConditions(String pkg, Condition[] conditions) {
+ private Condition[] getValidConditions(String pkg, Condition[] conditions) {
if (conditions == null || conditions.length == 0) return null;
final int N = conditions.length;
final ArrayMap<Uri, Condition> valid = new ArrayMap<Uri, Condition>(N);
for (int i = 0; i < N; i++) {
+ if (conditions[i] == null) {
+ Slog.w(TAG, "Ignoring null condition from " + pkg);
+ continue;
+ }
final Uri id = conditions[i].id;
if (valid.containsKey(id)) {
Slog.w(TAG, "Ignoring condition from " + pkg + " for duplicate id: " + id);
@@ -303,7 +307,7 @@
synchronized(mMutex) {
if (DEBUG) Slog.d(TAG, "notifyConditions pkg=" + pkg + " info=" + info + " conditions="
+ (conditions == null ? null : Arrays.asList(conditions)));
- conditions = removeDuplicateConditions(pkg, conditions);
+ conditions = getValidConditions(pkg, conditions);
if (conditions == null || conditions.length == 0) return;
final int N = conditions.length;
for (int i = 0; i < N; i++) {
diff --git a/services/core/java/com/android/server/notification/ValidateNotificationPeople.java b/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
index 5e0a180..8417049 100644
--- a/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
+++ b/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
@@ -62,7 +62,7 @@
public class ValidateNotificationPeople implements NotificationSignalExtractor {
// Using a shorter log tag since setprop has a limit of 32chars on variable name.
private static final String TAG = "ValidateNoPeople";
- private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);;
+ private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final boolean ENABLE_PEOPLE_VALIDATOR = true;
@@ -105,12 +105,13 @@
private int mEvictionCount;
private NotificationUsageStats mUsageStats;
+ @Override
public void initialize(Context context, NotificationUsageStats usageStats) {
if (DEBUG) Slog.d(TAG, "Initializing " + getClass().getSimpleName() + ".");
mUserToContextMap = new ArrayMap<>();
mBaseContext = context;
mUsageStats = usageStats;
- mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE);
+ mPeopleCache = new LruCache<>(PEOPLE_CACHE_SIZE);
mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt(
mBaseContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1);
if (mEnabled) {
@@ -134,7 +135,7 @@
// For tests: just do the setting of various local variables without actually doing work
@VisibleForTesting
protected void initForTests(Context context, NotificationUsageStats usageStats,
- LruCache peopleCache) {
+ LruCache<String, LookupResult> peopleCache) {
mUserToContextMap = new ArrayMap<>();
mBaseContext = context;
mUsageStats = usageStats;
@@ -142,6 +143,7 @@
mEnabled = true;
}
+ @Override
public RankingReconsideration process(NotificationRecord record) {
if (!mEnabled) {
if (VERBOSE) Slog.i(TAG, "disabled");
@@ -272,7 +274,7 @@
}
if (VERBOSE) Slog.i(TAG, "Validating: " + key + " for " + context.getUserId());
- final LinkedList<String> pendingLookups = new LinkedList<String>();
+ final LinkedList<String> pendingLookups = new LinkedList<>();
int personIdx = 0;
for (String handle : people) {
if (TextUtils.isEmpty(handle)) continue;
@@ -320,7 +322,6 @@
return Integer.toString(userId) + ":" + handle;
}
- // VisibleForTesting
public static String[] getExtraPeople(Bundle extras) {
String[] peopleList = getExtraPeopleForKey(extras, Notification.EXTRA_PEOPLE_LIST);
String[] legacyPeople = getExtraPeopleForKey(extras, Notification.EXTRA_PEOPLE);
@@ -417,101 +418,6 @@
return null;
}
- private LookupResult resolvePhoneContact(Context context, final String number) {
- Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
- Uri.encode(number));
- return searchContacts(context, phoneUri);
- }
-
- private LookupResult resolveEmailContact(Context context, final String email) {
- Uri numberUri = Uri.withAppendedPath(
- ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI,
- Uri.encode(email));
- return searchContacts(context, numberUri);
- }
-
- @VisibleForTesting
- LookupResult searchContacts(Context context, Uri lookupUri) {
- LookupResult lookupResult = new LookupResult();
- final Uri corpLookupUri =
- ContactsContract.Contacts.createCorpLookupUriFromEnterpriseLookupUri(lookupUri);
- if (corpLookupUri == null) {
- addContacts(lookupResult, context, lookupUri);
- } else {
- addWorkContacts(lookupResult, context, corpLookupUri);
- }
- return lookupResult;
- }
-
- @VisibleForTesting
- // Performs a contacts search using searchContacts, and then follows up by looking up
- // any phone numbers associated with the resulting contact information and merge those
- // into the lookup result as well. Will have no additional effect if the contact does
- // not have any phone numbers.
- LookupResult searchContactsAndLookupNumbers(Context context, Uri lookupUri) {
- LookupResult lookupResult = searchContacts(context, lookupUri);
- String phoneLookupKey = lookupResult.getPhoneLookupKey();
- if (phoneLookupKey != null) {
- String selection = Contacts.LOOKUP_KEY + " = ?";
- String[] selectionArgs = new String[] { phoneLookupKey };
- try (Cursor cursor = context.getContentResolver().query(
- ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PHONE_LOOKUP_PROJECTION,
- selection, selectionArgs, /* sortOrder= */ null)) {
- if (cursor == null) {
- Slog.w(TAG, "Cursor is null when querying contact phone number.");
- return lookupResult;
- }
-
- while (cursor.moveToNext()) {
- lookupResult.mergePhoneNumber(cursor);
- }
- } catch (Throwable t) {
- Slog.w(TAG, "Problem getting content resolver or querying phone numbers.", t);
- }
- }
- return lookupResult;
- }
-
- private void addWorkContacts(LookupResult lookupResult, Context context, Uri corpLookupUri) {
- final int workUserId = findWorkUserId(context);
- if (workUserId == -1) {
- Slog.w(TAG, "Work profile user ID not found for work contact: " + corpLookupUri);
- return;
- }
- final Uri corpLookupUriWithUserId =
- ContentProvider.maybeAddUserId(corpLookupUri, workUserId);
- addContacts(lookupResult, context, corpLookupUriWithUserId);
- }
-
- /** Returns the user ID of the managed profile or -1 if none is found. */
- private int findWorkUserId(Context context) {
- final UserManager userManager = context.getSystemService(UserManager.class);
- final int[] profileIds =
- userManager.getProfileIds(context.getUserId(), /* enabledOnly= */ true);
- for (int profileId : profileIds) {
- if (userManager.isManagedProfile(profileId)) {
- return profileId;
- }
- }
- return -1;
- }
-
- /** Modifies the given lookup result to add contacts found at the given URI. */
- private void addContacts(LookupResult lookupResult, Context context, Uri uri) {
- try (Cursor c = context.getContentResolver().query(
- uri, LOOKUP_PROJECTION, null, null, null)) {
- if (c == null) {
- Slog.w(TAG, "Null cursor from contacts query.");
- return;
- }
- while (c.moveToNext()) {
- lookupResult.mergeContact(c);
- }
- } catch (Throwable t) {
- Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
- }
- }
-
@VisibleForTesting
protected static class LookupResult {
private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr
@@ -619,19 +525,18 @@
}
}
- private class PeopleRankingReconsideration extends RankingReconsideration {
+ @VisibleForTesting
+ class PeopleRankingReconsideration extends RankingReconsideration {
private final LinkedList<String> mPendingLookups;
private final Context mContext;
- // Amount of time to wait for a result from the contacts db before rechecking affinity.
- private static final long LOOKUP_TIME = 1000;
private float mContactAffinity = NONE;
private ArraySet<String> mPhoneNumbers = null;
private NotificationRecord mRecord;
private PeopleRankingReconsideration(Context context, String key,
LinkedList<String> pendingLookups) {
- super(key, LOOKUP_TIME);
+ super(key);
mContext = context;
mPendingLookups = pendingLookups;
}
@@ -642,7 +547,7 @@
long timeStartMs = System.currentTimeMillis();
for (final String handle: mPendingLookups) {
final String cacheKey = getCacheKey(mContext.getUserId(), handle);
- LookupResult lookupResult = null;
+ LookupResult lookupResult;
boolean cacheHit = false;
synchronized (mPeopleCache) {
lookupResult = mPeopleCache.get(cacheKey);
@@ -703,6 +608,102 @@
}
}
+ private static LookupResult resolvePhoneContact(Context context, final String number) {
+ Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
+ Uri.encode(number));
+ return searchContacts(context, phoneUri);
+ }
+
+ private static LookupResult resolveEmailContact(Context context, final String email) {
+ Uri numberUri = Uri.withAppendedPath(
+ ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI,
+ Uri.encode(email));
+ return searchContacts(context, numberUri);
+ }
+
+ @VisibleForTesting
+ static LookupResult searchContacts(Context context, Uri lookupUri) {
+ LookupResult lookupResult = new LookupResult();
+ final Uri corpLookupUri =
+ ContactsContract.Contacts.createCorpLookupUriFromEnterpriseLookupUri(lookupUri);
+ if (corpLookupUri == null) {
+ addContacts(lookupResult, context, lookupUri);
+ } else {
+ addWorkContacts(lookupResult, context, corpLookupUri);
+ }
+ return lookupResult;
+ }
+
+ @VisibleForTesting
+ // Performs a contacts search using searchContacts, and then follows up by looking up
+ // any phone numbers associated with the resulting contact information and merge those
+ // into the lookup result as well. Will have no additional effect if the contact does
+ // not have any phone numbers.
+ static LookupResult searchContactsAndLookupNumbers(Context context, Uri lookupUri) {
+ LookupResult lookupResult = searchContacts(context, lookupUri);
+ String phoneLookupKey = lookupResult.getPhoneLookupKey();
+ if (phoneLookupKey != null) {
+ String selection = Contacts.LOOKUP_KEY + " = ?";
+ String[] selectionArgs = new String[] { phoneLookupKey };
+ try (Cursor cursor = context.getContentResolver().query(
+ ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PHONE_LOOKUP_PROJECTION,
+ selection, selectionArgs, /* sortOrder= */ null)) {
+ if (cursor == null) {
+ Slog.w(TAG, "Cursor is null when querying contact phone number.");
+ return lookupResult;
+ }
+
+ while (cursor.moveToNext()) {
+ lookupResult.mergePhoneNumber(cursor);
+ }
+ } catch (Throwable t) {
+ Slog.w(TAG, "Problem getting content resolver or querying phone numbers.", t);
+ }
+ }
+ return lookupResult;
+ }
+
+ private static void addWorkContacts(LookupResult lookupResult, Context context,
+ Uri corpLookupUri) {
+ final int workUserId = findWorkUserId(context);
+ if (workUserId == -1) {
+ Slog.w(TAG, "Work profile user ID not found for work contact: " + corpLookupUri);
+ return;
+ }
+ final Uri corpLookupUriWithUserId =
+ ContentProvider.maybeAddUserId(corpLookupUri, workUserId);
+ addContacts(lookupResult, context, corpLookupUriWithUserId);
+ }
+
+ /** Returns the user ID of the managed profile or -1 if none is found. */
+ private static int findWorkUserId(Context context) {
+ final UserManager userManager = context.getSystemService(UserManager.class);
+ final int[] profileIds =
+ userManager.getProfileIds(context.getUserId(), /* enabledOnly= */ true);
+ for (int profileId : profileIds) {
+ if (userManager.isManagedProfile(profileId)) {
+ return profileId;
+ }
+ }
+ return -1;
+ }
+
+ /** Modifies the given lookup result to add contacts found at the given URI. */
+ private static void addContacts(LookupResult lookupResult, Context context, Uri uri) {
+ try (Cursor c = context.getContentResolver().query(
+ uri, LOOKUP_PROJECTION, null, null, null)) {
+ if (c == null) {
+ Slog.w(TAG, "Null cursor from contacts query.");
+ return;
+ }
+ while (c.moveToNext()) {
+ lookupResult.mergeContact(c);
+ }
+ } catch (Throwable t) {
+ Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
+ }
+ }
+
@Override
public void applyChangesLocked(NotificationRecord operand) {
float affinityBound = operand.getContactAffinity();
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index b4d467f..721ad88 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -491,8 +491,11 @@
public abstract boolean isUserVisible(@UserIdInt int userId, int displayId);
/**
- * Returns the display id assigned to the user, or {@code Display.INVALID_DISPLAY} if the
- * user is not assigned to any display.
+ * Returns the main display id assigned to the user, or {@code Display.INVALID_DISPLAY} if the
+ * user is not assigned to any main display.
+ *
+ * <p>In the context of multi-user multi-display, there can be multiple main displays, at most
+ * one per each zone. Main displays are where UI is launched which a user interacts with.
*
* <p>The current foreground user and its running profiles are associated with the
* {@link android.view.Display#DEFAULT_DISPLAY default display}, while other users would only be
@@ -503,9 +506,20 @@
*
* <p>If the user is a profile and is running, it's assigned to its parent display.
*/
+ // TODO(b/272366483) rename this method to avoid confusion with getDisplaysAssignedTOUser().
public abstract int getDisplayAssignedToUser(@UserIdInt int userId);
/**
+ * Returns all display ids assigned to the user including {@link
+ * #assignUserToExtraDisplay(int, int) extra displays}, or {@code null} if there is no display
+ * assigned to the specified user.
+ *
+ * <p>Note that this method is different from {@link #getDisplayAssignedToUser(int)}, which
+ * returns a main display only.
+ */
+ public abstract @Nullable int[] getDisplaysAssignedToUser(@UserIdInt int userId);
+
+ /**
* Returns the main user (i.e., not a profile) that is assigned to the display, or the
* {@link android.app.ActivityManager#getCurrentUser() current foreground user} if no user is
* associated with the display.
@@ -573,5 +587,6 @@
* @throws UserManager.CheckedUserOperationException if no switchable user can be found
*/
- public abstract @UserIdInt int getBootUser() throws UserManager.CheckedUserOperationException;
+ public abstract @UserIdInt int getBootUser(boolean waitUntilSet)
+ throws UserManager.CheckedUserOperationException;
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 3343172..fdc2aff 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -161,7 +161,9 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -278,6 +280,8 @@
static final int WRITE_USER_MSG = 1;
static final int WRITE_USER_DELAY = 2*1000; // 2 seconds
+ private static final long BOOT_USER_SET_TIMEOUT_MS = 300_000;
+
// Tron counters
private static final String TRON_GUEST_CREATED = "users_guest_created";
private static final String TRON_USER_CREATED = "users_user_created";
@@ -333,6 +337,8 @@
/** Indicates that this is the 1st boot after the system user mode was changed by emulation. */
private boolean mUpdatingSystemUserMode;
+ /** Count down latch to wait while boot user is not set.*/
+ private final CountDownLatch mBootUserLatch = new CountDownLatch(1);
/**
* Internal non-parcelable wrapper for UserInfo that is not exposed to other system apps.
*/
@@ -952,18 +958,62 @@
Slogf.i(LOG_TAG, "setBootUser %d", userId);
mBootUser = userId;
}
+ mBootUserLatch.countDown();
}
@Override
public @UserIdInt int getBootUser() {
checkCreateUsersPermission("Get boot user");
try {
- return mLocalService.getBootUser();
+ return getBootUserUnchecked();
} catch (UserManager.CheckedUserOperationException e) {
throw e.toServiceSpecificException();
}
}
+ private @UserIdInt int getBootUserUnchecked() throws UserManager.CheckedUserOperationException {
+ synchronized (mUsersLock) {
+ if (mBootUser != UserHandle.USER_NULL) {
+ final UserData userData = mUsers.get(mBootUser);
+ if (userData != null && userData.info.supportsSwitchToByUser()) {
+ Slogf.i(LOG_TAG, "Using provided boot user: %d", mBootUser);
+ return mBootUser;
+ } else {
+ Slogf.w(LOG_TAG,
+ "Provided boot user cannot be switched to: %d", mBootUser);
+ }
+ }
+ }
+
+ if (isHeadlessSystemUserMode()) {
+ // Return the previous foreground user, if there is one.
+ final int previousUser = getPreviousFullUserToEnterForeground();
+ if (previousUser != UserHandle.USER_NULL) {
+ Slogf.i(LOG_TAG, "Boot user is previous user %d", previousUser);
+ return previousUser;
+ }
+ // No previous user. Return the first switchable user if there is one.
+ synchronized (mUsersLock) {
+ final int userSize = mUsers.size();
+ for (int i = 0; i < userSize; i++) {
+ final UserData userData = mUsers.valueAt(i);
+ if (userData.info.supportsSwitchToByUser()) {
+ int firstSwitchable = userData.info.id;
+ Slogf.i(LOG_TAG,
+ "Boot user is first switchable user %d", firstSwitchable);
+ return firstSwitchable;
+ }
+ }
+ }
+ // No switchable users found. Uh oh!
+ throw new UserManager.CheckedUserOperationException(
+ "No switchable users found", USER_OPERATION_ERROR_UNKNOWN);
+ }
+ // Not HSUM, return system user.
+ return UserHandle.USER_SYSTEM;
+ }
+
+
@Override
public int getPreviousFullUserToEnterForeground() {
checkQueryOrCreateUsersPermission("get previous user");
@@ -1495,7 +1545,8 @@
// intentSender
unlockIntent.putExtra(Intent.EXTRA_INTENT, pendingIntent.getIntentSender());
unlockIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- mContext.startActivity(unlockIntent);
+ mContext.startActivityAsUser(
+ unlockIntent, UserHandle.of(getProfileParentIdUnchecked(userId)));
}
@Override
@@ -5814,20 +5865,24 @@
}
/**
- * @deprecated Use {@link
- * android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin} instead.
+ * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * it is possible for there to be multiple managing agents on the device with the ability to set
+ * restrictions, e.g. an Enterprise DPC and a Supervision admin. This API will only to return
+ * the restrictions set by the DPCs. To retrieve restrictions set by all agents, use
+ * {@link android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin} instead.
*/
- @Deprecated
@Override
public Bundle getApplicationRestrictions(String packageName) {
return getApplicationRestrictionsForUser(packageName, UserHandle.getCallingUserId());
}
/**
- * @deprecated Use {@link
- * android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin} instead.
+ * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * it is possible for there to be multiple managing agents on the device with the ability to set
+ * restrictions, e.g. an Enterprise DPC and a Supervision admin. This API will only to return
+ * the restrictions set by the DPCs. To retrieve restrictions set by all agents, use
+ * {@link android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin} instead.
*/
- @Deprecated
@Override
public Bundle getApplicationRestrictionsForUser(String packageName, @UserIdInt int userId) {
if (UserHandle.getCallingUserId() != userId
@@ -7139,6 +7194,11 @@
}
@Override
+ public @Nullable int[] getDisplaysAssignedToUser(@UserIdInt int userId) {
+ return mUserVisibilityMediator.getDisplaysAssignedToUser(userId);
+ }
+
+ @Override
public @UserIdInt int getUserAssignedToDisplay(int displayId) {
return mUserVisibilityMediator.getUserAssignedToDisplay(displayId);
}
@@ -7182,47 +7242,29 @@
}
@Override
- public @UserIdInt int getBootUser() throws UserManager.CheckedUserOperationException {
- synchronized (mUsersLock) {
- // TODO(b/242195409): On Automotive, block if boot user not provided.
- if (mBootUser != UserHandle.USER_NULL) {
- final UserData userData = mUsers.get(mBootUser);
- if (userData != null && userData.info.supportsSwitchToByUser()) {
- Slogf.i(LOG_TAG, "Using provided boot user: %d", mBootUser);
- return mBootUser;
- } else {
- Slogf.w(LOG_TAG,
- "Provided boot user cannot be switched to: %d", mBootUser);
+ public @UserIdInt int getBootUser(boolean waitUntilSet)
+ throws UserManager.CheckedUserOperationException {
+ if (waitUntilSet) {
+ final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
+ t.traceBegin("wait-boot-user");
+ try {
+ if (mBootUserLatch.getCount() != 0) {
+ Slogf.d(LOG_TAG,
+ "Sleeping for boot user to be set. "
+ + "Max sleep for Time: %d", BOOT_USER_SET_TIMEOUT_MS);
}
+ if (!mBootUserLatch.await(BOOT_USER_SET_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ Slogf.w(LOG_TAG, "Boot user not set. Timeout: %d",
+ BOOT_USER_SET_TIMEOUT_MS);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Slogf.w(LOG_TAG, e, "InterruptedException during wait for boot user.");
}
+ t.traceEnd();
}
- if (isHeadlessSystemUserMode()) {
- // Return the previous foreground user, if there is one.
- final int previousUser = getPreviousFullUserToEnterForeground();
- if (previousUser != UserHandle.USER_NULL) {
- Slogf.i(LOG_TAG, "Boot user is previous user %d", previousUser);
- return previousUser;
- }
- // No previous user. Return the first switchable user if there is one.
- synchronized (mUsersLock) {
- final int userSize = mUsers.size();
- for (int i = 0; i < userSize; i++) {
- final UserData userData = mUsers.valueAt(i);
- if (userData.info.supportsSwitchToByUser()) {
- int firstSwitchable = userData.info.id;
- Slogf.i(LOG_TAG,
- "Boot user is first switchable user %d", firstSwitchable);
- return firstSwitchable;
- }
- }
- }
- // No switchable users found. Uh oh!
- throw new UserManager.CheckedUserOperationException(
- "No switchable users found", USER_OPERATION_ERROR_UNKNOWN);
- }
- // Not HSUM, return system user.
- return UserHandle.USER_SYSTEM;
+ return getBootUserUnchecked();
}
} // class LocalService
diff --git a/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java b/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java
index 12c9e98..2f99062 100644
--- a/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java
+++ b/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java
@@ -222,7 +222,7 @@
final Set<String> userAllowlist = getInstallablePackagesForUserId(userId);
pmInt.forEachPackageState(packageState -> {
- if (packageState.getPkg() == null) {
+ if (packageState.getPkg() == null || !packageState.isSystem()) {
return;
}
boolean install = (userAllowlist == null
diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
index a8615c2..3710af6 100644
--- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java
+++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
@@ -63,19 +63,39 @@
/**
* Class responsible for deciding whether a user is visible (or visible for a given display).
*
- * <p>Currently, it has 2 "modes" (set on constructor), which defines the class behavior (i.e, the
+ * <p>Currently, it has 3 "modes" (set on constructor), which defines the class behavior (i.e, the
* logic that dictates the result of methods such as {@link #isUserVisible(int)} and
* {@link #isUserVisible(int, int)}):
*
* <ul>
- * <li>default: this is the most common mode (used by phones, tablets, foldables, automotives with
- * just cluster and driver displayes, etc...), where the logic is based solely on the current
- * foreground user (and its started profiles)
- * <li>{@code MUMD}: mode for "(concurrent) Multiple Users on Multiple Displays", which is used on
- * automotives with passenger display. In this mode, users started in background on the secondary
- * display are stored in map.
+ * <li>default (A.K.A {@code SUSD} - Single User on Single Display): this is the most common mode
+ * (used by phones, tablets, foldables, cars with just cluster and driver displays, etc.),
+ * where just the current foreground user and its profiles are visible; hence, most methods are
+ * optimized to just check for the current user / profile. This mode is unit tested by
+ * {@link com.android.server.pm.UserVisibilityMediatorSUSDTest} and CTS tested by
+ * {@link android.multiuser.cts.UserVisibilityTest}.
+ * <li>concurrent users (A.K.A. {@code MUMD} - Multiple Users on Multiple Displays): typically
+ * used on automotive builds where the car has additional displays for passengers, it allows users
+ * to be started in the background but visible on these displays; hence, it contains additional
+ * maps to account for the visibility state. This mode is unit tested by
+ * {@link com.android.server.pm.UserVisibilityMediatorMUMDTest} and CTS tested by
+ * {@link android.multiuser.cts.UserVisibilityTest}.
+ * <li>no driver (A.K.A. {@code MUPAND} - MUltiple PAssengers, No Driver): extension of the
+ * previous mode and typically used on automotive builds where the car has additional displays for
+ * passengers but uses a secondary Android system for the back passengers, so all "human" users
+ * are started in the background (and the current foreground user is the system user), hence the
+ * "no driver name". This mode is unit tested by
+ * {@link com.android.server.pm.UserVisibilityMediatorMUPANDTest} and CTS tested by
+ * {@link android.multiuser.cts.UserVisibilityVisibleBackgroundUsersOnDefaultDisplayTest}.
* </ul>
*
+ * <p>When you make changes in this class, you should run at least the 3 unit tests and
+ * {@link android.multiuser.cts.UserVisibilityTest} (which actually applies for all modes); for
+ * example, by calling {@code atest UserVisibilityMediatorSUSDTest UserVisibilityMediatorMUMDTest
+ * UserVisibilityMediatorMUPANDTest UserVisibilityTest}. Ideally, you should run the other 2 CTS
+ * tests as well (you can emulate these modes using {@code adb} commands; their javadoc provides
+ * instructions on how to do so).
+ *
* <p>This class is thread safe.
*/
public final class UserVisibilityMediator implements Dumpable {
@@ -786,6 +806,49 @@
}
}
+ /** See {@link UserManagerInternal#getDisplaysAssignedToUser(int)}. */
+ @Nullable
+ public int[] getDisplaysAssignedToUser(@UserIdInt int userId) {
+ int mainDisplayId = getDisplayAssignedToUser(userId);
+ if (mainDisplayId == INVALID_DISPLAY) {
+ // The user will not have any extra displays if they have no main display.
+ // Return null if no display is assigned to the user.
+ if (DBG) {
+ Slogf.d(TAG, "getDisplaysAssignedToUser(): returning null"
+ + " because there is no display assigned to user %d", userId);
+ }
+ return null;
+ }
+
+ synchronized (mLock) {
+ if (mExtraDisplaysAssignedToUsers == null
+ || mExtraDisplaysAssignedToUsers.size() == 0) {
+ return new int[]{mainDisplayId};
+ }
+
+ int count = 0;
+ int[] displayIds = new int[mExtraDisplaysAssignedToUsers.size() + 1];
+ displayIds[count++] = mainDisplayId;
+ for (int i = 0; i < mExtraDisplaysAssignedToUsers.size(); ++i) {
+ if (mExtraDisplaysAssignedToUsers.valueAt(i) == userId) {
+ displayIds[count++] = mExtraDisplaysAssignedToUsers.keyAt(i);
+ }
+ }
+ // Return the array if the array length happens to be correct.
+ if (displayIds.length == count) {
+ return displayIds;
+ }
+
+ // Copy the results to a new array with the exact length. The size of displayIds[] is
+ // initialized to `1 + mExtraDisplaysAssignedToUsers.size()`, which is usually larger
+ // than the actual length, because mExtraDisplaysAssignedToUsers contains displayIds for
+ // other users. Therefore, we need to copy to a new array with the correct length.
+ int[] results = new int[count];
+ System.arraycopy(displayIds, 0, results, 0, count);
+ return results;
+ }
+ }
+
/**
* See {@link UserManagerInternal#getUserAssignedToDisplay(int)}.
*/
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 4ec8afd..ea53ea5 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -647,6 +647,9 @@
private boolean mLockNowPending = false;
+ // Timeout for showing the keyguard after the screen is on, in case no "ready" is received.
+ private int mKeyguardDrawnTimeout = 1000;
+
private static final int MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK = 3;
private static final int MSG_DISPATCH_MEDIA_KEY_REPEAT_WITH_WAKE_LOCK = 4;
private static final int MSG_KEYGUARD_DRAWN_COMPLETE = 5;
@@ -2236,6 +2239,8 @@
}
});
+ mKeyguardDrawnTimeout = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_keyguardDrawnTimeout);
mKeyguardDelegate = new KeyguardServiceDelegate(mContext,
new StateCallback() {
@Override
@@ -4981,7 +4986,7 @@
final boolean bootCompleted =
LocalServices.getService(SystemServiceManager.class).isBootCompleted();
// Set longer timeout if it has not booted yet to prevent showing empty window.
- return bootCompleted ? 1000 : 5000;
+ return bootCompleted ? mKeyguardDrawnTimeout : 5000;
}
// Called on the DisplayManager's DisplayPowerController thread.
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 bc23020..661715c 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -26,6 +26,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
+import android.app.AlarmManager;
import android.app.usage.NetworkStatsManager;
import android.bluetooth.BluetoothActivityEnergyInfo;
import android.bluetooth.UidTraffic;
@@ -500,14 +501,6 @@
}
- /** Handles calls to AlarmManager */
- public interface AlarmInterface {
- /** Schedule an RTC alarm */
- void schedule(long rtcTimeMs, long windowLengthMs);
- /** Cancel the previously scheduled alarm */
- void cancel();
- }
-
private final PlatformIdleStateCallback mPlatformIdleStateCallback;
private final Runnable mDeferSetCharging = new Runnable() {
@@ -1569,8 +1562,15 @@
@GuardedBy("this")
protected BatteryStatsConfig mBatteryStatsConfig = new BatteryStatsConfig.Builder().build();
- @VisibleForTesting
- protected AlarmInterface mLongPlugInAlarmInterface = null;
+ @GuardedBy("this")
+ private AlarmManager mAlarmManager = null;
+
+ private final AlarmManager.OnAlarmListener mLongPlugInAlarmHandler = () ->
+ mHandler.post(() -> {
+ synchronized (BatteryStatsImpl.this) {
+ maybeResetWhilePluggedInLocked();
+ }
+ });
/*
* Holds a SamplingTimer associated with each Resource Power Manager state and voter,
@@ -11061,18 +11061,6 @@
}
/**
- * Injects an AlarmInterface for the long plug in alarm.
- */
- public void setLongPlugInAlarmInterface(AlarmInterface longPlugInAlarmInterface) {
- synchronized (this) {
- mLongPlugInAlarmInterface = longPlugInAlarmInterface;
- if (mBatteryPluggedIn) {
- scheduleNextResetWhilePluggedInCheck();
- }
- }
- }
-
- /**
* Starts tracking CPU time-in-state for threads of the system server process,
* keeping a separate account of threads receiving incoming binder calls.
*/
@@ -14173,6 +14161,7 @@
/**
* Might reset battery stats if conditions are met. Assumed the device is currently plugged in.
*/
+ @VisibleForTesting
@GuardedBy("this")
public void maybeResetWhilePluggedInLocked() {
final long elapsedRealtimeMs = mClock.elapsedRealtime();
@@ -14189,28 +14178,31 @@
@GuardedBy("this")
private void scheduleNextResetWhilePluggedInCheck() {
- if (mLongPlugInAlarmInterface != null) {
- final long timeoutMs = mClock.currentTimeMillis()
- + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS
- * DateUtils.HOUR_IN_MILLIS;
- Calendar nextAlarm = Calendar.getInstance();
- nextAlarm.setTimeInMillis(timeoutMs);
+ if (mAlarmManager == null) return;
+ final long timeoutMs = mClock.currentTimeMillis()
+ + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS
+ * DateUtils.HOUR_IN_MILLIS;
+ Calendar nextAlarm = Calendar.getInstance();
+ nextAlarm.setTimeInMillis(timeoutMs);
- // Find the 2 AM the same day as the end of the minimum duration.
- // This logic does not handle a Daylight Savings transition, or a timezone change
- // while the alarm has been set. The need to reset after a long period while plugged
- // in is not strict enough to warrant a well architected out solution.
- nextAlarm.set(Calendar.MILLISECOND, 0);
- nextAlarm.set(Calendar.SECOND, 0);
- nextAlarm.set(Calendar.MINUTE, 0);
- nextAlarm.set(Calendar.HOUR_OF_DAY, 2);
- long nextTimeMs = nextAlarm.getTimeInMillis();
- if (nextTimeMs < timeoutMs) {
- // The 2AM on the day of the timeout, move on the next day.
- nextTimeMs += DateUtils.DAY_IN_MILLIS;
- }
- mLongPlugInAlarmInterface.schedule(nextTimeMs, DateUtils.HOUR_IN_MILLIS);
+ // Find the 2 AM the same day as the end of the minimum duration.
+ // This logic does not handle a Daylight Savings transition, or a timezone change
+ // while the alarm has been set. The need to reset after a long period while plugged
+ // in is not strict enough to warrant a well architected out solution.
+ nextAlarm.set(Calendar.MILLISECOND, 0);
+ nextAlarm.set(Calendar.SECOND, 0);
+ nextAlarm.set(Calendar.MINUTE, 0);
+ nextAlarm.set(Calendar.HOUR_OF_DAY, 2);
+ long possibleNextTimeMs = nextAlarm.getTimeInMillis();
+ if (possibleNextTimeMs < timeoutMs) {
+ // The 2AM on the day of the timeout, move on the next day.
+ possibleNextTimeMs += DateUtils.DAY_IN_MILLIS;
}
+ final long nextTimeMs = possibleNextTimeMs;
+ final AlarmManager am = mAlarmManager;
+ mHandler.post(() -> am.setWindow(AlarmManager.RTC, nextTimeMs,
+ DateUtils.HOUR_IN_MILLIS,
+ TAG, mLongPlugInAlarmHandler, mHandler));
}
@@ -14339,8 +14331,12 @@
initActiveHistoryEventsLocked(mSecRealtime, mSecUptime);
}
mBatteryPluggedIn = false;
- if (mLongPlugInAlarmInterface != null) {
- mLongPlugInAlarmInterface.cancel();
+ if (mAlarmManager != null) {
+ final AlarmManager am = mAlarmManager;
+ mHandler.post(() -> {
+ // No longer plugged in. Cancel the long plug in alarm.
+ am.cancel(mLongPlugInAlarmHandler);
+ });
}
mHistory.recordBatteryState(mSecRealtime, mSecUptime, level, mBatteryPluggedIn);
mDischargeCurrentLevel = mDischargeUnplugLevel = level;
@@ -15178,6 +15174,14 @@
public void systemServicesReady(Context context) {
mConstants.startObserving(context.getContentResolver());
registerUsbStateReceiver(context);
+
+ synchronized (this) {
+ mAlarmManager = context.getSystemService(AlarmManager.class);
+ if (mBatteryPluggedIn) {
+ // Already plugged in. Schedule the long plug in alarm.
+ scheduleNextResetWhilePluggedInCheck();
+ }
+ }
}
/**
diff --git a/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java b/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java
index 8e8abf6..96f4a01 100644
--- a/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java
+++ b/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java
@@ -250,15 +250,7 @@
service.checkRecognitionSupport(recognizerIntent, attributionSource, callback));
}
- void triggerModelDownload(Intent recognizerIntent, AttributionSource attributionSource) {
- if (!mConnected) {
- Slog.e(TAG, "#downloadModel failed due to connection.");
- return;
- }
- run(service -> service.triggerModelDownload(recognizerIntent, attributionSource));
- }
-
- void setModelDownloadListener(
+ void triggerModelDownload(
Intent recognizerIntent,
AttributionSource attributionSource,
IModelDownloadListener listener) {
@@ -266,25 +258,12 @@
try {
listener.onError(SpeechRecognizer.ERROR_SERVER_DISCONNECTED);
} catch (RemoteException e) {
- Slog.w(TAG, "Failed to report the connection broke to the caller.", e);
+ Slog.w(TAG, "#downloadModel failed due to connection.", e);
e.printStackTrace();
}
return;
}
-
- run(service ->
- service.setModelDownloadListener(recognizerIntent, attributionSource, listener));
- }
-
- void clearModelDownloadListener(
- Intent recognizerIntent,
- AttributionSource attributionSource) {
- if (!mConnected) {
- return;
- }
-
- run(service ->
- service.clearModelDownloadListener(recognizerIntent, attributionSource));
+ run(service -> service.triggerModelDownload(recognizerIntent, attributionSource, listener));
}
void shutdown(IBinder clientToken) {
diff --git a/services/core/java/com/android/server/speech/SpeechRecognitionManagerServiceImpl.java b/services/core/java/com/android/server/speech/SpeechRecognitionManagerServiceImpl.java
index bc73db1..bff6d50 100644
--- a/services/core/java/com/android/server/speech/SpeechRecognitionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/speech/SpeechRecognitionManagerServiceImpl.java
@@ -193,25 +193,11 @@
@Override
public void triggerModelDownload(
Intent recognizerIntent,
- AttributionSource attributionSource) {
- service.triggerModelDownload(recognizerIntent, attributionSource);
- }
-
- @Override
- public void setModelDownloadListener(
- Intent recognizerIntent,
AttributionSource attributionSource,
- IModelDownloadListener listener) throws RemoteException {
- service.setModelDownloadListener(
+ IModelDownloadListener listener) {
+ service.triggerModelDownload(
recognizerIntent, attributionSource, listener);
}
-
- @Override
- public void clearModelDownloadListener(
- Intent recognizerIntent,
- AttributionSource attributionSource) throws RemoteException {
- service.clearModelDownloadListener(recognizerIntent, attributionSource);
- }
});
} catch (RemoteException e) {
Slog.e(TAG, "Error creating a speech recognition session", e);
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
index ed91775..2d3928c 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
@@ -1019,6 +1019,7 @@
int inUseLowestPriorityFrHandle = TunerResourceManager.INVALID_RESOURCE_HANDLE;
// Priority max value is 1000
int currentLowestPriority = MAX_CLIENT_PRIORITY + 1;
+ boolean isRequestFromSameProcess = false;
// If the desired frontend id was specified, we only need to check the frontend.
boolean hasDesiredFrontend = request.desiredId != TunerFrontendRequest.DEFAULT_DESIRED_ID;
for (FrontendResource fr : getFrontendResources().values()) {
@@ -1048,6 +1049,8 @@
if (currentLowestPriority > priority) {
inUseLowestPriorityFrHandle = fr.getHandle();
currentLowestPriority = priority;
+ isRequestFromSameProcess = (requestClient.getProcessId()
+ == (getClientProfile(fr.getOwnerClientId())).getProcessId());
}
}
}
@@ -1063,7 +1066,8 @@
// When all the resources are occupied, grant the lowest priority resource if the
// request client has higher priority.
if (inUseLowestPriorityFrHandle != TunerResourceManager.INVALID_RESOURCE_HANDLE
- && (requestClient.getPriority() > currentLowestPriority)) {
+ && ((requestClient.getPriority() > currentLowestPriority) || (
+ (requestClient.getPriority() == currentLowestPriority) && isRequestFromSameProcess))) {
if (!reclaimResource(
getFrontendResource(inUseLowestPriorityFrHandle).getOwnerClientId(),
TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND)) {
@@ -1182,6 +1186,7 @@
int inUseLowestPriorityLnbHandle = TunerResourceManager.INVALID_RESOURCE_HANDLE;
// Priority max value is 1000
int currentLowestPriority = MAX_CLIENT_PRIORITY + 1;
+ boolean isRequestFromSameProcess = false;
for (LnbResource lnb : getLnbResources().values()) {
if (!lnb.isInUse()) {
// Grant the unused lnb with lower handle first
@@ -1194,6 +1199,8 @@
if (currentLowestPriority > priority) {
inUseLowestPriorityLnbHandle = lnb.getHandle();
currentLowestPriority = priority;
+ isRequestFromSameProcess = (requestClient.getProcessId()
+ == (getClientProfile(lnb.getOwnerClientId())).getProcessId());
}
}
}
@@ -1208,7 +1215,8 @@
// When all the resources are occupied, grant the lowest priority resource if the
// request client has higher priority.
if (inUseLowestPriorityLnbHandle > TunerResourceManager.INVALID_RESOURCE_HANDLE
- && (requestClient.getPriority() > currentLowestPriority)) {
+ && ((requestClient.getPriority() > currentLowestPriority) || (
+ (requestClient.getPriority() == currentLowestPriority) && isRequestFromSameProcess))) {
if (!reclaimResource(getLnbResource(inUseLowestPriorityLnbHandle).getOwnerClientId(),
TunerResourceManager.TUNER_RESOURCE_TYPE_LNB)) {
return false;
@@ -1240,6 +1248,7 @@
int lowestPriorityOwnerId = -1;
// Priority max value is 1000
int currentLowestPriority = MAX_CLIENT_PRIORITY + 1;
+ boolean isRequestFromSameProcess = false;
if (!cas.isFullyUsed()) {
casSessionHandle[0] = generateResourceHandle(
TunerResourceManager.TUNER_RESOURCE_TYPE_CAS_SESSION, cas.getSystemId());
@@ -1252,12 +1261,15 @@
if (currentLowestPriority > priority) {
lowestPriorityOwnerId = ownerId;
currentLowestPriority = priority;
+ isRequestFromSameProcess = (requestClient.getProcessId()
+ == (getClientProfile(ownerId)).getProcessId());
}
}
// When all the Cas sessions are occupied, reclaim the lowest priority client if the
// request client has higher priority.
- if (lowestPriorityOwnerId > -1 && (requestClient.getPriority() > currentLowestPriority)) {
+ if (lowestPriorityOwnerId > -1 && ((requestClient.getPriority() > currentLowestPriority)
+ || ((requestClient.getPriority() == currentLowestPriority) && isRequestFromSameProcess))) {
if (!reclaimResource(lowestPriorityOwnerId,
TunerResourceManager.TUNER_RESOURCE_TYPE_CAS_SESSION)) {
return false;
@@ -1289,6 +1301,7 @@
int lowestPriorityOwnerId = -1;
// Priority max value is 1000
int currentLowestPriority = MAX_CLIENT_PRIORITY + 1;
+ boolean isRequestFromSameProcess = false;
if (!ciCam.isFullyUsed()) {
ciCamHandle[0] = generateResourceHandle(
TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND_CICAM, ciCam.getCiCamId());
@@ -1301,12 +1314,16 @@
if (currentLowestPriority > priority) {
lowestPriorityOwnerId = ownerId;
currentLowestPriority = priority;
+ isRequestFromSameProcess = (requestClient.getProcessId()
+ == (getClientProfile(ownerId)).getProcessId());
}
}
// When all the CiCam sessions are occupied, reclaim the lowest priority client if the
// request client has higher priority.
- if (lowestPriorityOwnerId > -1 && (requestClient.getPriority() > currentLowestPriority)) {
+ if (lowestPriorityOwnerId > -1 && ((requestClient.getPriority() > currentLowestPriority)
+ || ((requestClient.getPriority() == currentLowestPriority)
+ && isRequestFromSameProcess))) {
if (!reclaimResource(lowestPriorityOwnerId,
TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND_CICAM)) {
return false;
@@ -1424,6 +1441,7 @@
int inUseLowestPriorityDrHandle = TunerResourceManager.INVALID_RESOURCE_HANDLE;
// Priority max value is 1000
int currentLowestPriority = MAX_CLIENT_PRIORITY + 1;
+ boolean isRequestFromSameProcess = false;
// If the desired demux id was specified, we only need to check the demux.
boolean hasDesiredDemuxCap = request.desiredFilterTypes
!= DemuxFilterMainType.UNDEFINED;
@@ -1448,6 +1466,8 @@
// update lowest priority
if (currentLowestPriority > priority) {
currentLowestPriority = priority;
+ isRequestFromSameProcess = (requestClient.getProcessId()
+ == (getClientProfile(dr.getOwnerClientId())).getProcessId());
shouldUpdate = true;
}
// update smallest caps
@@ -1473,7 +1493,8 @@
// When all the resources are occupied, grant the lowest priority resource if the
// request client has higher priority.
if (inUseLowestPriorityDrHandle != TunerResourceManager.INVALID_RESOURCE_HANDLE
- && (requestClient.getPriority() > currentLowestPriority)) {
+ && ((requestClient.getPriority() > currentLowestPriority) || (
+ (requestClient.getPriority() == currentLowestPriority) && isRequestFromSameProcess))) {
if (!reclaimResource(
getDemuxResource(inUseLowestPriorityDrHandle).getOwnerClientId(),
TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) {
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index d108f0d..f14a432 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -251,11 +251,6 @@
// {@link #restartActivityProcessIfVisible}.
restartingName = r.app.mName;
restartingUid = r.app.mUid;
- // Make EnsureActivitiesVisibleHelper#makeVisibleAndRestartIfNeeded not skip
- // restarting non-top activity.
- if (r != r.getTask().topRunningActivity()) {
- r.setVisibleRequested(false);
- }
}
r.activityStopped(icicle, persistentState, description);
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d15d094..324a0ad 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -4135,9 +4135,7 @@
} else if (!mVisibleRequested && launchCount > 2
&& lastLaunchTime > (SystemClock.uptimeMillis() - 60000)) {
// We have launched this activity too many times since it was able to run, so give up
- // and remove it. (Note if the activity is visible, we don't remove the record. We leave
- // the dead window on the screen but the process will not be restarted unless user
- // explicitly tap on it.)
+ // and remove it.
remove = true;
} else {
// The process may be gone, but the activity lives on!
@@ -4159,11 +4157,6 @@
if (DEBUG_APP) {
Slog.v(TAG_APP, "Keeping entry during removeHistory for activity " + this);
}
- // Set nowVisible to previous visible state. If the app was visible while it died, we
- // leave the dead window on screen so it's basically visible. This is needed when user
- // later tap on the dead window, we need to stop other apps when user transfers focus
- // to the restarted activity.
- nowVisible = mVisibleRequested;
}
// upgrade transition trigger to task if this is the last activity since it means we are
// closing the task.
@@ -5232,6 +5225,11 @@
Slog.w(TAG_WM, "Attempted to set visibility of non-existing app token: " + token);
return;
}
+ if (visible == mVisibleRequested && visible == mVisible
+ && mTransitionController.isShellTransitionsEnabled()) {
+ // For shell transition, it is no-op if there is no state change.
+ return;
+ }
if (visible) {
mDeferHidingClient = false;
}
@@ -5270,13 +5268,18 @@
// Before setting mVisibleRequested so we can track changes.
boolean isCollecting = false;
+ boolean inFinishingTransition = false;
if (mTransitionController.isShellTransitionsEnabled()) {
isCollecting = mTransitionController.isCollecting();
if (isCollecting) {
mTransitionController.collect(this);
} else {
- Slog.e(TAG, "setVisibility=" + visible + " while transition is not collecting "
- + this + " caller=" + Debug.getCallers(8));
+ inFinishingTransition = mTransitionController.inFinishingTransition(this);
+ if (!inFinishingTransition) {
+ Slog.e(TAG, "setVisibility=" + visible
+ + " while transition is not collecting or finishing "
+ + this + " caller=" + Debug.getCallers(8));
+ }
}
}
@@ -5290,10 +5293,6 @@
mLastDeferHidingClient = deferHidingClient;
if (!visible) {
- // If the app is dead while it was visible, we kept its dead window on screen.
- // Now that the app is going invisible, we can remove it. It will be restarted
- // if made visible again.
- removeDeadWindows();
// If this activity is about to finish/stopped and now becomes invisible, remove it
// from the unknownApp list in case the activity does not want to draw anything, which
// keep the user waiting for the next transition to start.
@@ -5357,6 +5356,10 @@
}
return;
}
+ if (inFinishingTransition) {
+ // Let the finishing transition commit the visibility.
+ return;
+ }
// If we are preparing an app transition, then delay changing
// the visibility of this token until we execute that transition.
if (deferCommitVisibilityChange(visible)) {
@@ -6642,9 +6645,6 @@
// stop tracking
mSplashScreenStyleSolidColor = true;
- // We now have a good window to show, remove dead placeholders
- removeDeadWindows();
-
if (mStartingWindow != null) {
ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Finish starting %s"
+ ": first real window is shown, no animation", win.mToken);
@@ -7380,20 +7380,6 @@
}
}
- void removeDeadWindows() {
- for (int winNdx = mChildren.size() - 1; winNdx >= 0; --winNdx) {
- WindowState win = mChildren.get(winNdx);
- if (win.mAppDied) {
- ProtoLog.w(WM_DEBUG_ADD_REMOVE,
- "removeDeadWindows: %s", win);
- // Set mDestroying, we don't want any animation or delayed removal here.
- win.mDestroying = true;
- // Also removes child windows.
- win.removeIfPossible();
- }
- }
- }
-
void setWillReplaceWindows(boolean animate) {
ProtoLog.d(WM_DEBUG_ADD_REMOVE,
"Marking app token %s with replacing windows.", this);
diff --git a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
index 5d038dc..be7d9b6 100644
--- a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
+++ b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
@@ -92,6 +92,7 @@
} else {
mInputWindowHandleWrapper.setInputConfigMasked(0, InputConfig.NOT_TOUCHABLE);
}
+ mInputWindowHandleWrapper.setDisplayId(mActivityRecord.getDisplayId());
return mInputWindowHandleWrapper;
}
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 211c230..ce29564 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1680,6 +1680,11 @@
targetTask.removeImmediately("bulky-task");
return START_ABORTED;
}
+ // When running transient transition, the transient launch target should keep on top.
+ // So disallow the transient hide activity to move itself to front, e.g. trampoline.
+ if (!mAvoidMoveToFront && r.mTransitionController.isTransientHide(targetTask)) {
+ mAvoidMoveToFront = true;
+ }
mPriorAboveTask = TaskDisplayArea.getRootTaskAbove(targetTask.getRootTask());
}
@@ -1796,7 +1801,7 @@
// root-task to the will not update the focused root-task. If starting the new
// activity now allows the task root-task to be focusable, then ensure that we
// now update the focused root-task accordingly.
- if (mTargetRootTask.isTopActivityFocusable()
+ if (!mAvoidMoveToFront && mTargetRootTask.isTopActivityFocusable()
&& !mRootWindowContainer.isTopDisplayFocusedRootTask(mTargetRootTask)) {
mTargetRootTask.moveToFront("startActivityInner");
}
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index f9f972c..b67bc62 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -862,8 +862,16 @@
WindowContainer target, boolean isOpen) {
final BackWindowAnimationAdaptor adaptor =
new BackWindowAnimationAdaptor(target, isOpen);
- target.startAnimation(target.getPendingTransaction(), adaptor, false /* hidden */,
- ANIMATION_TYPE_PREDICT_BACK);
+ final SurfaceControl.Transaction pt = target.getPendingTransaction();
+ target.startAnimation(pt, adaptor, false /* hidden */, ANIMATION_TYPE_PREDICT_BACK);
+ // Workaround to show TaskFragment which can be hide in Transitions and won't show
+ // during isAnimating.
+ if (isOpen && target.asActivityRecord() != null) {
+ final TaskFragment fragment = target.asActivityRecord().getTaskFragment();
+ if (fragment != null) {
+ pt.show(fragment.mSurfaceControl);
+ }
+ }
return adaptor;
}
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index dde89e9..9cc311d 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -193,7 +193,7 @@
}
if (!r.attachedToProcess()) {
- makeVisibleAndRestartIfNeeded(mStarting, mConfigChanges, isTop,
+ makeVisibleAndRestartIfNeeded(mStarting, mConfigChanges,
resumeTopActivity && isTop, r);
} else if (r.isVisibleRequested()) {
// If this activity is already visible, then there is nothing to do here.
@@ -243,15 +243,7 @@
}
private void makeVisibleAndRestartIfNeeded(ActivityRecord starting, int configChanges,
- boolean isTop, boolean andResume, ActivityRecord r) {
- // We need to make sure the app is running if it's the top, or it is just made visible from
- // invisible. If the app is already visible, it must have died while it was visible. In this
- // case, we'll show the dead window but will not restart the app. Otherwise we could end up
- // thrashing.
- if (!isTop && r.isVisibleRequested() && !r.isState(INITIALIZING)) {
- return;
- }
-
+ boolean andResume, ActivityRecord r) {
// This activity needs to be visible, but isn't even running...
// get it started and resume if no other root task in this root task is resumed.
if (DEBUG_VISIBILITY) {
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 1e9d451..0a47fe0 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -25,7 +25,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.graphics.PointF;
import android.os.Debug;
import android.os.IBinder;
import android.util.Slog;
@@ -221,11 +220,6 @@
}
@Override
- public PointF getCursorPosition() {
- return mService.getLatestMousePosition();
- }
-
- @Override
public void onPointerDownOutsideFocus(IBinder touchedToken) {
mService.mH.obtainMessage(ON_POINTER_DOWN_OUTSIDE_FOCUS, touchedToken).sendToTarget();
}
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index f355f08..5db39fc 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -997,7 +997,8 @@
@VisibleForTesting
boolean shouldShowLetterboxUi(WindowState mainWindow) {
- return isSurfaceVisible(mainWindow) && mainWindow.areAppWindowBoundsLetterboxed()
+ return (mActivityRecord.isInLetterboxAnimation() || isSurfaceVisible(mainWindow))
+ && mainWindow.areAppWindowBoundsLetterboxed()
// Check for FLAG_SHOW_WALLPAPER explicitly instead of using
// WindowContainer#showWallpaper because the later will return true when this
// activity is using blurred wallpaper for letterbox background.
@@ -1104,7 +1105,7 @@
// for all corners for consistency and pick a minimal bottom one for consistency with a
// taskbar rounded corners.
int getRoundedCornersRadius(final WindowState mainWindow) {
- if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) {
+ if (!requiresRoundedCorners(mainWindow)) {
return 0;
}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 969f65c..67ca844 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3398,8 +3398,10 @@
final boolean isTopActivityResumed = top != null
&& top.getOrganizedTask() == this && top.isState(RESUMED);
- // Whether the direct top activity is in size compat mode on foreground.
- info.topActivityInSizeCompat = isTopActivityResumed && top.inSizeCompatMode();
+ final boolean isTopActivityVisible = top != null
+ && top.getOrganizedTask() == this && top.isVisible();
+ // Whether the direct top activity is in size compat mode
+ info.topActivityInSizeCompat = isTopActivityVisible && top.inSizeCompatMode();
if (info.topActivityInSizeCompat
&& mWmService.mLetterboxConfiguration.isTranslucentLetterboxingEnabled()) {
// We hide the restart button in case of transparent activities.
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 2ddb307..7c57dc1 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1012,6 +1012,10 @@
if (isTopActivityLaunchedBehind()) {
return TASK_FRAGMENT_VISIBILITY_VISIBLE;
}
+ final Task thisTask = asTask();
+ if (thisTask != null && mTransitionController.isTransientHide(thisTask)) {
+ return TASK_FRAGMENT_VISIBILITY_VISIBLE;
+ }
boolean gotTranslucentFullscreen = false;
boolean gotTranslucentAdjacent = false;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 606f011..97f48b7 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -194,6 +194,13 @@
*/
private ArrayMap<ActivityRecord, Task> mTransientLaunches = null;
+ /**
+ * The tasks that may be occluded by the transient activity. Assume the task stack is
+ * [Home, A(opaque), B(opaque), C(translucent)] (bottom to top), then A is the restore-below
+ * task, and [B, C] are the transient-hide tasks.
+ */
+ private ArrayList<Task> mTransientHideTasks;
+
/** Custom activity-level animation options and callbacks. */
private TransitionInfo.AnimationOptions mOverrideOptions;
private IRemoteCallback mClientAnimationStartCallback = null;
@@ -265,35 +272,51 @@
void setTransientLaunch(@NonNull ActivityRecord activity, @Nullable Task restoreBelow) {
if (mTransientLaunches == null) {
mTransientLaunches = new ArrayMap<>();
+ mTransientHideTasks = new ArrayList<>();
}
mTransientLaunches.put(activity, restoreBelow);
setTransientLaunchToChanges(activity);
if (restoreBelow != null) {
- final ChangeInfo info = mChanges.get(restoreBelow);
- if (info != null) {
- info.mFlags |= ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH;
+ // Collect all visible activities which can be occluded by the transient activity to
+ // make sure they are in the participants so their visibilities can be updated when
+ // finishing transition.
+ ((WindowContainer<?>) restoreBelow.getParent()).forAllTasks(t -> {
+ if (t.isVisibleRequested() && !t.isAlwaysOnTop()
+ && !t.getWindowConfiguration().tasksAreFloating()) {
+ if (t.isRootTask()) {
+ mTransientHideTasks.add(t);
+ }
+ if (t.isLeafTask()) {
+ t.forAllActivities(r -> {
+ if (r.isVisibleRequested()) {
+ collect(r);
+ }
+ });
+ }
+ }
+ return t == restoreBelow;
+ });
+ // Add FLAG_ABOVE_TRANSIENT_LAUNCH to the tree of transient-hide tasks,
+ // so ChangeInfo#hasChanged() can return true to report the transition info.
+ for (int i = mChanges.size() - 1; i >= 0; --i) {
+ final WindowContainer<?> wc = mChanges.keyAt(i);
+ if (wc.asTaskFragment() == null && wc.asActivityRecord() == null) continue;
+ if (isInTransientHide(wc)) {
+ mChanges.valueAt(i).mFlags |= ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH;
+ }
}
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Transition %d: Set %s as "
+ "transient-launch", mSyncId, activity);
}
- boolean isTransientHide(@NonNull Task task) {
- if (mTransientLaunches == null) return false;
- for (int i = 0; i < mTransientLaunches.size(); ++i) {
- if (mTransientLaunches.valueAt(i) == task) {
- return true;
- }
- }
- return false;
- }
-
/** @return whether `wc` is a descendent of a transient-hide window. */
boolean isInTransientHide(@NonNull WindowContainer wc) {
- if (mTransientLaunches == null) return false;
- for (int i = 0; i < mTransientLaunches.size(); ++i) {
- if (wc.isDescendantOf(mTransientLaunches.valueAt(i))) {
+ if (mTransientHideTasks == null) return false;
+ for (int i = mTransientHideTasks.size() - 1; i >= 0; --i) {
+ final Task task = mTransientHideTasks.get(i);
+ if (wc == task || wc.isDescendantOf(task)) {
return true;
}
}
@@ -675,7 +698,7 @@
* needs to be passed/applied in shell because until finish is called, shell owns the surfaces.
* Additionally, this gives shell the ability to better deal with merged transitions.
*/
- private void buildFinishTransaction(SurfaceControl.Transaction t, SurfaceControl rootLeash) {
+ private void buildFinishTransaction(SurfaceControl.Transaction t, TransitionInfo info) {
final Point tmpPos = new Point();
// usually only size 1
final ArraySet<DisplayContent> displays = new ArraySet<>();
@@ -728,8 +751,8 @@
} finally {
mController.mBuildingFinishLayers = false;
}
- if (rootLeash.isValid()) {
- t.reparent(rootLeash, null);
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ t.reparent(info.getRoot(i).getLeash(), null);
}
}
@@ -814,6 +837,15 @@
if (mState < STATE_PLAYING) {
throw new IllegalStateException("Can't finish a non-playing transition " + mSyncId);
}
+ mController.mFinishingTransition = this;
+
+ if (mTransientHideTasks != null && !mTransientHideTasks.isEmpty()) {
+ // The transient hide tasks could be occluded now, e.g. returning to home. So trigger
+ // the update to make the activities in the tasks invisible-requested, then the next
+ // step can continue to commit the visibility.
+ mController.mAtm.mRootWindowContainer.ensureActivitiesVisible(null /* starting */,
+ 0 /* configChanges */, true /* preserveWindows */);
+ }
boolean hasParticipatedDisplay = false;
boolean hasVisibleTransientLaunch = false;
@@ -995,6 +1027,7 @@
// Handle back animation if it's already started.
mController.mAtm.mBackNavigationController.handleDeferredBackAnimation(mTargets);
+ mController.mFinishingTransition = null;
}
void abort() {
@@ -1066,13 +1099,6 @@
Slog.e(TAG, "Unexpected Sync ID " + syncId + ". Expected " + mSyncId);
return;
}
- if (mTargetDisplays.isEmpty()) {
- mTargetDisplays.add(mController.mAtm.mRootWindowContainer.getDefaultDisplay());
- }
- // While there can be multiple DC's involved. For now, we just use the first one as
- // the "primary" one for most things. Eventually, this will need to change, but, for the
- // time being, we don't have full cross-display transitions so it isn't a problem.
- final DisplayContent dc = mTargetDisplays.get(0);
// Commit the visibility of visible activities before calculateTransitionInfo(), so the
// TaskInfo can be visible. Also it needs to be done before moveToPlaying(), otherwise
@@ -1082,6 +1108,9 @@
if (mState == STATE_ABORT) {
mController.abort(this);
+ // Fall-back to the default display if there isn't one participating.
+ final DisplayContent dc = !mTargetDisplays.isEmpty() ? mTargetDisplays.get(0)
+ : mController.mAtm.mRootWindowContainer.getDefaultDisplay();
dc.getPendingTransaction().merge(transaction);
mSyncId = -1;
mOverrideOptions = null;
@@ -1094,16 +1123,24 @@
mFinishTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get();
mController.moveToPlaying(this);
- if (dc.isKeyguardLocked()) {
- mFlags |= TRANSIT_FLAG_KEYGUARD_LOCKED;
- }
-
// Check whether the participants were animated from back navigation.
final boolean markBackAnimated = mController.mAtm.mBackNavigationController
.containsBackAnimationTargets(this);
- // Resolve the animating targets from the participants
+ // Resolve the animating targets from the participants.
mTargets = calculateTargets(mParticipants, mChanges);
final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, transaction);
+
+ // Repopulate the displays based on the resolved targets.
+ mTargetDisplays.clear();
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ final DisplayContent dc = mController.mAtm.mRootWindowContainer.getDisplayContent(
+ info.getRoot(i).getDisplayId());
+ mTargetDisplays.add(dc);
+ if (dc.isKeyguardLocked()) {
+ mFlags |= TRANSIT_FLAG_KEYGUARD_LOCKED;
+ }
+ }
+
if (markBackAnimated) {
mController.mAtm.mBackNavigationController.clearBackAnimations(mStartTransaction);
}
@@ -1125,9 +1162,12 @@
}
// TODO(b/188669821): Move to animation impl in shell.
- handleLegacyRecentsStartBehavior(dc, info);
+ for (int i = 0; i < mTargetDisplays.size(); ++i) {
+ handleLegacyRecentsStartBehavior(mTargetDisplays.get(i), info);
+ if (mRecentsDisplayId != INVALID_DISPLAY) break;
+ }
- handleNonAppWindowsInTransition(dc, mType, mFlags);
+ handleNonAppWindowsInTransition(mType, mFlags);
// The callback is only populated for custom activity-level client animations
sendRemoteCallback(mClientAnimationStartCallback);
@@ -1166,14 +1206,13 @@
// Record windowtokens (activity/wallpaper) that are expected to be visible after the
// transition animation. This will be used in finishTransition to prevent prematurely
- // committing visibility.
- for (int i = mParticipants.size() - 1; i >= 0; --i) {
- final WindowContainer wc = mParticipants.valueAt(i);
- if (wc.asWindowToken() == null || !wc.isVisibleRequested()) continue;
- // don't include transient launches, though, since those are only temporarily visible.
- if (mTransientLaunches != null && wc.asActivityRecord() != null
- && mTransientLaunches.containsKey(wc.asActivityRecord())) continue;
- mVisibleAtTransitionEndTokens.add(wc.asWindowToken());
+ // committing visibility. Skip transient launches since those are only temporarily visible.
+ if (mTransientLaunches == null) {
+ for (int i = mParticipants.size() - 1; i >= 0; --i) {
+ final WindowContainer wc = mParticipants.valueAt(i);
+ if (wc.asWindowToken() == null || !wc.isVisibleRequested()) continue;
+ mVisibleAtTransitionEndTokens.add(wc.asWindowToken());
+ }
}
// Take task snapshots before the animation so that we can capture IME before it gets
@@ -1191,11 +1230,14 @@
// This is non-null only if display has changes. It handles the visible windows that don't
// need to be participated in the transition.
- final AsyncRotationController controller = dc.getAsyncRotationController();
- if (controller != null && containsChangeFor(dc, mTargets)) {
- controller.setupStartTransaction(transaction);
+ for (int i = 0; i < mTargetDisplays.size(); ++i) {
+ final DisplayContent dc = mTargetDisplays.get(i);
+ final AsyncRotationController controller = dc.getAsyncRotationController();
+ if (controller != null && containsChangeFor(dc, mTargets)) {
+ controller.setupStartTransaction(transaction);
+ }
}
- buildFinishTransaction(mFinishTransaction, info.getRootLeash());
+ buildFinishTransaction(mFinishTransaction, info);
if (mController.getTransitionPlayer() != null && mIsPlayerEnabled) {
mController.dispatchLegacyAppTransitionStarting(info, mStatusBarTransitionDelay);
try {
@@ -1216,10 +1258,13 @@
// client, we should finish and apply it here so the transactions aren't lost.
postCleanupOnFailure();
}
- final AccessibilityController accessibilityController =
- dc.mWmService.mAccessibilityController;
- if (accessibilityController.hasCallbacks()) {
- accessibilityController.onWMTransition(dc.getDisplayId(), mType);
+ for (int i = 0; i < mTargetDisplays.size(); ++i) {
+ final DisplayContent dc = mTargetDisplays.get(i);
+ final AccessibilityController accessibilityController =
+ dc.mWmService.mAccessibilityController;
+ if (accessibilityController.hasCallbacks()) {
+ accessibilityController.onWMTransition(dc.getDisplayId(), mType);
+ }
}
} else {
// No player registered or it's not enabled, so just finish/apply immediately
@@ -1261,7 +1306,7 @@
if (mFinishTransaction != null) {
mFinishTransaction.apply();
}
- mController.finishTransition(mToken);
+ mController.finishTransition(this);
}
private void cleanUpInternal() {
@@ -1296,16 +1341,15 @@
if ((mFlags & TRANSIT_FLAG_IS_RECENTS) == 0) {
return;
}
- mRecentsDisplayId = dc.mDisplayId;
// Recents has an input-consumer to grab input from the "live tile" app. Set that up here
final InputConsumerImpl recentsAnimationInputConsumer =
dc.getInputMonitor().getInputConsumer(INPUT_CONSUMER_RECENTS_ANIMATION);
+ ActivityRecord recentsActivity = null;
if (recentsAnimationInputConsumer != null) {
// find the top-most going-away activity and the recents activity. The top-most
// is used as layer reference while the recents is used for registering the consumer
// override.
- ActivityRecord recentsActivity = null;
ActivityRecord topActivity = null;
for (int i = 0; i < info.getChanges().size(); ++i) {
final TransitionInfo.Change change = info.getChanges().get(i);
@@ -1329,6 +1373,12 @@
}
}
+ if (recentsActivity == null) {
+ // No recents activity on `dc`, its probably on a different display.
+ return;
+ }
+ mRecentsDisplayId = dc.mDisplayId;
+
// The rest of this function handles nav-bar reparenting
if (!dc.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition()
@@ -1423,7 +1473,7 @@
}
}
- private void handleNonAppWindowsInTransition(@NonNull DisplayContent dc,
+ private void handleNonAppWindowsInTransition(
@TransitionType int transit, @TransitionFlags int flags) {
if ((flags & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0) {
// If the occlusion changed but the transition isn't an occlude/unocclude transition,
@@ -1801,6 +1851,41 @@
return wc instanceof DisplayContent;
}
+ private static int getDisplayId(@NonNull WindowContainer wc) {
+ return wc.getDisplayContent() != null
+ ? wc.getDisplayContent().getDisplayId() : INVALID_DISPLAY;
+ }
+
+ @VisibleForTesting
+ static void calculateTransitionRoots(@NonNull TransitionInfo outInfo,
+ ArrayList<ChangeInfo> sortedTargets,
+ @NonNull SurfaceControl.Transaction startT) {
+ // There needs to be a root on each display.
+ for (int i = 0; i < sortedTargets.size(); ++i) {
+ final WindowContainer<?> wc = sortedTargets.get(i).mContainer;
+ // Don't include wallpapers since they are in a different DA.
+ if (isWallpaper(wc)) continue;
+ final int endDisplayId = getDisplayId(wc);
+ if (endDisplayId < 0) continue;
+
+ // Check if Root was already created for this display with a higher-Z window
+ if (outInfo.findRootIndex(endDisplayId) >= 0) continue;
+
+ WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, wc);
+
+ // Make leash based on highest (z-order) direct child of ancestor with a participant.
+ WindowContainer leashReference = wc;
+ while (leashReference.getParent() != ancestor) {
+ leashReference = leashReference.getParent();
+ }
+ final SurfaceControl rootLeash = leashReference.makeAnimationLeash().setName(
+ "Transition Root: " + leashReference.getName()).build();
+ startT.setLayer(rootLeash, leashReference.getLastLayer());
+ outInfo.addRootLeash(endDisplayId, rootLeash,
+ ancestor.getBounds().left, ancestor.getBounds().top);
+ }
+ }
+
/**
* Construct a TransitionInfo object from a set of targets and changes. Also populates the
* root surface.
@@ -1811,37 +1896,13 @@
@NonNull
static TransitionInfo calculateTransitionInfo(@TransitionType int type, int flags,
ArrayList<ChangeInfo> sortedTargets,
- @Nullable SurfaceControl.Transaction startT) {
+ @NonNull SurfaceControl.Transaction startT) {
final TransitionInfo out = new TransitionInfo(type, flags);
-
- WindowContainer<?> topApp = null;
- for (int i = 0; i < sortedTargets.size(); i++) {
- final WindowContainer<?> wc = sortedTargets.get(i).mContainer;
- if (!isWallpaper(wc)) {
- topApp = wc;
- break;
- }
- }
- if (topApp == null) {
- out.setRootLeash(new SurfaceControl(), 0, 0);
+ calculateTransitionRoots(out, sortedTargets, startT);
+ if (out.getRootCount() == 0) {
return out;
}
- WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, topApp);
-
- // Make leash based on highest (z-order) direct child of ancestor with a participant.
- // TODO(b/261418859): Handle the case when the target contains window containers which
- // belong to a different display. As a workaround we use topApp, from which wallpaper
- // window container is removed, instead of sortedTargets here.
- WindowContainer leashReference = topApp;
- while (leashReference.getParent() != ancestor) {
- leashReference = leashReference.getParent();
- }
- final SurfaceControl rootLeash = leashReference.makeAnimationLeash().setName(
- "Transition Root: " + leashReference.getName()).build();
- startT.setLayer(rootLeash, leashReference.getLastLayer());
- out.setRootLeash(rootLeash, ancestor.getBounds().left, ancestor.getBounds().top);
-
// Convert all the resolved ChangeInfos into TransactionInfo.Change objects in order.
final int count = sortedTargets.size();
for (int i = 0; i < count; ++i) {
@@ -1861,6 +1922,7 @@
change.setMode(info.getTransitMode(target));
change.setStartAbsBounds(info.mAbsoluteBounds);
change.setFlags(info.getChangeFlags(target));
+ change.setDisplayId(info.mDisplayId, getDisplayId(target));
final Task task = target.asTask();
final TaskFragment taskFragment = target.asTaskFragment();
@@ -1947,6 +2009,15 @@
}
TransitionInfo.AnimationOptions animOptions = null;
+
+ // Check if the top-most app is an activity (ie. activity->activity). If so, make sure to
+ // honor its custom transition options.
+ WindowContainer<?> topApp = null;
+ for (int i = 0; i < sortedTargets.size(); i++) {
+ if (isWallpaper(sortedTargets.get(i).mContainer)) continue;
+ topApp = sortedTargets.get(i).mContainer;
+ break;
+ }
if (topApp.asActivityRecord() != null) {
final ActivityRecord topActivity = topApp.asActivityRecord();
animOptions = addCustomActivityTransition(topActivity, true/* open */, null);
@@ -1997,14 +2068,15 @@
private static WindowContainer<?> findCommonAncestor(
@NonNull ArrayList<ChangeInfo> targets,
@NonNull WindowContainer<?> topApp) {
+ final int displayId = getDisplayId(topApp);
WindowContainer<?> ancestor = topApp.getParent();
// Go up ancestor parent chain until all targets are descendants. Ancestor should never be
// null because all targets are attached.
for (int i = targets.size() - 1; i >= 0; i--) {
final ChangeInfo change = targets.get(i);
final WindowContainer wc = change.mContainer;
- if (isWallpaper(wc)) {
- // Skip the non-app window.
+ if (isWallpaper(wc) || getDisplayId(wc) != displayId) {
+ // Skip the non-app window or windows on a different display
continue;
}
while (!wc.isDescendantOf(ancestor)) {
@@ -2180,6 +2252,7 @@
final Rect mAbsoluteBounds = new Rect();
boolean mShowWallpaper;
int mRotation = ROTATION_UNDEFINED;
+ int mDisplayId = -1;
@ActivityInfo.Config int mKnownConfigChanges;
/** These are just extra info. They aren't used for change-detection. */
@@ -2197,6 +2270,7 @@
mShowWallpaper = origState.showWallpaper();
mRotation = origState.getWindowConfiguration().getRotation();
mStartParent = origState.getParent();
+ mDisplayId = getDisplayId(origState);
}
@VisibleForTesting
@@ -2228,7 +2302,8 @@
// assume no change in windowing-mode.
|| (mWindowingMode != 0 && mContainer.getWindowingMode() != mWindowingMode)
|| !mContainer.getBounds().equals(mAbsoluteBounds)
- || mRotation != mContainer.getWindowConfiguration().getRotation();
+ || mRotation != mContainer.getWindowConfiguration().getRotation()
+ || mDisplayId != getDisplayId(mContainer);
}
@TransitionInfo.TransitionMode
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index bacc6e6..86bb6b5 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -107,6 +107,9 @@
*/
private final ArrayList<Transition> mPlayingTransitions = new ArrayList<>();
+ /** The currently finishing transition. */
+ Transition mFinishingTransition;
+
/**
* The windows that request to be invisible while it is in transition. After the transition
* is finished and the windows are no longer animating, their surfaces will be destroyed.
@@ -313,6 +316,11 @@
return false;
}
+ /** Returns {@code true} if the `wc` is a participant of the finishing transition. */
+ boolean inFinishingTransition(WindowContainer<?> wc) {
+ return mFinishingTransition != null && mFinishingTransition.mParticipants.contains(wc);
+ }
+
/** @return {@code true} if a transition is running */
boolean inTransition() {
// TODO(shell-transitions): eventually properly support multiple
@@ -358,11 +366,11 @@
}
boolean isTransientHide(@NonNull Task task) {
- if (mCollectingTransition != null && mCollectingTransition.isTransientHide(task)) {
+ if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) {
return true;
}
for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
- if (mPlayingTransitions.get(i).isTransientHide(task)) return true;
+ if (mPlayingTransitions.get(i).isInTransientHide(task)) return true;
}
return false;
}
@@ -672,14 +680,13 @@
}
/** @see Transition#finishTransition */
- void finishTransition(@NonNull IBinder token) {
+ void finishTransition(Transition record) {
// It is usually a no-op but make sure that the metric consumer is removed.
- mTransitionMetricsReporter.reportAnimationStart(token, 0 /* startTime */);
+ mTransitionMetricsReporter.reportAnimationStart(record.getToken(), 0 /* startTime */);
// It is a no-op if the transition did not change the display.
mAtm.endLaunchPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY);
- final Transition record = Transition.fromBinder(token);
- if (record == null || !mPlayingTransitions.contains(record)) {
- Slog.e(TAG, "Trying to finish a non-playing transition " + token);
+ if (!mPlayingTransitions.contains(record)) {
+ Slog.e(TAG, "Trying to finish a non-playing transition " + record);
return;
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Finish Transition: %s", record);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 45cdacd..42d23e7 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -186,7 +186,6 @@
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Point;
-import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.configstore.V1_0.OptionalBool;
@@ -7361,14 +7360,6 @@
.setPointerIconType(PointerIcon.TYPE_DEFAULT);
}
}
-
- PointF getLatestMousePosition() {
- synchronized (mMousePositionTracker) {
- return new PointF(mMousePositionTracker.mLatestMouseX,
- mMousePositionTracker.mLatestMouseY);
- }
- }
-
void setMousePointerDisplayId(int displayId) {
mMousePositionTracker.setPointerDisplayId(displayId);
}
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index c3c87af..17d4f1b 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -135,7 +135,7 @@
*/
static final int CONTROLLABLE_CONFIGS = ActivityInfo.CONFIG_WINDOW_CONFIGURATION
| ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE | ActivityInfo.CONFIG_SCREEN_SIZE
- | ActivityInfo.CONFIG_LAYOUT_DIRECTION;
+ | ActivityInfo.CONFIG_LAYOUT_DIRECTION | ActivityInfo.CONFIG_DENSITY;
static final int CONTROLLABLE_WINDOW_CONFIGS = WINDOW_CONFIG_BOUNDS
| WindowConfiguration.WINDOW_CONFIG_APP_BOUNDS;
@@ -391,9 +391,14 @@
// apply the incoming transaction before finish in case it alters the visibility
// of the participants.
if (t != null) {
+ // Set the finishing transition before applyTransaction so the visibility
+ // changes of the transition participants will only set visible-requested
+ // and still let finishTransition handle the participants.
+ mTransitionController.mFinishingTransition = transition;
applyTransaction(t, syncId, null /*transition*/, caller, transition);
}
- getTransitionController().finishTransition(transitionToken);
+ mTransitionController.finishTransition(transition);
+ mTransitionController.mFinishingTransition = null;
if (syncId >= 0) {
setSyncReady(syncId);
}
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 694f1be..834b708 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -1381,6 +1381,13 @@
}
/**
+ * Destroys the WindwoProcessController, after the process has been removed.
+ */
+ void destroy() {
+ unregisterConfigurationListeners();
+ }
+
+ /**
* Check if activity configuration override for the activity process needs an update and perform
* if needed. By default we try to override the process configuration to match the top activity
* config to increase app compatibility with multi-window and multi-display. The process will
diff --git a/services/core/java/com/android/server/wm/WindowProcessControllerMap.java b/services/core/java/com/android/server/wm/WindowProcessControllerMap.java
index 2767972..424b043 100644
--- a/services/core/java/com/android/server/wm/WindowProcessControllerMap.java
+++ b/services/core/java/com/android/server/wm/WindowProcessControllerMap.java
@@ -19,8 +19,8 @@
import android.util.ArraySet;
import android.util.SparseArray;
-import java.util.Map;
import java.util.HashMap;
+import java.util.Map;
final class WindowProcessControllerMap {
@@ -67,6 +67,7 @@
mPidMap.remove(pid);
// remove process from mUidMap
removeProcessFromUidMap(proc);
+ proc.destroy();
}
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index f86b997..d6c0311 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -221,8 +221,6 @@
import android.view.IWindowFocusObserver;
import android.view.IWindowId;
import android.view.InputChannel;
-import android.view.InputEvent;
-import android.view.InputEventReceiver;
import android.view.InputWindowHandle;
import android.view.InsetsSource;
import android.view.InsetsState;
@@ -572,12 +570,6 @@
boolean mRemoveOnExit;
/**
- * Whether the app died while it was visible, if true we might need
- * to continue to show it until it's restarted.
- */
- boolean mAppDied;
-
- /**
* Set when the orientation is changing and this window has not yet
* been updated for the new orientation.
*/
@@ -760,7 +752,6 @@
*/
private InsetsState mFrozenInsetsState;
- private static final float DEFAULT_DIM_AMOUNT_DEAD_WINDOW = 0.5f;
private KeyInterceptionInfo mKeyInterceptionInfo;
/**
@@ -1504,13 +1495,6 @@
}
}
- // If it's a dead window left on screen, and the configuration changed, there is nothing
- // we can do about it. Remove the window now.
- if (mActivityRecord != null && mAppDied) {
- mActivityRecord.removeDeadWindows();
- return;
- }
-
onResizeHandled();
mWmService.makeWindowFreezingScreenIfNeededLocked(this);
@@ -2009,7 +1993,7 @@
boolean isInteresting() {
final RecentsAnimationController recentsAnimationController =
mWmService.getRecentsAnimationController();
- return mActivityRecord != null && !mAppDied
+ return mActivityRecord != null
&& (!mActivityRecord.isFreezingScreen() || !mAppFreezing)
&& mViewVisibility == View.VISIBLE
&& (recentsAnimationController == null
@@ -2448,11 +2432,6 @@
@Override
void removeIfPossible() {
- super.removeIfPossible();
- removeIfPossible(false /*keepVisibleDeadWindow*/);
- }
-
- private void removeIfPossible(boolean keepVisibleDeadWindow) {
mWindowRemovalAllowed = true;
ProtoLog.v(WM_DEBUG_ADD_REMOVE,
"removeIfPossible: %s callers=%s", this, Debug.getCallers(5));
@@ -2527,21 +2506,6 @@
// If we are not currently running the exit animation, we need to see about starting one
wasVisible = isVisible();
- if (keepVisibleDeadWindow) {
- ProtoLog.v(WM_DEBUG_ADD_REMOVE,
- "Not removing %s because app died while it's visible", this);
-
- mAppDied = true;
- setDisplayLayoutNeeded();
- mWmService.mWindowPlacerLocked.performSurfacePlacement();
-
- // Set up a replacement input channel since the app is now dead.
- // We need to catch tapping on the dead window to restart the app.
- openInputChannel(null);
- displayContent.getInputMonitor().updateInputWindowsLw(true /*force*/);
- return;
- }
-
// Remove immediately if there is display transition because the animation is
// usually unnoticeable (e.g. covered by rotation animation) and the animation
// bounds could be inconsistent, such as depending on when the window applies
@@ -2715,19 +2679,7 @@
|| (isVisible() && mActivityRecord != null && mActivityRecord.isVisible());
}
- private final class DeadWindowEventReceiver extends InputEventReceiver {
- DeadWindowEventReceiver(InputChannel inputChannel) {
- super(inputChannel, mWmService.mH.getLooper());
- }
- @Override
- public void onInputEvent(InputEvent event) {
- finishInputEvent(event, true);
- }
- }
- /** Fake event receiver for windows that died visible. */
- private DeadWindowEventReceiver mDeadWindowEventReceiver;
-
- void openInputChannel(InputChannel outInputChannel) {
+ void openInputChannel(@NonNull InputChannel outInputChannel) {
if (mInputChannel != null) {
throw new IllegalStateException("Window already has an input channel.");
}
@@ -2736,14 +2688,7 @@
mInputChannelToken = mInputChannel.getToken();
mInputWindowHandle.setToken(mInputChannelToken);
mWmService.mInputToWindowMap.put(mInputChannelToken, this);
- if (outInputChannel != null) {
- mInputChannel.copyTo(outInputChannel);
- } else {
- // If the window died visible, we setup a fake input channel, so that taps
- // can still detected by input monitor channel, and we can relaunch the app.
- // Create fake event receiver that simply reports all events as handled.
- mDeadWindowEventReceiver = new DeadWindowEventReceiver(mInputChannel);
- }
+ mInputChannel.copyTo(outInputChannel);
}
/**
@@ -2754,10 +2699,6 @@
}
void disposeInputChannel() {
- if (mDeadWindowEventReceiver != null) {
- mDeadWindowEventReceiver.dispose();
- mDeadWindowEventReceiver = null;
- }
if (mInputChannelToken != null) {
// Unregister server channel first otherwise it complains about broken channel.
mWmService.mInputManager.removeInputChannel(mInputChannelToken);
@@ -3084,11 +3025,10 @@
.windowForClientLocked(mSession, mClient, false);
Slog.i(TAG, "WIN DEATH: " + win);
if (win != null) {
- final DisplayContent dc = getDisplayContent();
if (win.mActivityRecord != null && win.mActivityRecord.findMainWindow() == win) {
mWmService.mTaskSnapshotController.onAppDied(win.mActivityRecord);
}
- win.removeIfPossible(shouldKeepVisibleDeadAppWindow());
+ win.removeIfPossible();
} else if (mHasSurface) {
Slog.e(TAG, "!!! LEAK !!! Window removed but surface still valid.");
WindowState.this.removeIfPossible();
@@ -3100,32 +3040,6 @@
}
}
- /**
- * Returns true if this window is visible and belongs to a dead app and shouldn't be removed,
- * because we want to preserve its location on screen to be re-activated later when the user
- * interacts with it.
- */
- private boolean shouldKeepVisibleDeadAppWindow() {
- if (!isVisible() || mActivityRecord == null || !mActivityRecord.isClientVisible()) {
- // Not a visible app window or the app isn't dead.
- return false;
- }
-
- if (mAttrs.token != mClient.asBinder()) {
- // The window was add by a client using another client's app token. We don't want to
- // keep the dead window around for this case since this is meant for 'real' apps.
- return false;
- }
-
- if (mAttrs.type == TYPE_APPLICATION_STARTING) {
- // We don't keep starting windows since they were added by the window manager before
- // the app even launched.
- return false;
- }
-
- return getWindowConfiguration().keepVisibleDeadAppWindowOnScreen();
- }
-
/** Returns {@code true} if this window desires key events. */
boolean canReceiveKeys() {
return canReceiveKeys(false /* fromUserTouch */);
@@ -3972,7 +3886,7 @@
@Override
public void notifyInsetsControlChanged() {
ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "notifyInsetsControlChanged for %s ", this);
- if (mAppDied || mRemoved) {
+ if (mRemoved) {
return;
}
final InsetsStateController stateController =
@@ -4278,7 +4192,6 @@
pw.println(prefix + "mToken=" + mToken);
if (mActivityRecord != null) {
pw.println(prefix + "mActivityRecord=" + mActivityRecord);
- pw.print(prefix + "mAppDied=" + mAppDied);
pw.print(prefix + "drawnStateEvaluated=" + getDrawnStateEvaluated());
pw.println(prefix + "mightAffectAllDrawn=" + mightAffectAllDrawn());
}
@@ -5407,10 +5320,7 @@
}
private void applyDims() {
- if (!mAnimatingExit && mAppDied) {
- mIsDimming = true;
- getDimmer().dimAbove(getSyncTransaction(), this, DEFAULT_DIM_AMOUNT_DEAD_WINDOW);
- } else if (((mAttrs.flags & FLAG_DIM_BEHIND) != 0 || shouldDrawBlurBehind())
+ if (((mAttrs.flags & FLAG_DIM_BEHIND) != 0 || shouldDrawBlurBehind())
&& isVisibleNow() && !mHidden) {
// Only show the Dimmer when the following is satisfied:
// 1. The window has the flag FLAG_DIM_BEHIND or blur behind is requested
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index b4e2fb6..da44da4 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -308,6 +308,7 @@
void setMotionClassifierEnabled(bool enabled);
std::optional<std::string> getBluetoothAddress(int32_t deviceId);
void setStylusButtonMotionEventsEnabled(bool enabled);
+ FloatPoint getMouseCursorPosition();
/* --- InputReaderPolicyInterface implementation --- */
@@ -366,7 +367,7 @@
virtual PointerIconStyle getDefaultPointerIconId();
virtual PointerIconStyle getDefaultStylusIconId();
virtual PointerIconStyle getCustomPointerIconId();
- virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos);
+ virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position);
/* --- If touch mode is enabled per display or global --- */
@@ -730,11 +731,11 @@
return controller;
}
-void NativeInputManager::onPointerDisplayIdChanged(int32_t pointerDisplayId, float xPos,
- float yPos) {
+void NativeInputManager::onPointerDisplayIdChanged(int32_t pointerDisplayId,
+ const FloatPoint& position) {
JNIEnv* env = jniEnv();
env->CallVoidMethod(mServiceObj, gServiceClassInfo.onPointerDisplayIdChanged, pointerDisplayId,
- xPos, yPos);
+ position.x, position.y);
checkAndClearExceptionFromCallback(env, "onPointerDisplayIdChanged");
}
@@ -1655,6 +1656,14 @@
return static_cast<bool>(enabled);
}
+FloatPoint NativeInputManager::getMouseCursorPosition() {
+ AutoMutex _l(mLock);
+ const auto pc = mLocked.pointerController.lock();
+ if (!pc) return {AMOTION_EVENT_INVALID_CURSOR_POSITION, AMOTION_EVENT_INVALID_CURSOR_POSITION};
+
+ return pc->getPosition();
+}
+
// ----------------------------------------------------------------------------
static NativeInputManager* getNativeInputManager(JNIEnv* env, jobject clazz) {
@@ -2547,6 +2556,15 @@
im->setStylusButtonMotionEventsEnabled(enabled);
}
+static jfloatArray nativeGetMouseCursorPosition(JNIEnv* env, jobject nativeImplObj) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+ const auto p = im->getMouseCursorPosition();
+ const std::array<float, 2> arr = {{p.x, p.y}};
+ jfloatArray outArr = env->NewFloatArray(2);
+ env->SetFloatArrayRegion(outArr, 0, arr.size(), arr.data());
+ return outArr;
+}
+
// ----------------------------------------------------------------------------
static const JNINativeMethod gInputManagerMethods[] = {
@@ -2640,6 +2658,7 @@
{"getBluetoothAddress", "(I)Ljava/lang/String;", (void*)nativeGetBluetoothAddress},
{"setStylusButtonMotionEventsEnabled", "(Z)V",
(void*)nativeSetStylusButtonMotionEventsEnabled},
+ {"getMouseCursorPosition", "()[F", (void*)nativeGetMouseCursorPosition},
};
#define FIND_CLASS(var, className) \
diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
index 94260e20..e09c0a2 100644
--- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
@@ -122,7 +122,7 @@
private void respondToClientWithResponseAndFinish() {
Log.i(TAG, "respondToClientWithResponseAndFinish");
if (isSessionCancelled()) {
- mChosenProviderMetric.setChosenProviderStatus(
+ mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode());
logApiCall(ApiName.CLEAR_CREDENTIAL, /* apiStatus */
ApiStatus.CLIENT_CANCELED);
@@ -134,7 +134,7 @@
logApiCall(ApiName.CLEAR_CREDENTIAL, /* apiStatus */
ApiStatus.SUCCESS);
} catch (RemoteException e) {
- mChosenProviderMetric.setChosenProviderStatus(
+ mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode());
Log.i(TAG, "Issue while propagating the response to the client");
logApiCall(ApiName.CLEAR_CREDENTIAL, /* apiStatus */
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
index 47b8c7d..793d83e 100644
--- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -83,6 +83,7 @@
@Override
protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
+ mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(System.nanoTime());
try {
mClientCallback.onPendingIntent(mCredentialManagerUi.createPendingIntent(
RequestInfo.newCreateRequestInfo(
@@ -90,6 +91,7 @@
mClientAppInfo.getPackageName()),
providerDataList));
} catch (RemoteException e) {
+ mChosenProviderFinalPhaseMetric.setUiReturned(false);
respondToClientWithErrorAndFinish(
CreateCredentialException.TYPE_UNKNOWN,
"Unable to invoke selector");
@@ -99,14 +101,16 @@
@Override
public void onFinalResponseReceived(ComponentName componentName,
@Nullable CreateCredentialResponse response) {
+ mChosenProviderFinalPhaseMetric.setUiReturned(true);
+ mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(System.nanoTime());
Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString());
setChosenMetric(componentName);
if (response != null) {
- mChosenProviderMetric.setChosenProviderStatus(
+ mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode());
respondToClientWithResponseAndFinish(response);
} else {
- mChosenProviderMetric.setChosenProviderStatus(
+ mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode());
respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_NO_CREATE_OPTIONS,
"Invalid response");
@@ -138,6 +142,8 @@
private void respondToClientWithResponseAndFinish(CreateCredentialResponse response) {
Log.i(TAG, "respondToClientWithResponseAndFinish");
+ // TODO immediately add exception bit to chosen provider and do final emits across all
+ // including sequenceCounter!
if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
Log.i(TAG, "Request has already been completed. This is strange.");
return;
@@ -162,6 +168,7 @@
private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) {
Log.i(TAG, "respondToClientWithErrorAndFinish");
+
if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
Log.i(TAG, "Request has already been completed. This is strange.");
return;
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index 10d3dc0..4c5c366 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -41,6 +41,7 @@
import android.credentials.ICreateCredentialCallback;
import android.credentials.ICredentialManager;
import android.credentials.IGetCredentialCallback;
+import android.credentials.IGetPendingCredentialCallback;
import android.credentials.ISetEnabledProvidersCallback;
import android.credentials.RegisterCredentialDescriptionRequest;
import android.credentials.UnregisterCredentialDescriptionRequest;
@@ -438,6 +439,17 @@
return cancelTransport;
}
+ @Override
+ public ICancellationSignal executeGetPendingCredential(
+ GetCredentialRequest request,
+ IGetPendingCredentialCallback callback,
+ final String callingPackage) {
+ // TODO(b/273308895): implement
+
+ ICancellationSignal cancelTransport = CancellationSignal.createTransport();
+ return cancelTransport;
+ }
+
private void processGetCredential(
GetCredentialRequest request,
IGetCredentialCallback callback,
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index 8e90c09..00fbbba 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -82,12 +82,14 @@
@Override
protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
+ mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(System.nanoTime());
try {
mClientCallback.onPendingIntent(mCredentialManagerUi.createPendingIntent(
RequestInfo.newGetRequestInfo(
mRequestId, mClientRequest, mClientAppInfo.getPackageName()),
providerDataList));
} catch (RemoteException e) {
+ mChosenProviderFinalPhaseMetric.setUiReturned(false);
respondToClientWithErrorAndFinish(
GetCredentialException.TYPE_UNKNOWN, "Unable to instantiate selector");
}
@@ -96,14 +98,16 @@
@Override
public void onFinalResponseReceived(ComponentName componentName,
@Nullable GetCredentialResponse response) {
+ mChosenProviderFinalPhaseMetric.setUiReturned(true);
+ mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(System.nanoTime());
Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString());
setChosenMetric(componentName);
if (response != null) {
- mChosenProviderMetric.setChosenProviderStatus(
+ mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode());
respondToClientWithResponseAndFinish(response);
} else {
- mChosenProviderMetric.setChosenProviderStatus(
+ mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode());
respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL,
"Invalid response from provider");
diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
index 1b3e37a..ed139b5 100644
--- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java
+++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
@@ -25,7 +25,7 @@
import com.android.server.credentials.metrics.ApiName;
import com.android.server.credentials.metrics.ApiStatus;
import com.android.server.credentials.metrics.CandidatePhaseMetric;
-import com.android.server.credentials.metrics.ChosenProviderMetric;
+import com.android.server.credentials.metrics.ChosenProviderFinalPhaseMetric;
import com.android.server.credentials.metrics.InitialPhaseMetric;
import java.util.Map;
@@ -89,11 +89,11 @@
* @param apiStatus the api status to log
* @param providers a map with known providers
* @param callingUid the calling UID of the client app
- * @param chosenProviderMetric the metric data type of the final chosen provider
+ * @param chosenProviderFinalPhaseMetric the metric data type of the final chosen provider
*/
protected static void logApiCalled(ApiName apiName, ApiStatus apiStatus,
Map<String, ProviderSession> providers, int callingUid,
- ChosenProviderMetric chosenProviderMetric) {
+ ChosenProviderFinalPhaseMetric chosenProviderFinalPhaseMetric) {
try {
var providerSessions = providers.values();
int providerSize = providerSessions.size();
@@ -102,7 +102,7 @@
int[] candidateStatusList = new int[providerSize];
int index = 0;
for (var session : providerSessions) {
- CandidatePhaseMetric metric = session.mCandidateProviderMetric;
+ CandidatePhaseMetric metric = session.mCandidatePhasePerProviderMetric;
candidateUidList[index] = metric.getCandidateUid();
candidateQueryRoundTripTimeList[index] = metric.getQueryLatencyMicroseconds();
candidateStatusList[index] = metric.getProviderQueryStatus();
@@ -116,12 +116,13 @@
/* repeated_candidate_provider_round_trip_time_query_microseconds */
candidateQueryRoundTripTimeList,
/* repeated_candidate_provider_status */ candidateStatusList,
- /* chosen_provider_uid */ chosenProviderMetric.getChosenUid(),
+ /* chosen_provider_uid */ chosenProviderFinalPhaseMetric.getChosenUid(),
/* chosen_provider_round_trip_time_overall_microseconds */
- chosenProviderMetric.getEntireProviderLatencyMicroseconds(),
+ chosenProviderFinalPhaseMetric.getEntireProviderLatencyMicroseconds(),
/* chosen_provider_final_phase_microseconds (backwards compat only) */
DEFAULT_INT_32,
- /* chosen_provider_status */ chosenProviderMetric.getChosenProviderStatus());
+ /* chosen_provider_status */ chosenProviderFinalPhaseMetric
+ .getChosenProviderStatus());
} catch (Exception e) {
Log.w(TAG, "Unexpected error during metric logging: " + e);
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
index ab29acc..950cf4f 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
@@ -120,7 +120,14 @@
@Override
protected void invokeSession() {
if (mRemoteCredentialService != null) {
- mCandidateProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
+ /*
+ InitialPhaseMetric initMetric = ((RequestSession)mCallbacks).initMetric;
+ TODO immediately once the other change patched through
+ mCandidateProviderMetric.setSessionId(initMetric
+ .mInitialPhaseMetric.getSessionId());
+ mCandidateProviderMetric.setStartTime(initMetric.getStartTime())
+ */
+ mCandidatePhasePerProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
mRemoteCredentialService.onClearCredentialState(mProviderRequest, this);
}
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
index 8c9c6cf..3ec0fc0 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
@@ -226,7 +226,14 @@
@Override
protected void invokeSession() {
if (mRemoteCredentialService != null) {
- mCandidateProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
+ /*
+ InitialPhaseMetric initMetric = ((RequestSession)mCallbacks).initMetric;
+ TODO immediately once the other change patched through
+ mCandidateProviderMetric.setSessionId(initMetric
+ .mInitialPhaseMetric.getSessionId());
+ mCandidateProviderMetric.setStartTime(initMetric.getStartTime())
+ */
+ mCandidatePhasePerProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
mRemoteCredentialService.onCreateCredential(mProviderRequest, this);
}
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index 955b721..ec8bf22 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -61,13 +61,13 @@
RemoteCredentialService.ProviderCallbacks<BeginGetCredentialResponse> {
private static final String TAG = "ProviderGetSession";
// Key to be used as the entry key for an action entry
- private static final String ACTION_ENTRY_KEY = "action_key";
+ public static final String ACTION_ENTRY_KEY = "action_key";
// Key to be used as the entry key for the authentication entry
- private static final String AUTHENTICATION_ACTION_ENTRY_KEY = "authentication_action_key";
+ public static final String AUTHENTICATION_ACTION_ENTRY_KEY = "authentication_action_key";
// Key to be used as an entry key for a remote entry
- private static final String REMOTE_ENTRY_KEY = "remote_entry_key";
+ public static final String REMOTE_ENTRY_KEY = "remote_entry_key";
// Key to be used as an entry key for a credential entry
- private static final String CREDENTIAL_ENTRY_KEY = "credential_key";
+ public static final String CREDENTIAL_ENTRY_KEY = "credential_key";
@NonNull
private final Map<String, CredentialOption> mBeginGetOptionToCredentialOptionMap;
@@ -269,7 +269,14 @@
@Override
protected void invokeSession() {
if (mRemoteCredentialService != null) {
- mCandidateProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
+ /*
+ InitialPhaseMetric initMetric = ((RequestSession)mCallbacks).initMetric;
+ TODO immediately once the other change patched through
+ mCandidateProviderMetric.setSessionId(initMetric
+ .mInitialPhaseMetric.getSessionId());
+ mCandidateProviderMetric.setStartTime(initMetric.getStartTime())
+ */
+ mCandidatePhasePerProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
mRemoteCredentialService.onBeginGetCredential(mProviderRequest, this);
}
}
@@ -427,10 +434,13 @@
private void onSetInitialRemoteResponse(BeginGetCredentialResponse response) {
mProviderResponse = response;
addToInitialRemoteResponse(response, /*isInitialResponse=*/true);
+ // Log the data.
if (mProviderResponseDataHandler.isEmptyResponse(response)) {
updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE);
return;
}
+ // TODO immediately, add to Candidate Phase counts, repeat across all sessions
+ // Use sets to dedup type counts
updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED);
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java
index 03e2a32..77d4e77 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -38,6 +38,7 @@
/**
* Provider session storing the state of provider response and ui entries.
+ *
* @param <T> The request to be sent to the provider
* @param <R> The response to be expected from the provider
*/
@@ -46,21 +47,35 @@
private static final String TAG = "ProviderSession";
- @NonNull protected final Context mContext;
- @NonNull protected final ComponentName mComponentName;
- @Nullable protected final CredentialProviderInfo mProviderInfo;
- @Nullable protected final RemoteCredentialService mRemoteCredentialService;
- @NonNull protected final int mUserId;
- @NonNull protected Status mStatus = Status.NOT_STARTED;
- @Nullable protected final ProviderInternalCallback mCallbacks;
- @Nullable protected Credential mFinalCredentialResponse;
- @Nullable protected ICancellationSignal mProviderCancellationSignal;
- @NonNull protected final T mProviderRequest;
- @Nullable protected R mProviderResponse;
- @NonNull protected Boolean mProviderResponseSet = false;
+ @NonNull
+ protected final Context mContext;
+ @NonNull
+ protected final ComponentName mComponentName;
+ @Nullable
+ protected final CredentialProviderInfo mProviderInfo;
+ @Nullable
+ protected final RemoteCredentialService mRemoteCredentialService;
+ @NonNull
+ protected final int mUserId;
+ @NonNull
+ protected Status mStatus = Status.NOT_STARTED;
+ @Nullable
+ protected final ProviderInternalCallback mCallbacks;
+ @Nullable
+ protected Credential mFinalCredentialResponse;
+ @Nullable
+ protected ICancellationSignal mProviderCancellationSignal;
+ @NonNull
+ protected final T mProviderRequest;
+ @Nullable
+ protected R mProviderResponse;
+ @NonNull
+ protected Boolean mProviderResponseSet = false;
// Specific candidate provider metric for the provider this session handles
- @Nullable protected CandidatePhaseMetric mCandidateProviderMetric;
- @NonNull private int mProviderSessionUid;
+ @Nullable
+ protected CandidatePhaseMetric mCandidatePhasePerProviderMetric;
+ @NonNull
+ private int mProviderSessionUid;
/**
* Returns true if the given status reflects that the provider state is ready to be shown
@@ -100,6 +115,7 @@
* Interface to be implemented by any class that wishes to get a callback when a particular
* provider session's status changes. Typically, implemented by the {@link RequestSession}
* class.
+ *
* @param <V> the type of the final response expected
*/
public interface ProviderInternalCallback<V> {
@@ -127,7 +143,7 @@
mUserId = userId;
mComponentName = componentName;
mRemoteCredentialService = remoteCredentialService;
- mCandidateProviderMetric = new CandidatePhaseMetric();
+ mCandidatePhasePerProviderMetric = new CandidatePhaseMetric();
mProviderSessionUid = MetricUtilities.getPackageUid(mContext, mComponentName);
}
@@ -158,7 +174,7 @@
}
public Credential getFinalCredentialResponse() {
- return mFinalCredentialResponse;
+ return mFinalCredentialResponse;
}
/** Propagates cancellation signal to the remote provider service. */
@@ -192,7 +208,7 @@
return mRemoteCredentialService;
}
- /** Updates the status .*/
+ /** Updates the status . */
protected void updateStatusAndInvokeCallback(@NonNull Status status) {
setStatus(status);
updateCandidateMetric(status);
@@ -200,15 +216,18 @@
}
private void updateCandidateMetric(Status status) {
- mCandidateProviderMetric.setCandidateUid(mProviderSessionUid);
- mCandidateProviderMetric
+ mCandidatePhasePerProviderMetric.setCandidateUid(mProviderSessionUid);
+ // TODO immediately update the candidate phase here to have more new data
+ mCandidatePhasePerProviderMetric
.setQueryFinishTimeNanoseconds(System.nanoTime());
if (isTerminatingStatus(status)) {
- mCandidateProviderMetric.setProviderQueryStatus(ProviderStatusForMetrics.QUERY_FAILURE
- .getMetricCode());
+ mCandidatePhasePerProviderMetric.setProviderQueryStatus(
+ ProviderStatusForMetrics.QUERY_FAILURE
+ .getMetricCode());
} else if (isCompletionStatus(status)) {
- mCandidateProviderMetric.setProviderQueryStatus(ProviderStatusForMetrics.QUERY_SUCCESS
- .getMetricCode());
+ mCandidatePhasePerProviderMetric.setProviderQueryStatus(
+ ProviderStatusForMetrics.QUERY_SUCCESS
+ .getMetricCode());
}
}
@@ -228,7 +247,8 @@
}
/** Update the response state stored with the provider session. */
- @Nullable protected R getProviderResponse() {
+ @Nullable
+ protected R getProviderResponse() {
return mProviderResponse;
}
@@ -265,15 +285,20 @@
return false;
}
- /** Should be overridden to prepare, and stores state for {@link ProviderData} to be
- * shown on the UI. */
- @Nullable protected abstract ProviderData prepareUiData();
+ /**
+ * Should be overridden to prepare, and stores state for {@link ProviderData} to be
+ * shown on the UI.
+ */
+ @Nullable
+ protected abstract ProviderData prepareUiData();
/** Should be overridden to handle the selected entry from the UI. */
protected abstract void onUiEntrySelected(String entryType, String entryId,
ProviderPendingIntentResponse providerPendingIntentResponse);
- /** Should be overridden to invoke the provider at a defined location. Helpful for
- * situations such as metric generation. */
+ /**
+ * Should be overridden to invoke the provider at a defined location. Helpful for
+ * situations such as metric generation.
+ */
protected abstract void invokeSession();
}
diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java
index edddba0..5dc7adf 100644
--- a/services/credentials/java/com/android/server/credentials/RequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -36,12 +36,15 @@
import com.android.internal.R;
import com.android.server.credentials.metrics.ApiName;
import com.android.server.credentials.metrics.ApiStatus;
+import com.android.server.credentials.metrics.CandidateBrowsingPhaseMetric;
import com.android.server.credentials.metrics.CandidatePhaseMetric;
-import com.android.server.credentials.metrics.ChosenProviderMetric;
+import com.android.server.credentials.metrics.ChosenProviderFinalPhaseMetric;
+import com.android.server.credentials.metrics.EntryEnum;
import com.android.server.credentials.metrics.InitialPhaseMetric;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/**
@@ -75,8 +78,16 @@
protected final CancellationSignal mCancellationSignal;
protected final Map<String, ProviderSession> mProviders = new HashMap<>();
- protected ChosenProviderMetric mChosenProviderMetric = new ChosenProviderMetric();
protected InitialPhaseMetric mInitialPhaseMetric = new InitialPhaseMetric();
+ protected ChosenProviderFinalPhaseMetric
+ mChosenProviderFinalPhaseMetric = new ChosenProviderFinalPhaseMetric();
+
+ // TODO(b/271135048) - Group metrics used in a scope together, such as here in RequestSession
+ // TODO(b/271135048) - Replace this with a new atom per each browsing emit (V4)
+ @NonNull
+ protected List<CandidateBrowsingPhaseMetric> mCandidateBrowsingPhaseMetric = new ArrayList<>();
+ // As emits occur in sequential order, increment this counter and utilize
+ protected int mSequenceCounter = 0;
protected final String mHybridService;
@NonNull
@@ -152,10 +163,22 @@
return;
}
Log.i(TAG, "Provider session found");
+ logBrowsingPhasePerSelect(selection, providerSession);
providerSession.onUiEntrySelected(selection.getEntryKey(),
selection.getEntrySubkey(), selection.getPendingIntentProviderResponse());
}
+ private void logBrowsingPhasePerSelect(UserSelectionDialogResult selection,
+ ProviderSession providerSession) {
+ CandidateBrowsingPhaseMetric browsingPhaseMetric = new CandidateBrowsingPhaseMetric();
+ browsingPhaseMetric.setSessionId(this.mInitialPhaseMetric.getSessionId());
+ browsingPhaseMetric.setEntryEnum(
+ EntryEnum.getMetricCodeFromString(selection.getEntryKey()));
+ browsingPhaseMetric.setProviderUid(providerSession.mCandidatePhasePerProviderMetric
+ .getCandidateUid());
+ this.mCandidateBrowsingPhaseMetric.add(new CandidateBrowsingPhaseMetric());
+ }
+
protected void finishSession(boolean propagateCancellation) {
Log.i(TAG, "finishing session");
if (propagateCancellation) {
@@ -176,7 +199,14 @@
protected void logApiCall(ApiName apiName, ApiStatus apiStatus) {
logApiCalled(apiName, apiStatus, mProviders, mCallingUid,
- mChosenProviderMetric);
+ mChosenProviderFinalPhaseMetric);
+ }
+
+ protected void logApiCall(ChosenProviderFinalPhaseMetric finalPhaseMetric,
+ List<CandidateBrowsingPhaseMetric> browsingPhaseMetrics) {
+ // TODO (b/270403549) - this browsing phase object is fine but also have a new emit
+ // For the returned types by authentication entries - i.e. a CandidatePhase During Browse
+ // TODO call MetricUtilities with new setup
}
protected boolean isSessionCancelled() {
@@ -218,8 +248,10 @@
}
if (!providerDataList.isEmpty()) {
Log.i(TAG, "provider list not empty about to initiate ui");
+ // TODO immediately Add paths to end it (say it fails)
if (isSessionCancelled()) {
Log.i(TAG, "In getProviderDataAndInitiateUi but session has been cancelled");
+ // TODO immedaitely Add paths
} else {
launchUiWithProviderData(providerDataList);
}
@@ -233,11 +265,21 @@
*/
protected void setChosenMetric(ComponentName componentName) {
CandidatePhaseMetric metric = this.mProviders.get(componentName.flattenToString())
- .mCandidateProviderMetric;
- mChosenProviderMetric.setChosenUid(metric.getCandidateUid());
- mChosenProviderMetric.setFinalFinishTimeNanoseconds(System.nanoTime());
- mChosenProviderMetric.setQueryPhaseLatencyMicroseconds(
+ .mCandidatePhasePerProviderMetric;
+
+ mChosenProviderFinalPhaseMetric.setSessionId(metric.getSessionId());
+ mChosenProviderFinalPhaseMetric.setChosenUid(metric.getCandidateUid());
+
+ mChosenProviderFinalPhaseMetric.setQueryPhaseLatencyMicroseconds(
metric.getQueryLatencyMicroseconds());
- mChosenProviderMetric.setQueryStartTimeNanoseconds(metric.getStartQueryTimeNanoseconds());
+
+ mChosenProviderFinalPhaseMetric.setServiceBeganTimeNanoseconds(
+ metric.getServiceBeganTimeNanoseconds());
+ mChosenProviderFinalPhaseMetric.setQueryStartTimeNanoseconds(
+ metric.getStartQueryTimeNanoseconds());
+
+ // TODO immediately update with the entry count numbers from the candidate metrics
+
+ mChosenProviderFinalPhaseMetric.setFinalFinishTimeNanoseconds(System.nanoTime());
}
}
diff --git a/services/credentials/java/com/android/server/credentials/metrics/CandidateBrowsingPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/CandidateBrowsingPhaseMetric.java
index 37ec8f0..0e1e0389 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/CandidateBrowsingPhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/CandidateBrowsingPhaseMetric.java
@@ -17,23 +17,22 @@
package com.android.server.credentials.metrics;
/**
- * A part of the Candidate Phase, but emitted alongside {@link ChosenProviderMetric}. The user is
- * shown various entries from the provider responses, and may selectively browse through many
- * entries. It is possible that the initial set of browsing is for a provider that is ultimately
- * not chosen. This metric will be gathered PER browsing click, and aggregated, so that we can
- * understand where user interaction is more cumbersome, informing us for future improvements. This
- * can only be complete when the browsing is finished, ending in a final user choice, or possibly
- * a cancellation. Thus, this will be collected and emitted in the final phase, though collection
- * will begin in the candidate phase when the user begins browsing options.
+ * A part of the Candidate Phase, but emitted alongside {@link ChosenProviderFinalPhaseMetric}.
+ * The user is shown various entries from the provider responses, and may selectively browse through
+ * many entries. It is possible that the initial set of browsing is for a provider that is
+ * ultimately not chosen. This metric will be gathered PER browsing click, and aggregated, so that
+ * we can understand where user interaction is more cumbersome, informing us for future
+ * improvements. This can only be complete when the browsing is finished, ending in a final user
+ * choice, or possibly a cancellation. Thus, this will be collected and emitted in the final phase,
+ * though collection will begin in the candidate phase when the user begins browsing options.
*/
public class CandidateBrowsingPhaseMetric {
- private static final String TAG = "CandidateSelectionPhaseMetric";
- private static final int SEQUENCE_ID = 3;
+ private static final String TAG = "CandidateBrowsingPhaseMetric";
// The session id associated with the API Call this candidate provider is a part of, default -1
private int mSessionId = -1;
- // The EntryEnum that was pressed, defaults to -1 (TODO immediately, generate entry enum).
- private int mEntryEnum = -1;
+ // The EntryEnum that was pressed, defaults to -1
+ private int mEntryEnum = EntryEnum.UNKNOWN.getMetricCode();
// The provider associated with the press, defaults to -1
private int mProviderUid = -1;
@@ -47,12 +46,6 @@
return mSessionId;
}
- /* -- The sequence ID -- */
-
- public int getSequenceId() {
- return SEQUENCE_ID;
- }
-
/* -- The Entry of this tap -- */
public void setEntryEnum(int entryEnum) {
diff --git a/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java
index 1c7fb69..c392d78 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java
@@ -30,10 +30,8 @@
public class CandidatePhaseMetric {
private static final String TAG = "CandidateProviderMetric";
- // Since this will always be the second in the split sequence, this is statically 2
- private static final int SESSION_ID = 2;
- // The sequence number of this emit of the API call, default -1, equal for all candidates
- private int mSequenceId = -1;
+ // The session id of this provider, default set to -1
+ private int mSessionId = -1;
// Indicates if this provider returned from the query phase, default false
private boolean mQueryReturned = false;
@@ -150,18 +148,13 @@
}
/* -------------- Session Id ---------------- */
+
+ public void setSessionId(int sessionId) {
+ mSessionId = sessionId;
+ }
+
public int getSessionId() {
- return SESSION_ID;
- }
-
- /* -------------- Sequence Id ---------------- */
-
- public void setSequenceId(int sequenceId) {
- mSequenceId = sequenceId;
- }
-
- public int getSequenceId() {
- return mSequenceId;
+ return mSessionId;
}
/* -------------- Query Returned Status ---------------- */
diff --git a/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderMetric.java b/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java
similarity index 80%
rename from services/credentials/java/com/android/server/credentials/metrics/ChosenProviderMetric.java
rename to services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java
index 1a61091..32fe204 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java
@@ -21,16 +21,22 @@
import com.android.server.credentials.MetricUtilities;
/**
- * The central chosen provider metric object that mimics our defined metric setup.
+ * The central chosen provider metric object that mimics our defined metric setup. This is used
+ * in the final phase of the flow and emits final status metrics.
* Some types are redundant across these metric collectors, but that has debug use-cases as
* these data-types are available at different moments of the flow (and typically, one can feed
* into the next).
* TODO(b/270403549) - iterate on this in V3+
+ * TODO(Immediately) - finalize V3 only types
*/
-public class ChosenProviderMetric {
+public class ChosenProviderFinalPhaseMetric {
- // TODO(b/270403549) - applies elsewhere, likely removed or replaced with a count-index (1,2,3)
- private static final String TAG = "ChosenProviderMetric";
+ // TODO(b/270403549) - applies elsewhere, likely removed or replaced w/ some hashed/count index
+ private static final String TAG = "ChosenFinalPhaseMetric";
+ // The session id associated with this API call, used to unite split emits
+ private long mSessionId = -1;
+ // Reveals if the UI was returned, false by default
+ private boolean mUiReturned = false;
private int mChosenUid = -1;
// Latency figures typically fed in from prior CandidateProviderMetric
@@ -39,16 +45,29 @@
private int mQueryPhaseLatencyMicroseconds = -1;
// Timestamps kept in raw nanoseconds. Expected to be converted to microseconds from using
- // reference 'mServiceBeganTimeNanoseconds' during metric log point.
+ // reference 'mServiceBeganTimeNanoseconds' during metric log point
+ // Kept for local reference purposes, the initial timestamp of the service called passed in
private long mServiceBeganTimeNanoseconds = -1;
+ // The first query timestamp, which upon emit is normalized to microseconds using the reference
+ // start timestamp
private long mQueryStartTimeNanoseconds = -1;
+ // The UI call timestamp, which upon emit will be normalized to microseconds using reference
private long mUiCallStartTimeNanoseconds = -1;
+ // The UI return timestamp, which upon emit will be normalized to microseconds using reference
private long mUiCallEndTimeNanoseconds = -1;
+ // The final finish timestamp, which upon emit will be normalized to microseconds with reference
private long mFinalFinishTimeNanoseconds = -1;
- private int mChosenProviderStatus = -1;
+ // The status of this provider after selection
- public ChosenProviderMetric() {
+ // Other General Information, such as final api status, provider status, entry info, etc...
+
+ private int mChosenProviderStatus = -1;
+ // TODO add remaining properties based on the Atom ; specifically, migrate the candidate
+ // Entry information, and store final status here
+
+
+ public ChosenProviderFinalPhaseMetric() {
}
/* ------------------- UID ------------------- */
@@ -200,4 +219,24 @@
public void setChosenProviderStatus(int chosenProviderStatus) {
mChosenProviderStatus = chosenProviderStatus;
}
+
+ /* ----------- Session ID -------------- */
+
+ public void setSessionId(long sessionId) {
+ mSessionId = sessionId;
+ }
+
+ public long getSessionId() {
+ return mSessionId;
+ }
+
+ /* ----------- UI Returned Successfully -------------- */
+
+ public void setUiReturned(boolean uiReturned) {
+ mUiReturned = uiReturned;
+ }
+
+ public boolean isUiReturned() {
+ return mUiReturned;
+ }
}
diff --git a/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java b/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java
new file mode 100644
index 0000000..73403a6
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.credentials.metrics;
+
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_CLEAR_CREDENTIAL;
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_CREATE_CREDENTIAL;
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_GET_CREDENTIAL;
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_IS_ENABLED_CREDENTIAL_PROVIDER_SERVICE;
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_UNKNOWN;
+import static com.android.server.credentials.ProviderGetSession.ACTION_ENTRY_KEY;
+import static com.android.server.credentials.ProviderGetSession.AUTHENTICATION_ACTION_ENTRY_KEY;
+import static com.android.server.credentials.ProviderGetSession.CREDENTIAL_ENTRY_KEY;
+import static com.android.server.credentials.ProviderGetSession.REMOTE_ENTRY_KEY;
+
+import android.util.Log;
+
+import java.util.AbstractMap;
+import java.util.Map;
+
+public enum EntryEnum {
+ // TODO immediately, update with built entries
+ UNKNOWN(CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_UNKNOWN),
+ ACTION_ENTRY(CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_GET_CREDENTIAL),
+ CREDENTIAL_ENTRY(CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_CREATE_CREDENTIAL),
+ REMOTE_ENTRY(CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_CLEAR_CREDENTIAL),
+ AUTHENTICATION_ENTRY(
+ CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_IS_ENABLED_CREDENTIAL_PROVIDER_SERVICE
+ );
+
+ private static final String TAG = "EntryEnum";
+
+ private final int mInnerMetricCode;
+
+ private static final Map<String, Integer> sKeyToEntryCode = Map.ofEntries(
+ new AbstractMap.SimpleEntry<>(ACTION_ENTRY_KEY,
+ ACTION_ENTRY.mInnerMetricCode),
+ new AbstractMap.SimpleEntry<>(AUTHENTICATION_ACTION_ENTRY_KEY,
+ AUTHENTICATION_ENTRY.mInnerMetricCode),
+ new AbstractMap.SimpleEntry<>(REMOTE_ENTRY_KEY,
+ REMOTE_ENTRY.mInnerMetricCode),
+ new AbstractMap.SimpleEntry<>(CREDENTIAL_ENTRY_KEY,
+ CREDENTIAL_ENTRY.mInnerMetricCode)
+ );
+
+ EntryEnum(int innerMetricCode) {
+ this.mInnerMetricCode = innerMetricCode;
+ }
+
+ /**
+ * Gives the West-world version of the metric name.
+ *
+ * @return a code corresponding to the west world metric name
+ */
+ public int getMetricCode() {
+ return this.mInnerMetricCode;
+ }
+
+ /**
+ * Given a string key type known to the framework, this returns the known metric code associated
+ * with that string.
+ *
+ * @param stringKey a string key type for a particular entry
+ * @return the metric code associated with this enum
+ */
+ public static int getMetricCodeFromString(String stringKey) {
+ if (!sKeyToEntryCode.containsKey(stringKey)) {
+ Log.w(TAG, "Attempted to use an unsupported string key entry type");
+ return UNKNOWN.mInnerMetricCode;
+ }
+ return sKeyToEntryCode.get(stringKey);
+ }
+}
diff --git a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
index 31c6f6f..a73495f 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
@@ -24,16 +24,14 @@
* TODO(b/270403549) - iterate on this in V3+
*/
public class InitialPhaseMetric {
- private static final String TAG = "PreCandidateMetric";
- // A sequence id to order united emits, due to split, this will statically always be 1
- public static final int SEQUENCE_ID = 1;
+ private static final String TAG = "InitialPhaseMetric";
// The api being called, default set to unknown
private int mApiName = ApiName.UNKNOWN.getMetricCode();
// The caller uid of the calling application, default to -1
private int mCallerUid = -1;
// The session id to unite multiple atom emits, default to -1
- private long mSessionId = -1;
+ private int mSessionId = -1;
private int mCountRequestClassType = -1;
// Raw timestamps in nanoseconds, *the only* one logged as such (i.e. 64 bits) since it is a
@@ -100,15 +98,14 @@
/* ------ SessionId ------ */
- public void setSessionId(long sessionId) {
+ public void setSessionId(int sessionId) {
mSessionId = sessionId;
}
- public long getSessionId() {
+ public int getSessionId() {
return mSessionId;
}
-
/* ------ Count Request Class Types ------ */
public void setCountRequestClassType(int countRequestClassType) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
index ec101cd..7eeb51c 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
@@ -16,10 +16,11 @@
package com.android.server.devicepolicy;
-import static android.app.admin.PolicyUpdateResult.RESULT_FAILURE_CONFLICTING_ADMIN_POLICY;
-import static android.app.admin.PolicyUpdateResult.RESULT_POLICY_CLEARED;
import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_TARGET_USER_ID;
import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_UPDATE_RESULT_KEY;
+import static android.app.admin.PolicyUpdateResult.RESULT_FAILURE_CONFLICTING_ADMIN_POLICY;
+import static android.app.admin.PolicyUpdateResult.RESULT_FAILURE_HARDWARE_LIMITATION;
+import static android.app.admin.PolicyUpdateResult.RESULT_POLICY_CLEARED;
import static android.app.admin.PolicyUpdateResult.RESULT_POLICY_SET;
import static android.content.pm.UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT;
import static android.provider.DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER;
@@ -27,6 +28,7 @@
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.admin.DevicePolicyIdentifiers;
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyState;
import android.app.admin.PolicyKey;
@@ -46,6 +48,7 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.DeviceConfig;
+import android.telephony.TelephonyManager;
import android.util.AtomicFile;
import android.util.Log;
import android.util.SparseArray;
@@ -79,6 +82,10 @@
final class DevicePolicyEngine {
static final String TAG = "DevicePolicyEngine";
+ private static final String CELLULAR_2G_USER_RESTRICTION_ID =
+ DevicePolicyIdentifiers.getIdentifierForUserRestriction(
+ UserManager.DISALLOW_CELLULAR_2G);
+
private static final String ENABLE_COEXISTENCE_FLAG = "enable_coexistence";
private static final boolean DEFAULT_ENABLE_COEXISTENCE_FLAG = true;
@@ -380,6 +387,15 @@
Objects.requireNonNull(value);
synchronized (mLock) {
+ // TODO(b/270999567): Move error handling for DISALLOW_CELLULAR_2G into the code
+ // that honors the restriction once there's an API available
+ if (checkFor2gFailure(policyDefinition, enforcingAdmin)) {
+ Log.i(TAG,
+ "Device does not support capabilities required to disable 2g. Not setting"
+ + " global policy state.");
+ return;
+ }
+
PolicyState<V> globalPolicyState = getGlobalPolicyStateLocked(policyDefinition);
boolean policyChanged = globalPolicyState.addPolicy(enforcingAdmin, value);
@@ -1162,6 +1178,36 @@
DEFAULT_ENABLE_COEXISTENCE_FLAG);
}
+ private <V> boolean checkFor2gFailure(@NonNull PolicyDefinition<V> policyDefinition,
+ @NonNull EnforcingAdmin enforcingAdmin) {
+ if (!policyDefinition.getPolicyKey().getIdentifier().equals(
+ CELLULAR_2G_USER_RESTRICTION_ID)) {
+ return false;
+ }
+
+ boolean isCapabilitySupported;
+ try {
+ isCapabilitySupported = mContext.getSystemService(
+ TelephonyManager.class).isRadioInterfaceCapabilitySupported(
+ TelephonyManager.CAPABILITY_USES_ALLOWED_NETWORK_TYPES_BITMASK);
+ } catch (IllegalStateException e) {
+ // isRadioInterfaceCapabilitySupported can throw if there is no Telephony
+ // service initialized.
+ isCapabilitySupported = false;
+ }
+
+ if (!isCapabilitySupported) {
+ sendPolicyResultToAdmin(
+ enforcingAdmin,
+ policyDefinition,
+ RESULT_FAILURE_HARDWARE_LIMITATION,
+ UserHandle.USER_ALL);
+ return true;
+ }
+
+ return false;
+ }
+
private class DevicePoliciesReaderWriter {
private static final String DEVICE_POLICIES_XML = "device_policy_state.xml";
private static final String TAG_LOCAL_POLICY_ENTRY = "local-policy-entry";
@@ -1316,8 +1362,8 @@
if (adminsPolicy != null) {
mLocalPolicies.get(userId).put(policyKey, adminsPolicy);
} else {
- Log.e(TAG, "Error parsing file, " + policyKey + "doesn't have an "
- + "AdminsPolicy.");
+ Log.e(TAG,
+ "Error parsing file, " + policyKey + "doesn't have an " + "AdminsPolicy.");
}
}
@@ -1328,8 +1374,8 @@
if (adminsPolicy != null) {
mGlobalPolicies.put(policyKey, adminsPolicy);
} else {
- Log.e(TAG, "Error parsing file, " + policyKey + "doesn't have an "
- + "AdminsPolicy.");
+ Log.e(TAG,
+ "Error parsing file, " + policyKey + "doesn't have an " + "AdminsPolicy.");
}
}
diff --git a/services/java/com/android/server/HsumBootUserInitializer.java b/services/java/com/android/server/HsumBootUserInitializer.java
index 50113fe..b895812 100644
--- a/services/java/com/android/server/HsumBootUserInitializer.java
+++ b/services/java/com/android/server/HsumBootUserInitializer.java
@@ -18,6 +18,7 @@
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ContentResolver;
+import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.os.Handler;
@@ -27,6 +28,7 @@
import android.provider.Settings;
import com.android.server.am.ActivityManagerService;
+import com.android.server.pm.PackageManagerService;
import com.android.server.pm.UserManagerInternal;
import com.android.server.utils.Slogf;
import com.android.server.utils.TimingsTraceAndSlog;
@@ -41,6 +43,7 @@
private final UserManagerInternal mUmi;
private final ActivityManagerService mAms;
+ private final PackageManagerService mPms;
private final ContentResolver mContentResolver;
private final ContentObserver mDeviceProvisionedObserver =
@@ -63,20 +66,23 @@
/** Static factory method for creating a {@link HsumBootUserInitializer} instance. */
public static @Nullable HsumBootUserInitializer createInstance(ActivityManagerService am,
- ContentResolver contentResolver, boolean shouldAlwaysHaveMainUser) {
+ PackageManagerService pms, ContentResolver contentResolver,
+ boolean shouldAlwaysHaveMainUser) {
if (!UserManager.isHeadlessSystemUserMode()) {
return null;
}
return new HsumBootUserInitializer(
LocalServices.getService(UserManagerInternal.class),
- am, contentResolver, shouldAlwaysHaveMainUser);
+ am, pms, contentResolver, shouldAlwaysHaveMainUser);
}
private HsumBootUserInitializer(UserManagerInternal umi, ActivityManagerService am,
- ContentResolver contentResolver, boolean shouldAlwaysHaveMainUser) {
+ PackageManagerService pms, ContentResolver contentResolver,
+ boolean shouldAlwaysHaveMainUser) {
mUmi = umi;
mAms = am;
+ mPms = pms;
mContentResolver = contentResolver;
mShouldAlwaysHaveMainUser = shouldAlwaysHaveMainUser;
}
@@ -131,7 +137,8 @@
try {
t.traceBegin("getBootUser");
- final int bootUser = mUmi.getBootUser();
+ final int bootUser = mUmi.getBootUser(/* waitUntilSet= */ mPms
+ .hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, /* version= */0));
t.traceEnd();
t.traceBegin("switchToBootUser-" + bootUser);
switchToBootUser(bootUser);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 4c31645..b933508 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1793,6 +1793,10 @@
}
t.traceEnd();
+ t.traceBegin("StartAppHibernationService");
+ mSystemServiceManager.startService(APP_HIBERNATION_SERVICE_CLASS);
+ t.traceEnd();
+
t.traceBegin("ArtManagerLocal");
DexOptHelper.initializeArtManagerLocal(context, mPackageManagerService);
t.traceEnd();
@@ -2316,10 +2320,6 @@
mSystemServiceManager.startService(VOICE_RECOGNITION_MANAGER_SERVICE_CLASS);
t.traceEnd();
- t.traceBegin("StartAppHibernationService");
- mSystemServiceManager.startService(APP_HIBERNATION_SERVICE_CLASS);
- t.traceEnd();
-
if (GestureLauncherService.isGestureLauncherEnabled(context.getResources())) {
t.traceBegin("StartGestureLauncher");
mSystemServiceManager.startService(GestureLauncherService.class);
@@ -2721,7 +2721,7 @@
// on it in their setup, but likely needs to be done after LockSettingsService is ready.
final HsumBootUserInitializer hsumBootUserInitializer =
HsumBootUserInitializer.createInstance(
- mActivityManagerService, mContentResolver,
+ mActivityManagerService, mPackageManagerService, mContentResolver,
context.getResources().getBoolean(R.bool.config_isMainUserPermanentAdmin));
if (hsumBootUserInitializer != null) {
t.traceBegin("HsumBootUserInitializer.init");
@@ -3021,8 +3021,7 @@
mSystemServiceManager.startBootPhase(t, SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
t.traceEnd();
- if (hsumBootUserInitializer != null && !isAutomotive) {
- // TODO(b/261924826): remove isAutomotive check once the workflow is finalized
+ if (hsumBootUserInitializer != null) {
t.traceBegin("HsumBootUserInitializer.systemRunning");
hsumBootUserInitializer.systemRunning(t);
t.traceEnd();
diff --git a/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/Android.bp b/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/Android.bp
index 2894395..24e380c 100644
--- a/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/Android.bp
+++ b/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/Android.bp
@@ -32,9 +32,9 @@
}
android_test_helper_app {
- name: "FrameworksServicesTests_install_uses_sdk_r1000",
+ name: "FrameworksServicesTests_install_uses_sdk_r10000",
defaults: ["FrameworksServicesTests_apks_defaults"],
- manifest: "AndroidManifest-r1000.xml",
+ manifest: "AndroidManifest-r10000.xml",
}
android_test_helper_app {
@@ -44,9 +44,9 @@
}
android_test_helper_app {
- name: "FrameworksServicesTests_install_uses_sdk_r0_s1000",
+ name: "FrameworksServicesTests_install_uses_sdk_r0_s10000",
defaults: ["FrameworksServicesTests_apks_defaults"],
- manifest: "AndroidManifest-r0-s1000.xml",
+ manifest: "AndroidManifest-r0-s10000.xml",
}
android_test_helper_app {
diff --git a/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r0-s1000.xml b/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r0-s10000.xml
similarity index 97%
rename from services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r0-s1000.xml
rename to services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r0-s10000.xml
index 25743b8..383e60a 100644
--- a/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r0-s1000.xml
+++ b/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r0-s10000.xml
@@ -19,7 +19,7 @@
<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="29">
<!-- This fails because 31 is not version 5 -->
<extension-sdk android:sdkVersion="30" android:minExtensionVersion="0" />
- <extension-sdk android:sdkVersion="31" android:minExtensionVersion="1000" />
+ <extension-sdk android:sdkVersion="31" android:minExtensionVersion="10000" />
</uses-sdk>
<application>
diff --git a/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r1000.xml b/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r10000.xml
similarity index 97%
rename from services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r1000.xml
rename to services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r10000.xml
index 9bf9254..fe7a212 100644
--- a/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r1000.xml
+++ b/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r10000.xml
@@ -18,7 +18,7 @@
<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="29">
<!-- This will fail to install, because minExtensionVersion is not met -->
- <extension-sdk android:sdkVersion="30" android:minExtensionVersion="1000" />
+ <extension-sdk android:sdkVersion="30" android:minExtensionVersion="10000" />
</uses-sdk>
<application>
diff --git a/services/tests/PackageManagerServiceTests/server/Android.bp b/services/tests/PackageManagerServiceTests/server/Android.bp
index e711cab..1146271 100644
--- a/services/tests/PackageManagerServiceTests/server/Android.bp
+++ b/services/tests/PackageManagerServiceTests/server/Android.bp
@@ -127,10 +127,10 @@
":FrameworksServicesTests_install_uses_sdk_q0",
":FrameworksServicesTests_install_uses_sdk_q0_r0",
":FrameworksServicesTests_install_uses_sdk_r0",
- ":FrameworksServicesTests_install_uses_sdk_r1000",
+ ":FrameworksServicesTests_install_uses_sdk_r10000",
":FrameworksServicesTests_install_uses_sdk_r_none",
":FrameworksServicesTests_install_uses_sdk_r0_s0",
- ":FrameworksServicesTests_install_uses_sdk_r0_s1000",
+ ":FrameworksServicesTests_install_uses_sdk_r0_s10000",
":FrameworksServicesTests_keyset_permdef_sa_unone",
":FrameworksServicesTests_keyset_permuse_sa_ua_ub",
":FrameworksServicesTests_keyset_permuse_sb_ua_ub",
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
index ebf309f..906cc83 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
@@ -575,10 +575,10 @@
assertEquals(0, minExtVers.get(31, -1));
Map<Pair<String, Integer>, Integer> appToError = new HashMap<>();
- appToError.put(Pair.create("install_uses_sdk.apk_r1000", R.raw.install_uses_sdk_r1000),
+ appToError.put(Pair.create("install_uses_sdk.apk_r10000", R.raw.install_uses_sdk_r10000),
PackageManager.INSTALL_FAILED_OLDER_SDK);
appToError.put(
- Pair.create("install_uses_sdk.apk_r0_s1000", R.raw.install_uses_sdk_r0_s1000),
+ Pair.create("install_uses_sdk.apk_r0_s10000", R.raw.install_uses_sdk_r0_s10000),
PackageManager.INSTALL_FAILED_OLDER_SDK);
appToError.put(Pair.create("install_uses_sdk.apk_q0", R.raw.install_uses_sdk_q0),
diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
index 83441bf..1a75170 100644
--- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
@@ -68,6 +68,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
/**
* Test RescueParty.
@@ -94,6 +95,9 @@
"persist.device_config.configuration.disable_rescue_party";
private static final String PROP_DISABLE_FACTORY_RESET_FLAG =
"persist.device_config.configuration.disable_rescue_party_factory_reset";
+ private static final String PROP_LAST_FACTORY_RESET_TIME_MS = "persist.sys.last_factory_reset";
+
+ private static final int THROTTLING_DURATION_MIN = 10;
private MockitoSession mSession;
private HashMap<String, String> mSystemSettingsMap;
@@ -459,6 +463,53 @@
}
@Test
+ public void testThrottlingOnBootFailures() {
+ SystemProperties.set(RescueParty.PROP_ATTEMPTING_REBOOT, Boolean.toString(false));
+ long now = System.currentTimeMillis();
+ long beforeTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN - 1);
+ SystemProperties.set(PROP_LAST_FACTORY_RESET_TIME_MS, Long.toString(beforeTimeout));
+ for (int i = 1; i <= LEVEL_FACTORY_RESET; i++) {
+ noteBoot(i);
+ }
+ assertFalse(RescueParty.isAttemptingFactoryReset());
+ }
+
+ @Test
+ public void testThrottlingOnAppCrash() {
+ SystemProperties.set(RescueParty.PROP_ATTEMPTING_REBOOT, Boolean.toString(false));
+ long now = System.currentTimeMillis();
+ long beforeTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN - 1);
+ SystemProperties.set(PROP_LAST_FACTORY_RESET_TIME_MS, Long.toString(beforeTimeout));
+ for (int i = 0; i <= LEVEL_FACTORY_RESET; i++) {
+ noteAppCrash(i + 1, true);
+ }
+ assertFalse(RescueParty.isAttemptingFactoryReset());
+ }
+
+ @Test
+ public void testNotThrottlingAfterTimeoutOnBootFailures() {
+ SystemProperties.set(RescueParty.PROP_ATTEMPTING_REBOOT, Boolean.toString(false));
+ long now = System.currentTimeMillis();
+ long afterTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN + 1);
+ SystemProperties.set(PROP_LAST_FACTORY_RESET_TIME_MS, Long.toString(afterTimeout));
+ for (int i = 1; i <= LEVEL_FACTORY_RESET; i++) {
+ noteBoot(i);
+ }
+ assertTrue(RescueParty.isAttemptingFactoryReset());
+ }
+ @Test
+ public void testNotThrottlingAfterTimeoutOnAppCrash() {
+ SystemProperties.set(RescueParty.PROP_ATTEMPTING_REBOOT, Boolean.toString(false));
+ long now = System.currentTimeMillis();
+ long afterTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN + 1);
+ SystemProperties.set(PROP_LAST_FACTORY_RESET_TIME_MS, Long.toString(afterTimeout));
+ for (int i = 0; i <= LEVEL_FACTORY_RESET; i++) {
+ noteAppCrash(i + 1, true);
+ }
+ assertTrue(RescueParty.isAttemptingFactoryReset());
+ }
+
+ @Test
public void testNativeRescuePartyResets() {
doReturn(true).when(() -> SettingsToPropertiesMapper.isNativeFlagsResetPerformed());
doReturn(FAKE_RESET_NATIVE_NAMESPACES).when(
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java
index 9263bff..d56229c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java
@@ -989,9 +989,16 @@
private ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, Integer definingUid,
int connectionGroup, int procState, long pss, long rss,
String processName, String packageName) {
+ return makeProcessRecord(pid, uid, packageUid, definingUid, connectionGroup,
+ procState, pss, rss, processName, packageName, mAms);
+ }
+
+ static ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, Integer definingUid,
+ int connectionGroup, int procState, long pss, long rss,
+ String processName, String packageName, ActivityManagerService ams) {
ApplicationInfo ai = new ApplicationInfo();
ai.packageName = packageName;
- ProcessRecord app = new ProcessRecord(mAms, ai, processName, uid);
+ ProcessRecord app = new ProcessRecord(ams, ai, processName, uid);
app.setPid(pid);
app.info.uid = packageUid;
if (definingUid != null) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceTimeoutTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceTimeoutTest.java
new file mode 100644
index 0000000..fd1b068
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceTimeoutTest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static android.app.ActivityManager.PROCESS_STATE_SERVICE;
+
+import static com.android.server.am.ApplicationExitInfoTest.makeProcessRecord;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.IApplicationThread;
+import android.app.usage.UsageStatsManagerInternal;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManagerInternal;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.DropBoxManagerInternal;
+import com.android.server.LocalServices;
+import com.android.server.am.ActivityManagerService.Injector;
+import com.android.server.am.ApplicationExitInfoTest.ServiceThreadRule;
+import com.android.server.appop.AppOpsService;
+import com.android.server.wm.ActivityTaskManagerService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+
+/**
+ * Test class for the service timeout.
+ *
+ * Build/Install/Run:
+ * atest ServiceTimeoutTest
+ */
+@Presubmit
+public final class ServiceTimeoutTest {
+ private static final String TAG = ServiceTimeoutTest.class.getSimpleName();
+ private static final long DEFAULT_SERVICE_TIMEOUT = 2000;
+
+ @Rule
+ public final ServiceThreadRule mServiceThreadRule = new ServiceThreadRule();
+ private Context mContext;
+ private HandlerThread mHandlerThread;
+
+ @Mock
+ private AppOpsService mAppOpsService;
+ @Mock
+ private DropBoxManagerInternal mDropBoxManagerInt;
+ @Mock
+ private PackageManagerInternal mPackageManagerInt;
+ @Mock
+ private UsageStatsManagerInternal mUsageStatsManagerInt;
+
+ private ActivityManagerService mAms;
+ private ProcessList mProcessList;
+ private ActiveServices mActiveServices;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+ mProcessList = spy(new ProcessList());
+
+ LocalServices.removeServiceForTest(DropBoxManagerInternal.class);
+ LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt);
+ LocalServices.removeServiceForTest(PackageManagerInternal.class);
+ LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
+ doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
+
+ final ActivityManagerService realAms = new ActivityManagerService(
+ new TestInjector(mContext), mServiceThreadRule.getThread());
+ realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
+ realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
+ realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal());
+ realAms.mOomAdjuster.mCachedAppOptimizer = spy(realAms.mOomAdjuster.mCachedAppOptimizer);
+ realAms.mPackageManagerInt = mPackageManagerInt;
+ realAms.mUsageStatsService = mUsageStatsManagerInt;
+ realAms.mProcessesReady = true;
+ realAms.mConstants.SERVICE_TIMEOUT = DEFAULT_SERVICE_TIMEOUT;
+ realAms.mConstants.SERVICE_BACKGROUND_TIMEOUT = DEFAULT_SERVICE_TIMEOUT;
+ mAms = spy(realAms);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ LocalServices.removeServiceForTest(DropBoxManagerInternal.class);
+ LocalServices.removeServiceForTest(PackageManagerInternal.class);
+ mHandlerThread.quit();
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ public void testServiceTimeoutAndProcessKill() throws Exception {
+ final int pid = 12345;
+ final int uid = 10123;
+ final String name = "com.example.foo";
+ final ProcessRecord app = makeProcessRecord(
+ pid, // pid
+ uid, // uid
+ uid, // packageUid
+ null, // definingUid
+ 0, // connectionGroup
+ PROCESS_STATE_SERVICE, // procstate
+ 0, // pss
+ 0, // rss
+ name, // processName
+ name, // packageName
+ mAms);
+ app.makeActive(mock(IApplicationThread.class), mAms.mProcessStats);
+ mProcessList.updateLruProcessLocked(app, false, null);
+
+ final long now = SystemClock.uptimeMillis();
+ final ServiceRecord sr = spy(ServiceRecord.newEmptyInstanceForTest(mAms));
+ doNothing().when(sr).dump(any(), anyString());
+ sr.startRequested = true;
+ sr.executingStart = now;
+
+ app.mServices.startExecutingService(sr);
+ mActiveServices.scheduleServiceTimeoutLocked(app);
+
+ verify(mActiveServices, timeout(DEFAULT_SERVICE_TIMEOUT * 2).times(1))
+ .serviceTimeout(eq(app));
+
+ clearInvocations(mActiveServices);
+
+ app.mServices.startExecutingService(sr);
+ mActiveServices.scheduleServiceTimeoutLocked(app);
+
+ app.killLocked(TAG, 42, false);
+ mAms.removeLruProcessLocked(app);
+
+ verify(mActiveServices, after(DEFAULT_SERVICE_TIMEOUT * 4)
+ .times(1)).serviceTimeout(eq(app));
+ }
+
+ private class TestInjector extends Injector {
+ TestInjector(Context context) {
+ super(context);
+ }
+
+ @Override
+ public AppOpsService getAppOpsService(File recentAccessesFile, File storageFile,
+ Handler handler) {
+ return mAppOpsService;
+ }
+
+ @Override
+ public Handler getUiHandler(ActivityManagerService service) {
+ return mHandlerThread.getThreadHandler();
+ }
+
+ @Override
+ public ProcessList getProcessList(ActivityManagerService service) {
+ return mProcessList;
+ }
+
+ @Override
+ public ActiveServices getActiveServices(ActivityManagerService service) {
+ if (mActiveServices == null) {
+ mActiveServices = spy(new ActiveServices(service));
+ }
+ return mActiveServices;
+ }
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 564893c..e7b3e6f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -260,7 +260,7 @@
mUms.setBootUser(OTHER_USER_ID);
assertWithMessage("getBootUser")
- .that(mUmi.getBootUser()).isEqualTo(OTHER_USER_ID);
+ .that(mUmi.getBootUser(/* waitUntilSet= */ false)).isEqualTo(OTHER_USER_ID);
}
@Test
@@ -273,7 +273,8 @@
mUms.setBootUser(PROFILE_USER_ID);
assertWithMessage("getBootUser")
- .that(mUmi.getBootUser()).isEqualTo(UserHandle.USER_SYSTEM);
+ .that(mUmi.getBootUser(/* waitUntilSet= */ false))
+ .isEqualTo(UserHandle.USER_SYSTEM);
}
@Test
@@ -289,7 +290,7 @@
// Boot user not switchable so return most recently in foreground.
assertWithMessage("getBootUser")
- .that(mUmi.getBootUser()).isEqualTo(OTHER_USER_ID);
+ .that(mUmi.getBootUser(/* waitUntilSet= */ false)).isEqualTo(OTHER_USER_ID);
}
@Test
@@ -299,7 +300,8 @@
addUser(OTHER_USER_ID);
assertWithMessage("getBootUser")
- .that(mUmi.getBootUser()).isEqualTo(UserHandle.USER_SYSTEM);
+ .that(mUmi.getBootUser(/* waitUntilSet= */ false))
+ .isEqualTo(UserHandle.USER_SYSTEM);
}
@Test
@@ -312,14 +314,15 @@
setLastForegroundTime(OTHER_USER_ID, 2_000_000L);
assertWithMessage("getBootUser")
- .that(mUmi.getBootUser()).isEqualTo(OTHER_USER_ID);
+ .that(mUmi.getBootUser(/* waitUntilSet= */ false)).isEqualTo(OTHER_USER_ID);
}
@Test
public void testGetBootUser_Headless_ThrowsIfOnlySystemUserExists() throws Exception {
setSystemUserHeadless(true);
- assertThrows(UserManager.CheckedUserOperationException.class, () -> mUmi.getBootUser());
+ assertThrows(UserManager.CheckedUserOperationException.class,
+ () -> mUmi.getBootUser(/* waitUntilSet= */ false));
}
private void mockCurrentUser(@UserIdInt int userId) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
index 239b6fd..70b5ac0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
@@ -596,6 +596,7 @@
.that(mMediator.assignUserToExtraDisplay(userId, displayId))
.isTrue();
expectUserIsVisibleOnDisplay(userId, displayId);
+ expectDisplaysAssignedToUserContainsDisplayId(userId, displayId);
if (unassign) {
Log.d(TAG, "Calling unassignUserFromExtraDisplay(" + userId + ", " + displayId + ")");
@@ -603,6 +604,7 @@
.that(mMediator.unassignUserFromExtraDisplay(userId, displayId))
.isTrue();
expectUserIsNotVisibleOnDisplay(userId, displayId);
+ expectDisplaysAssignedToUserDoesNotContainDisplayId(userId, displayId);
}
}
@@ -668,6 +670,7 @@
expectUserIsNotVisibleOnDisplay(userId, INVALID_DISPLAY);
expectUserIsNotVisibleOnDisplay(userId, SECONDARY_DISPLAY_ID);
expectUserIsNotVisibleOnDisplay(userId, OTHER_SECONDARY_DISPLAY_ID);
+ expectDisplaysAssignedToUserIsEmpty(userId);
}
protected void expectDisplayAssignedToUser(@UserIdInt int userId, int displayId) {
@@ -680,6 +683,24 @@
.that(mMediator.getDisplayAssignedToUser(userId)).isEqualTo(INVALID_DISPLAY);
}
+ protected void expectDisplaysAssignedToUserContainsDisplayId(
+ @UserIdInt int userId, int displayId) {
+ expectWithMessage("getDisplaysAssignedToUser(%s)", userId)
+ .that(mMediator.getDisplaysAssignedToUser(userId)).asList().contains(displayId);
+ }
+
+ protected void expectDisplaysAssignedToUserDoesNotContainDisplayId(
+ @UserIdInt int userId, int displayId) {
+ expectWithMessage("getDisplaysAssignedToUser(%s)", userId)
+ .that(mMediator.getDisplaysAssignedToUser(userId)).asList()
+ .doesNotContain(displayId);
+ }
+
+ protected void expectDisplaysAssignedToUserIsEmpty(@UserIdInt int userId) {
+ expectWithMessage("getDisplaysAssignedToUser(%s)", userId)
+ .that(mMediator.getDisplaysAssignedToUser(userId)).isNull();
+ }
+
protected void expectUserCannotBeUnassignedFromDisplay(@UserIdInt int userId, int displayId) {
expectWithMessage("unassignUserFromExtraDisplay(%s, %s)", userId, displayId)
.that(mMediator.unassignUserFromExtraDisplay(userId, displayId)).isFalse();
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index 7642e7b..6c6b608 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -28,7 +28,7 @@
import android.hardware.display.DisplayManagerInternal;
import android.hardware.input.IInputManager;
-import android.hardware.input.InputManager;
+import android.hardware.input.InputManagerGlobal;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
@@ -109,7 +109,7 @@
device1Id).isNotEqualTo(device2Id);
- int[] deviceIds = InputManager.getInstance().getInputDeviceIds();
+ int[] deviceIds = InputManagerGlobal.getInstance().getInputDeviceIds();
assertWithMessage("InputManager's deviceIds list should contain id of device 1").that(
deviceIds).asList().contains(device1Id);
assertWithMessage("InputManager's deviceIds list should contain id of device 2").that(
@@ -153,7 +153,7 @@
deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50);
int deviceId = mInputController.getInputDeviceId(deviceToken);
- int[] deviceIds = InputManager.getInstance().getInputDeviceIds();
+ int[] deviceIds = InputManagerGlobal.getInstance().getInputDeviceIds();
assertWithMessage("InputManager's deviceIds list should contain id of the device").that(
deviceIds).asList().contains(deviceId);
diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamOverlayServiceTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamOverlayServiceTest.java
index 6c73f71..851d8f9 100644
--- a/services/tests/servicestests/src/com/android/server/dreams/DreamOverlayServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/dreams/DreamOverlayServiceTest.java
@@ -18,6 +18,8 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -39,10 +41,13 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.concurrent.Executor;
+
/**
* A collection of tests to exercise {@link DreamOverlayService}.
*/
@@ -60,6 +65,9 @@
@Mock
IDreamOverlayCallback mOverlayCallback;
+ @Mock
+ Executor mExecutor;
+
/**
* {@link TestDreamOverlayService} is a simple {@link DreamOverlayService} implementation for
* tracking interactions across {@link IDreamOverlay} binder interface. The service reports
@@ -78,8 +86,8 @@
private final Monitor mMonitor;
- TestDreamOverlayService(Monitor monitor) {
- super();
+ TestDreamOverlayService(Monitor monitor, Executor executor) {
+ super(executor);
mMonitor = monitor;
}
@@ -118,13 +126,63 @@
}
/**
+ * Verifies that callbacks for subclasses are run on the provided executor.
+ */
+ @Test
+ public void testCallbacksRunOnExecutor() throws RemoteException {
+ final TestDreamOverlayService.Monitor monitor = Mockito.mock(
+ TestDreamOverlayService.Monitor.class);
+ final TestDreamOverlayService service = new TestDreamOverlayService(monitor, mExecutor);
+ final IBinder binder = service.onBind(new Intent());
+ final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(binder);
+
+ final IDreamOverlayClient client = getClient(overlay);
+
+ // Start the dream.
+ client.startDream(mLayoutParams, mOverlayCallback,
+ FIRST_DREAM_COMPONENT.flattenToString(), false);
+
+ // The callback should not have run yet.
+ verify(monitor, never()).onStartDream();
+
+ // Run the Runnable sent to the executor.
+ ArgumentCaptor<Runnable> mRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mExecutor).execute(mRunnableCaptor.capture());
+ mRunnableCaptor.getValue().run();
+
+ // Callback is run.
+ verify(monitor).onStartDream();
+
+ // Verify onWakeUp is run on the executor.
+ client.wakeUp();
+ verify(monitor, never()).onWakeUp();
+ mRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mExecutor).execute(mRunnableCaptor.capture());
+ mRunnableCaptor.getValue().run();
+ verify(monitor).onWakeUp();
+
+ // Verify onEndDream is run on the executor.
+ client.endDream();
+ verify(monitor, never()).onEndDream();
+ mRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mExecutor).execute(mRunnableCaptor.capture());
+ mRunnableCaptor.getValue().run();
+ verify(monitor).onEndDream();
+ }
+
+ /**
* Verifies that only the currently started dream is able to affect the overlay.
*/
@Test
public void testOverlayClientInteraction() throws RemoteException {
+ doAnswer(invocation -> {
+ ((Runnable) invocation.getArgument(0)).run();
+ return null;
+ }).when(mExecutor).execute(any());
+
final TestDreamOverlayService.Monitor monitor = Mockito.mock(
TestDreamOverlayService.Monitor.class);
- final TestDreamOverlayService service = new TestDreamOverlayService(monitor);
+ final TestDreamOverlayService service = new TestDreamOverlayService(monitor, mExecutor);
final IBinder binder = service.onBind(new Intent());
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(binder);
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
index d9d0715..64e6236 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
@@ -74,6 +74,8 @@
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import javax.crypto.SecretKey;
@@ -324,16 +326,30 @@
mInjected = mock(MockableRebootEscrowInjected.class);
mMockInjector = new MockInjector(mContext, mUserManager, mRebootEscrow,
mKeyStoreManager, mStorage, mInjected);
- mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage);
HandlerThread thread = new HandlerThread("RebootEscrowManagerTest");
thread.start();
mHandler = new Handler(thread.getLooper());
+ mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage, mHandler);
+
}
private void setServerBasedRebootEscrowProvider() throws Exception {
mMockInjector = new MockInjector(mContext, mUserManager, mServiceConnection,
mKeyStoreManager, mStorage, mInjected);
- mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage);
+ mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage, mHandler);
+ }
+
+ private void waitForHandler() throws InterruptedException {
+ // Wait for handler to complete processing.
+ CountDownLatch latch = new CountDownLatch(1);
+ mHandler.post(latch::countDown);
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+
+ }
+
+ private void callToRebootEscrowIfNeededAndWait(int userId) throws InterruptedException {
+ mService.callToRebootEscrowIfNeeded(userId, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ waitForHandler();
}
@Test
@@ -343,7 +359,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
}
@@ -355,8 +371,7 @@
mService.setRebootEscrowListener(mockListener);
mService.prepareRebootEscrow();
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
- verify(mockListener).onPreparedForReboot(eq(true));
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
assertFalse(mStorage.hasRebootEscrowServerBlob());
}
@@ -366,7 +381,7 @@
RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
mService.setRebootEscrowListener(mockListener);
mService.prepareRebootEscrow();
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
clearInvocations(mRebootEscrow);
@@ -390,7 +405,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -414,7 +429,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -435,7 +450,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -453,10 +468,9 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
- mService.callToRebootEscrowIfNeeded(SECURE_SECONDARY_USER_ID, FAKE_SP_VERSION,
- FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(SECURE_SECONDARY_USER_ID);
verify(mRebootEscrow, never()).storeKey(any());
assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID));
@@ -488,7 +502,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -511,7 +525,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -554,7 +568,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -598,7 +612,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -643,7 +657,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -689,7 +703,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -738,7 +752,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -791,7 +805,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -846,7 +860,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -893,7 +907,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -949,7 +963,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -1008,7 +1022,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -1068,7 +1082,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -1124,7 +1138,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -1176,7 +1190,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -1207,7 +1221,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -1235,7 +1249,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -1274,7 +1288,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -1309,7 +1323,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID));
verify(mRebootEscrow, never()).storeKey(any());
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java b/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java
index 6edef75..07b4345 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java
@@ -34,6 +34,7 @@
import android.hardware.input.IInputDevicesChangedListener;
import android.hardware.input.IInputManager;
import android.hardware.input.InputManager;
+import android.hardware.input.InputManagerGlobal;
import android.os.CombinedVibration;
import android.os.Handler;
import android.os.Process;
@@ -82,8 +83,8 @@
@Before
public void setUp() throws Exception {
mTestLooper = new TestLooper();
- mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
InputManager inputManager = InputManager.resetInstance(mIInputManagerMock);
+ mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
when(mContextSpy.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager);
doAnswer(invocation -> mIInputDevicesChangedListener = invocation.getArgument(0))
@@ -314,7 +315,7 @@
deviceIdsAndGenerations[i + 1] = 2; // update by increasing it's generation to 2.
}
// Force initialization of mIInputDevicesChangedListener, if it still haven't
- InputManager.getInstance().getInputDeviceIds();
+ InputManagerGlobal.getInstance().getInputDeviceIds();
mIInputDevicesChangedListener.onInputDevicesChanged(deviceIdsAndGenerations);
// Makes sure all callbacks from InputDeviceDelegate are executed.
mTestLooper.dispatchAll();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
new file mode 100644
index 0000000..b94ed01
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.content.ComponentName;
+import android.content.ServiceConnection;
+import android.content.pm.IPackageManager;
+import android.net.Uri;
+import android.os.IInterface;
+import android.service.notification.Condition;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class ConditionProvidersTest extends UiServiceTestCase {
+
+ private ConditionProviders mProviders;
+
+ @Mock
+ private IPackageManager mIpm;
+ @Mock
+ private ManagedServices.UserProfiles mUserProfiles;
+ @Mock
+ private ConditionProviders.Callback mCallback;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mProviders = new ConditionProviders(mContext, mUserProfiles, mIpm);
+ mProviders.setCallback(mCallback);
+ }
+
+ @Test
+ public void notifyConditions_findCondition() {
+ ComponentName cn = new ComponentName("package", "cls");
+ ManagedServices.ManagedServiceInfo msi = mProviders.new ManagedServiceInfo(
+ mock(IInterface.class), cn, 0, false, mock(ServiceConnection.class), 33, 100);
+ Condition[] conditions = new Condition[] {
+ new Condition(Uri.parse("a"), "summary", Condition.STATE_TRUE),
+ new Condition(Uri.parse("b"), "summary2", Condition.STATE_TRUE)
+ };
+
+ mProviders.notifyConditions("package", msi, conditions);
+
+ assertThat(mProviders.findCondition(cn, Uri.parse("a"))).isEqualTo(conditions[0]);
+ assertThat(mProviders.findCondition(cn, Uri.parse("b"))).isEqualTo(conditions[1]);
+ assertThat(mProviders.findCondition(null, Uri.parse("a"))).isNull();
+ assertThat(mProviders.findCondition(cn, null)).isNull();
+ }
+
+ @Test
+ public void notifyConditions_callbackOnConditionChanged() {
+ ManagedServices.ManagedServiceInfo msi = mProviders.new ManagedServiceInfo(
+ mock(IInterface.class), new ComponentName("package", "cls"), 0, false,
+ mock(ServiceConnection.class), 33, 100);
+ Condition[] conditionsToNotify = new Condition[] {
+ new Condition(Uri.parse("a"), "summary", Condition.STATE_TRUE),
+ new Condition(Uri.parse("b"), "summary2", Condition.STATE_TRUE),
+ new Condition(Uri.parse("c"), "summary3", Condition.STATE_TRUE)
+ };
+
+ mProviders.notifyConditions("package", msi, conditionsToNotify);
+
+ verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0]));
+ verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[1]));
+ verify(mCallback).onConditionChanged(eq(Uri.parse("c")), eq(conditionsToNotify[2]));
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void notifyConditions_duplicateIds_ignored() {
+ ManagedServices.ManagedServiceInfo msi = mProviders.new ManagedServiceInfo(
+ mock(IInterface.class), new ComponentName("package", "cls"), 0, false,
+ mock(ServiceConnection.class), 33, 100);
+ Condition[] conditionsToNotify = new Condition[] {
+ new Condition(Uri.parse("a"), "summary", Condition.STATE_TRUE),
+ new Condition(Uri.parse("b"), "summary2", Condition.STATE_TRUE),
+ new Condition(Uri.parse("a"), "summary3", Condition.STATE_FALSE),
+ new Condition(Uri.parse("a"), "summary4", Condition.STATE_FALSE)
+ };
+
+ mProviders.notifyConditions("package", msi, conditionsToNotify);
+
+ verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0]));
+ verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[1]));
+
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void notifyConditions_nullItems_ignored() {
+ ManagedServices.ManagedServiceInfo msi = mProviders.new ManagedServiceInfo(
+ mock(IInterface.class), new ComponentName("package", "cls"), 0, false,
+ mock(ServiceConnection.class), 33, 100);
+ Condition[] conditionsToNotify = new Condition[] {
+ new Condition(Uri.parse("a"), "summary", Condition.STATE_TRUE),
+ null,
+ null,
+ new Condition(Uri.parse("b"), "summary", Condition.STATE_TRUE)
+ };
+
+ mProviders.notifyConditions("package", msi, conditionsToNotify);
+
+ verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0]));
+ verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[3]));
+ verifyNoMoreInteractions(mCallback);
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
index d72cfc7..0564a73 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
@@ -15,6 +15,8 @@
*/
package com.android.server.notification;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -51,6 +53,8 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.server.UiServiceTestCase;
+import com.android.server.notification.ValidateNotificationPeople.LookupResult;
+import com.android.server.notification.ValidateNotificationPeople.PeopleRankingReconsideration;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -60,6 +64,7 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -215,7 +220,7 @@
ContactsContract.Contacts.CONTENT_LOOKUP_URI,
ContactsContract.Contacts.ENTERPRISE_CONTACT_LOOKUP_PREFIX + contactId);
- new ValidateNotificationPeople().searchContacts(mockContext, lookupUri);
+ PeopleRankingReconsideration.searchContacts(mockContext, lookupUri);
ArgumentCaptor<Uri> queryUri = ArgumentCaptor.forClass(Uri.class);
verify(mockContentResolver).query(
@@ -242,7 +247,7 @@
final Uri lookupUri = Uri.withAppendedPath(
ContactsContract.Contacts.CONTENT_LOOKUP_URI, String.valueOf(contactId));
- new ValidateNotificationPeople().searchContacts(mockContext, lookupUri);
+ PeopleRankingReconsideration.searchContacts(mockContext, lookupUri);
ArgumentCaptor<Uri> queryUri = ArgumentCaptor.forClass(Uri.class);
verify(mockContentResolver).query(
@@ -277,7 +282,7 @@
// call searchContacts and then mergePhoneNumbers, make sure we never actually
// query the content resolver for a phone number
- new ValidateNotificationPeople().searchContactsAndLookupNumbers(mockContext, lookupUri);
+ PeopleRankingReconsideration.searchContactsAndLookupNumbers(mockContext, lookupUri);
verify(mockContentResolver, never()).query(
eq(ContactsContract.CommonDataKinds.Phone.CONTENT_URI),
eq(ValidateNotificationPeople.PHONE_LOOKUP_PROJECTION),
@@ -320,7 +325,7 @@
// call searchContacts and then mergePhoneNumbers, and check that we query
// once for the
- new ValidateNotificationPeople().searchContactsAndLookupNumbers(mockContext, lookupUri);
+ PeopleRankingReconsideration.searchContactsAndLookupNumbers(mockContext, lookupUri);
verify(mockContentResolver, times(1)).query(
eq(ContactsContract.CommonDataKinds.Phone.CONTENT_URI),
eq(ValidateNotificationPeople.PHONE_LOOKUP_PROJECTION),
@@ -339,7 +344,7 @@
// Create validator with empty cache
ValidateNotificationPeople vnp = new ValidateNotificationPeople();
- LruCache cache = new LruCache<String, ValidateNotificationPeople.LookupResult>(5);
+ LruCache<String, LookupResult> cache = new LruCache<>(5);
vnp.initForTests(mockContext, mockNotificationUsageStats, cache);
NotificationRecord record = getNotificationRecord();
@@ -366,9 +371,8 @@
float affinity = 0.7f;
// Create a fake LookupResult for the data we'll pass in
- LruCache cache = new LruCache<String, ValidateNotificationPeople.LookupResult>(5);
- ValidateNotificationPeople.LookupResult lr =
- mock(ValidateNotificationPeople.LookupResult.class);
+ LruCache<String, LookupResult> cache = new LruCache<>(5);
+ LookupResult lr = mock(LookupResult.class);
when(lr.getAffinity()).thenReturn(affinity);
when(lr.getPhoneNumbers()).thenReturn(new ArraySet<>(new String[]{lookupTel}));
when(lr.isExpired()).thenReturn(false);
@@ -392,6 +396,23 @@
assertTrue(record.getPhoneNumbers().contains(lookupTel));
}
+ @Test
+ public void validatePeople_reconsiderationWillNotBeDelayed() {
+ final Context mockContext = mock(Context.class);
+ final ContentResolver mockContentResolver = mock(ContentResolver.class);
+ when(mockContext.getContentResolver()).thenReturn(mockContentResolver);
+ ValidateNotificationPeople vnp = new ValidateNotificationPeople();
+ vnp.initForTests(mockContext, mock(NotificationUsageStats.class), new LruCache<>(5));
+ NotificationRecord record = getNotificationRecord();
+ String[] callNumber = new String[]{"tel:12345678910"};
+ setNotificationPeople(record, callNumber);
+
+ RankingReconsideration rr = vnp.validatePeople(mockContext, record);
+
+ assertThat(rr).isNotNull();
+ assertThat(rr.getDelay(TimeUnit.MILLISECONDS)).isEqualTo(0);
+ }
+
// Creates a cursor that points to one item of Contacts data with the specified
// columns.
private Cursor makeMockCursor(int id, String lookupKey, int starred, int hasPhone) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index 06b6ed8..da078a2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -459,8 +459,17 @@
mainWindow.mInvGlobalScale = invGlobalScale;
mLetterboxConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
+ doReturn(true).when(mActivity).isInLetterboxAnimation();
assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow));
+ doReturn(false).when(mActivity).isInLetterboxAnimation();
+ assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow));
+
+ doReturn(false).when(mainWindow).isOnScreen();
+ assertEquals(0, mController.getRoundedCornersRadius(mainWindow));
+
+ doReturn(true).when(mActivity).isInLetterboxAnimation();
+ assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow));
}
@Test
@@ -495,6 +504,7 @@
insets.addSource(taskbar);
}
doReturn(mLetterboxedPortraitTaskBounds).when(mActivity).getBounds();
+ doReturn(false).when(mActivity).isInLetterboxAnimation();
doReturn(true).when(mActivity).isVisible();
doReturn(true).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
doReturn(insets).when(mainWindow).getInsetsState();
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 753cc62..2cc46a9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -51,6 +51,7 @@
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
+import static com.android.server.wm.ActivityRecord.State.PAUSED;
import static com.android.server.wm.ActivityRecord.State.RESTARTING_PROCESS;
import static com.android.server.wm.ActivityRecord.State.RESUMED;
import static com.android.server.wm.ActivityRecord.State.STOPPED;
@@ -3918,6 +3919,24 @@
assertTrue(mActivity.inSizeCompatMode());
}
+ @Test
+ public void testTopActivityInSizeCompatMode_pausedAndInSizeCompatMode_returnsTrue() {
+ setUpDisplaySizeWithApp(1000, 2500);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+ spyOn(mActivity);
+ doReturn(mTask).when(mActivity).getOrganizedTask();
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+ rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+ mActivity.setState(PAUSED, "test");
+
+ assertTrue(mActivity.inSizeCompatMode());
+ assertEquals(mActivity.getState(), PAUSED);
+ assertTrue(mActivity.isVisible());
+ assertTrue(mTask.getTaskInfo().topActivityInSizeCompat);
+ }
+
/**
* Tests that all three paths in which aspect ratio logic can be applied yield the same
* result, which is that aspect ratio is respected on app bounds. The three paths are
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index debfc84..616d528 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -40,6 +40,7 @@
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static android.window.TransitionInfo.isIndependent;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -54,6 +55,7 @@
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -520,6 +522,47 @@
}
@Test
+ public void testCreateInfo_MultiDisplay() {
+ DisplayContent otherDisplay = createNewDisplay();
+ final Transition transition = createTestTransition(TRANSIT_OPEN);
+ ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
+ ArraySet<WindowContainer> participants = transition.mParticipants;
+
+ final Task display0Task = createTask(mDisplayContent);
+ final Task display1Task = createTask(otherDisplay);
+ // Start states.
+ changes.put(display0Task,
+ new Transition.ChangeInfo(display0Task, false /* vis */, true /* exChg */));
+ changes.put(display1Task,
+ new Transition.ChangeInfo(display1Task, false /* vis */, true /* exChg */));
+ fillChangeMap(changes, display0Task);
+ fillChangeMap(changes, display1Task);
+ // End states.
+ display0Task.setVisibleRequested(true);
+ display1Task.setVisibleRequested(true);
+
+ final int transit = transition.mType;
+ int flags = 0;
+
+ participants.add(display0Task);
+ participants.add(display1Task);
+ ArrayList<Transition.ChangeInfo> targets =
+ Transition.calculateTargets(participants, changes);
+ TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
+ assertEquals(2, info.getRootCount());
+ // Check that the changes are assigned to the correct display
+ assertEquals(mDisplayContent.getDisplayId(), info.getChange(
+ display0Task.mRemoteToken.toWindowContainerToken()).getEndDisplayId());
+ assertEquals(otherDisplay.getDisplayId(), info.getChange(
+ display1Task.mRemoteToken.toWindowContainerToken()).getEndDisplayId());
+ // Check that roots can be found by display and have the correct display
+ assertEquals(mDisplayContent.getDisplayId(),
+ info.getRoot(info.findRootIndex(mDisplayContent.getDisplayId())).getDisplayId());
+ assertEquals(otherDisplay.getDisplayId(),
+ info.getRoot(info.findRootIndex(otherDisplay.getDisplayId())).getDisplayId());
+ }
+
+ @Test
public void testTargets_noIntermediatesToWallpaper() {
final Transition transition = createTestTransition(TRANSIT_OPEN);
@@ -1350,7 +1393,7 @@
verify(snapshotController, times(1)).recordSnapshot(eq(task2), eq(false));
- openTransition.finishTransition();
+ controller.finishTransition(openTransition);
// We are now going to simulate closing task1 to return back to (open) task2.
final Transition closeTransition = controller.createTransition(TRANSIT_CLOSE);
@@ -1359,7 +1402,13 @@
closeTransition.collectExistenceChange(activity1);
closeTransition.collectExistenceChange(task2);
closeTransition.collectExistenceChange(activity2);
- closeTransition.setTransientLaunch(activity2, null /* restoreBelow */);
+ closeTransition.setTransientLaunch(activity2, task1);
+ final Transition.ChangeInfo task1ChangeInfo = closeTransition.mChanges.get(task1);
+ assertNotNull(task1ChangeInfo);
+ assertTrue(task1ChangeInfo.hasChanged());
+ final Transition.ChangeInfo activity1ChangeInfo = closeTransition.mChanges.get(activity1);
+ assertNotNull(activity1ChangeInfo);
+ assertTrue(activity1ChangeInfo.hasChanged());
activity1.setVisibleRequested(false);
activity2.setVisibleRequested(true);
@@ -1375,9 +1424,27 @@
verify(snapshotController, times(0)).recordSnapshot(eq(task1), eq(false));
enteringAnimReports.clear();
- closeTransition.finishTransition();
+ doCallRealMethod().when(mWm.mRoot).ensureActivitiesVisible(any(),
+ anyInt(), anyBoolean(), anyBoolean());
+ final boolean[] wasInFinishingTransition = { false };
+ controller.registerLegacyListener(new WindowManagerInternal.AppTransitionListener() {
+ @Override
+ public void onAppTransitionFinishedLocked(IBinder token) {
+ final ActivityRecord r = ActivityRecord.forToken(token);
+ if (r != null) {
+ wasInFinishingTransition[0] = controller.inFinishingTransition(r);
+ }
+ }
+ });
+ controller.finishTransition(closeTransition);
+ assertTrue(wasInFinishingTransition[0]);
+ assertNull(controller.mFinishingTransition);
+ assertTrue(activity2.isVisible());
assertEquals(ActivityTaskManagerService.APP_SWITCH_DISALLOW, mAtm.getBalAppSwitchesState());
+ // Because task1 is occluded by task2, finishTransition should make activity1 invisible.
+ assertFalse(activity1.isVisibleRequested());
+ assertFalse(activity1.isVisible());
assertFalse(activity1.app.hasActivityInVisibleTask());
verify(snapshotController, times(1)).recordSnapshot(eq(task1), eq(false));
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index 65631ea..984b868 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -396,7 +396,7 @@
final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
token.finishSync(t, false /* cancel */);
transit.onTransactionReady(transit.getSyncId(), t);
- dc.mTransitionController.finishTransition(transit.getToken());
+ dc.mTransitionController.finishTransition(transit);
assertFalse(wallpaperWindow.isVisible());
assertFalse(token.isVisible());
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index a68a573..17ad4e3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -1486,9 +1486,9 @@
assertEquals(rootTask.mTaskId, info.taskId);
assertTrue(info.topActivityInSizeCompat);
- // Ensure task info show top activity that is not in foreground as not in size compat.
+ // Ensure task info show top activity that is not visible as not in size compat.
clearInvocations(organizer);
- doReturn(false).when(activity).isState(RESUMED);
+ doReturn(false).when(activity).isVisible();
rootTask.onSizeCompatActivityChanged();
mWm.mAtmService.mTaskOrganizerController.dispatchPendingEvents();
verify(organizer).onTaskInfoChanged(infoCaptor.capture());
@@ -1498,7 +1498,7 @@
// Ensure task info show non size compat top activity as not in size compat.
clearInvocations(organizer);
- doReturn(true).when(activity).isState(RESUMED);
+ doReturn(true).when(activity).isVisible();
doReturn(false).when(activity).inSizeCompatMode();
rootTask.onSizeCompatActivityChanged();
mWm.mAtmService.mTaskOrganizerController.dispatchPendingEvents();
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerMapTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerMapTests.java
index c2ee079..2a3c9bc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerMapTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerMapTests.java
@@ -22,6 +22,8 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
@@ -129,4 +131,14 @@
assertEquals(uid2processes.size(), 1);
assertEquals(mProcessMap.getProcess(FAKE_PID1), pid1uid2);
}
+
+ @Test
+ public void testRemove_callsDestroy() {
+ var proc = spy(pid1uid1);
+ mProcessMap.put(FAKE_PID1, proc);
+
+ mProcessMap.remove(FAKE_PID1);
+
+ verify(proc).destroy();
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index d6cfd00..cf83981 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -159,6 +159,17 @@
}
@Test
+ public void testDestroy_unregistersDisplayAreaListener() {
+ final TestDisplayContent testDisplayContent1 = createTestDisplayContentInContainer();
+ final DisplayArea imeContainer1 = testDisplayContent1.getImeContainer();
+ mWpc.registerDisplayAreaConfigurationListener(imeContainer1);
+
+ mWpc.destroy();
+
+ assertNull(mWpc.getDisplayArea());
+ }
+
+ @Test
public void testSetRunningRecentsAnimation() {
mWpc.setRunningRecentsAnimation(true);
mWpc.setRunningRecentsAnimation(false);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index ce6cd90..b80500a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -1799,7 +1799,7 @@
}
public void finish() {
- mController.finishTransition(mLastTransit.getToken());
+ mController.finishTransition(mLastTransit);
}
}
}
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index bf12b9c..212dc41 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -1628,6 +1628,7 @@
* <li> 9: WiFi Calling</li>
* <li> 10: VoWifi</li>
* <li> 11: %s WiFi Calling</li>
+ * <li> 12: WiFi Call</li>
* @hide
*/
public static final String KEY_WFC_SPN_FORMAT_IDX_INT = "wfc_spn_format_idx_int";
@@ -1974,8 +1975,13 @@
/**
* Boolean indicating if LTE+ icon should be shown if available.
*/
- public static final String KEY_HIDE_LTE_PLUS_DATA_ICON_BOOL =
- "hide_lte_plus_data_icon_bool";
+ public static final String KEY_HIDE_LTE_PLUS_DATA_ICON_BOOL = "hide_lte_plus_data_icon_bool";
+
+ /**
+ * Boolean indicting if the 5G slice icon should be shown if available.
+ * @hide
+ */
+ public static final String KEY_SHOW_5G_SLICE_ICON_BOOL = "show_5g_slice_icon_bool";
/**
* The combined channel bandwidth threshold (non-inclusive) in KHz required to display the
@@ -9913,6 +9919,7 @@
sDefaults.putString(KEY_OPERATOR_NAME_FILTER_PATTERN_STRING, "");
sDefaults.putString(KEY_SHOW_CARRIER_DATA_ICON_PATTERN_STRING, "");
sDefaults.putBoolean(KEY_HIDE_LTE_PLUS_DATA_ICON_BOOL, true);
+ sDefaults.putBoolean(KEY_SHOW_5G_SLICE_ICON_BOOL, true);
sDefaults.putInt(KEY_LTE_PLUS_THRESHOLD_BANDWIDTH_KHZ_INT, 20000);
sDefaults.putInt(KEY_NR_ADVANCED_THRESHOLD_BANDWIDTH_KHZ_INT, 0);
sDefaults.putBoolean(KEY_INCLUDE_LTE_FOR_NR_ADVANCED_THRESHOLD_BANDWIDTH_BOOL, false);
diff --git a/telephony/java/android/telephony/data/QosBearerSession.java b/telephony/java/android/telephony/data/QosBearerSession.java
index dd08085..1668193 100644
--- a/telephony/java/android/telephony/data/QosBearerSession.java
+++ b/telephony/java/android/telephony/data/QosBearerSession.java
@@ -102,12 +102,11 @@
QosBearerSession other = (QosBearerSession) o;
return this.qosBearerSessionId == other.qosBearerSessionId
- && this.qos.equals(other.qos)
+ && Objects.equals(this.qos, other.qos)
&& this.qosBearerFilterList.size() == other.qosBearerFilterList.size()
&& this.qosBearerFilterList.containsAll(other.qosBearerFilterList);
}
-
public static final @NonNull Parcelable.Creator<QosBearerSession> CREATOR =
new Parcelable.Creator<QosBearerSession>() {
@Override
diff --git a/telephony/java/android/telephony/satellite/ISatelliteStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteStateCallback.aidl
index 98221c9..cd9d81e 100644
--- a/telephony/java/android/telephony/satellite/ISatelliteStateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/ISatelliteStateCallback.aidl
@@ -22,13 +22,6 @@
*/
oneway interface ISatelliteStateCallback {
/**
- * Indicates that the satellite has pending datagrams for the device to be pulled.
- *
- * @param count Number of pending datagrams.
- */
- void onPendingDatagramCount(in int count);
-
- /**
* Indicates that the satellite modem state has changed.
*
* @param state The current satellite modem state.
diff --git a/telephony/java/android/telephony/satellite/ISatellitePositionUpdateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteTransmissionUpdateCallback.aidl
similarity index 96%
rename from telephony/java/android/telephony/satellite/ISatellitePositionUpdateCallback.aidl
rename to telephony/java/android/telephony/satellite/ISatelliteTransmissionUpdateCallback.aidl
index d3f1091..2442083 100644
--- a/telephony/java/android/telephony/satellite/ISatellitePositionUpdateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/ISatelliteTransmissionUpdateCallback.aidl
@@ -22,7 +22,7 @@
* Interface for position update and datagram transfer state change callback.
* @hide
*/
-oneway interface ISatellitePositionUpdateCallback {
+oneway interface ISatelliteTransmissionUpdateCallback {
/**
* Called when satellite datagram transfer state changed.
*
diff --git a/telephony/java/android/telephony/satellite/PointingInfo.java b/telephony/java/android/telephony/satellite/PointingInfo.java
index a3c3f19..7c794473 100644
--- a/telephony/java/android/telephony/satellite/PointingInfo.java
+++ b/telephony/java/android/telephony/satellite/PointingInfo.java
@@ -30,31 +30,12 @@
/** Satellite elevation in degrees */
private float mSatelliteElevationDegrees;
- /** Antenna azimuth in degrees */
- private float mAntennaAzimuthDegrees;
-
- /**
- * Angle of rotation about the x axis. This value represents the angle between a plane
- * parallel to the device's screen and a plane parallel to the ground.
- */
- private float mAntennaPitchDegrees;
-
- /**
- * Angle of rotation about the y axis. This value represents the angle between a plane
- * perpendicular to the device's screen and a plane parallel to the ground.
- */
- private float mAntennaRollDegrees;
-
/**
* @hide
*/
- public PointingInfo(float satelliteAzimuthDegrees, float satelliteElevationDegrees,
- float antennaAzimuthDegrees, float antennaPitchDegrees, float antennaRollDegrees) {
+ public PointingInfo(float satelliteAzimuthDegrees, float satelliteElevationDegrees) {
mSatelliteAzimuthDegrees = satelliteAzimuthDegrees;
mSatelliteElevationDegrees = satelliteElevationDegrees;
- mAntennaAzimuthDegrees = antennaAzimuthDegrees;
- mAntennaPitchDegrees = antennaPitchDegrees;
- mAntennaRollDegrees = antennaRollDegrees;
}
private PointingInfo(Parcel in) {
@@ -70,9 +51,6 @@
public void writeToParcel(@NonNull Parcel out, int flags) {
out.writeFloat(mSatelliteAzimuthDegrees);
out.writeFloat(mSatelliteElevationDegrees);
- out.writeFloat(mAntennaAzimuthDegrees);
- out.writeFloat(mAntennaPitchDegrees);
- out.writeFloat(mAntennaRollDegrees);
}
public static final @android.annotation.NonNull Creator<PointingInfo> CREATOR =
@@ -99,18 +77,6 @@
sb.append("SatelliteElevationDegrees:");
sb.append(mSatelliteElevationDegrees);
- sb.append(",");
-
- sb.append("AntennaAzimuthDegrees:");
- sb.append(mAntennaAzimuthDegrees);
- sb.append(",");
-
- sb.append("AntennaPitchDegrees:");
- sb.append(mAntennaPitchDegrees);
- sb.append(",");
-
- sb.append("AntennaRollDegrees:");
- sb.append(mAntennaRollDegrees);
return sb.toString();
}
@@ -122,23 +88,8 @@
return mSatelliteElevationDegrees;
}
- public float getAntennaAzimuthDegrees() {
- return mAntennaAzimuthDegrees;
- }
-
- public float getAntennaPitchDegrees() {
- return mAntennaPitchDegrees;
- }
-
- public float getAntennaRollDegrees() {
- return mAntennaRollDegrees;
- }
-
private void readFromParcel(Parcel in) {
mSatelliteAzimuthDegrees = in.readFloat();
mSatelliteElevationDegrees = in.readFloat();
- mAntennaAzimuthDegrees = in.readFloat();
- mAntennaPitchDegrees = in.readFloat();
- mAntennaRollDegrees = in.readFloat();
}
}
diff --git a/telephony/java/android/telephony/satellite/SatelliteCapabilities.java b/telephony/java/android/telephony/satellite/SatelliteCapabilities.java
index 889856b..df80159 100644
--- a/telephony/java/android/telephony/satellite/SatelliteCapabilities.java
+++ b/telephony/java/android/telephony/satellite/SatelliteCapabilities.java
@@ -33,31 +33,24 @@
@NonNull @SatelliteManager.NTRadioTechnology private Set<Integer> mSupportedRadioTechnologies;
/**
- * Whether satellite modem is always on.
- * This indicates the power impact of keeping it on is very minimal.
- */
- private boolean mIsAlwaysOn;
-
- /**
* Whether UE needs to point to a satellite to send and receive data.
*/
- private boolean mNeedsPointingToSatellite;
+ private boolean mIsPointingRequired;
/**
- * Whether UE needs a separate SIM profile to communicate with the satellite network.
+ * The maximum number of bytes per datagram that can be sent over satellite.
*/
- private boolean mNeedsSeparateSimProfile;
+ private int mMaxBytesPerOutgoingDatagram;
/**
* @hide
*/
- public SatelliteCapabilities(Set<Integer> supportedRadioTechnologies, boolean isAlwaysOn,
- boolean needsPointingToSatellite, boolean needsSeparateSimProfile) {
+ public SatelliteCapabilities(Set<Integer> supportedRadioTechnologies,
+ boolean isPointingRequired, int maxBytesPerOutgoingDatagram) {
mSupportedRadioTechnologies = supportedRadioTechnologies == null
? new HashSet<>() : supportedRadioTechnologies;
- mIsAlwaysOn = isAlwaysOn;
- mNeedsPointingToSatellite = needsPointingToSatellite;
- mNeedsSeparateSimProfile = needsSeparateSimProfile;
+ mIsPointingRequired = isPointingRequired;
+ mMaxBytesPerOutgoingDatagram = maxBytesPerOutgoingDatagram;
}
private SatelliteCapabilities(Parcel in) {
@@ -80,9 +73,8 @@
out.writeInt(0);
}
- out.writeBoolean(mIsAlwaysOn);
- out.writeBoolean(mNeedsPointingToSatellite);
- out.writeBoolean(mNeedsSeparateSimProfile);
+ out.writeBoolean(mIsPointingRequired);
+ out.writeInt(mMaxBytesPerOutgoingDatagram);
}
@NonNull public static final Creator<SatelliteCapabilities> CREATOR = new Creator<>() {
@@ -111,16 +103,12 @@
sb.append("none,");
}
- sb.append("isAlwaysOn:");
- sb.append(mIsAlwaysOn);
+ sb.append("isPointingRequired:");
+ sb.append(mIsPointingRequired);
sb.append(",");
- sb.append("needsPointingToSatellite:");
- sb.append(mNeedsPointingToSatellite);
- sb.append(",");
-
- sb.append("needsSeparateSimProfile:");
- sb.append(mNeedsSeparateSimProfile);
+ sb.append("maxBytesPerOutgoingDatagram");
+ sb.append(mMaxBytesPerOutgoingDatagram);
return sb.toString();
}
@@ -133,33 +121,22 @@
}
/**
- * Get whether the satellite modem is always on.
- * This indicates the power impact of keeping it on is very minimal.
- *
- * @return {@code true} if the satellite modem is always on and {@code false} otherwise.
- */
- public boolean isAlwaysOn() {
- return mIsAlwaysOn;
- }
-
- /**
* Get whether UE needs to point to a satellite to send and receive data.
*
- * @return {@code true} if UE needs to pointing to a satellite to send and receive data and
+ * @return {@code true} if UE needs to point to a satellite to send and receive data and
* {@code false} otherwise.
*/
- public boolean needsPointingToSatellite() {
- return mNeedsPointingToSatellite;
+ public boolean isPointingRequired() {
+ return mIsPointingRequired;
}
/**
- * Get whether UE needs a separate SIM profile to communicate with the satellite network.
+ * The maximum number of bytes per datagram that can be sent over satellite.
*
- * @return {@code true} if UE needs a separate SIM profile to comunicate with the satellite
- * network and {@code false} otherwise.
+ * @return The maximum number of bytes per datagram that can be sent over satellite.
*/
- public boolean needsSeparateSimProfile() {
- return mNeedsSeparateSimProfile;
+ public int getMaxBytesPerOutgoingDatagram() {
+ return mMaxBytesPerOutgoingDatagram;
}
private void readFromParcel(Parcel in) {
@@ -171,8 +148,7 @@
}
}
- mIsAlwaysOn = in.readBoolean();
- mNeedsPointingToSatellite = in.readBoolean();
- mNeedsSeparateSimProfile = in.readBoolean();
+ mIsPointingRequired = in.readBoolean();
+ mMaxBytesPerOutgoingDatagram = in.readInt();
}
}
diff --git a/telephony/java/android/telephony/satellite/SatelliteDatagram.java b/telephony/java/android/telephony/satellite/SatelliteDatagram.java
index cc5a9f4..d3cb8a0 100644
--- a/telephony/java/android/telephony/satellite/SatelliteDatagram.java
+++ b/telephony/java/android/telephony/satellite/SatelliteDatagram.java
@@ -17,7 +17,6 @@
package android.telephony.satellite;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
@@ -28,7 +27,7 @@
/**
* Datagram to be sent or received over satellite.
*/
- private byte[] mData;
+ @NonNull private byte[] mData;
/**
* @hide
@@ -51,8 +50,8 @@
out.writeByteArray(mData);
}
- public static final @android.annotation.NonNull Creator<SatelliteDatagram> CREATOR =
- new Creator<SatelliteDatagram>() {
+ @NonNull public static final Creator<SatelliteDatagram> CREATOR =
+ new Creator<>() {
@Override
public SatelliteDatagram createFromParcel(Parcel in) {
return new SatelliteDatagram(in);
@@ -64,8 +63,7 @@
}
};
- @Nullable
- public byte[] getSatelliteDatagram() {
+ @NonNull public byte[] getSatelliteDatagram() {
return mData;
}
diff --git a/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java b/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java
index 213b985..f237ada 100644
--- a/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java
@@ -17,48 +17,18 @@
package android.telephony.satellite;
import android.annotation.NonNull;
-import android.os.Binder;
import com.android.internal.telephony.ILongConsumer;
-import java.util.concurrent.Executor;
-
/**
* A callback class for listening to satellite datagrams.
*
* @hide
*/
-public class SatelliteDatagramCallback {
- private final CallbackBinder mBinder = new CallbackBinder(this);
-
- private static class CallbackBinder extends ISatelliteDatagramCallback.Stub {
- private final SatelliteDatagramCallback mLocalCallback;
- private Executor mExecutor;
-
- private CallbackBinder(SatelliteDatagramCallback localCallback) {
- mLocalCallback = localCallback;
- }
-
- @Override
- public void onSatelliteDatagramReceived(long datagramId,
- @NonNull SatelliteDatagram datagram, int pendingCount,
- @NonNull ILongConsumer callback) {
- final long callingIdentity = Binder.clearCallingIdentity();
- try {
- mExecutor.execute(() -> mLocalCallback.onSatelliteDatagramReceived(datagramId,
- datagram, pendingCount, callback));
- } finally {
- restoreCallingIdentity(callingIdentity);
- }
- }
-
- private void setExecutor(Executor executor) {
- mExecutor = executor;
- }
- }
-
+public interface SatelliteDatagramCallback {
/**
* Called when there is an incoming datagram to be received.
+ *
* @param datagramId An id that uniquely identifies incoming datagram.
* @param datagram Datagram to be received over satellite.
* @param pendingCount Number of datagrams yet to be received by the app.
@@ -66,19 +36,6 @@
* datagramId to Telephony. If the callback is not received within five minutes,
* Telephony will resend the datagram.
*/
- public void onSatelliteDatagramReceived(long datagramId, @NonNull SatelliteDatagram datagram,
- int pendingCount, @NonNull ILongConsumer callback) {
- // Base Implementation
- }
-
- /** @hide */
- @NonNull
- final ISatelliteDatagramCallback getBinder() {
- return mBinder;
- }
-
- /** @hide */
- public void setExecutor(@NonNull Executor executor) {
- mBinder.setExecutor(executor);
- }
+ void onSatelliteDatagramReceived(long datagramId, @NonNull SatelliteDatagram datagram,
+ int pendingCount, @NonNull ILongConsumer callback);
}
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 9ec6929..d0abfbf 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -36,12 +36,15 @@
import android.telephony.TelephonyFrameworkInitializer;
import com.android.internal.telephony.IIntegerConsumer;
+import com.android.internal.telephony.ILongConsumer;
import com.android.internal.telephony.ITelephony;
import com.android.telephony.Rlog;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -55,6 +58,17 @@
public class SatelliteManager {
private static final String TAG = "SatelliteManager";
+ private static final ConcurrentHashMap<SatelliteDatagramCallback, ISatelliteDatagramCallback>
+ sSatelliteDatagramCallbackMap = new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap<SatelliteProvisionStateCallback,
+ ISatelliteProvisionStateCallback> sSatelliteProvisionStateCallbackMap =
+ new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap<SatelliteStateCallback, ISatelliteStateCallback>
+ sSatelliteStateCallbackMap = new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap<SatelliteTransmissionUpdateCallback,
+ ISatelliteTransmissionUpdateCallback> sSatelliteTransmissionUpdateCallbackMap =
+ new ConcurrentHashMap<>();
+
private final int mSubId;
/**
@@ -66,6 +80,7 @@
* Create an instance of the SatelliteManager.
*
* @param context The context the SatelliteManager belongs to.
+ * @hide
*/
public SatelliteManager(@Nullable Context context) {
this(context, SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
@@ -116,7 +131,7 @@
/**
* Bundle key to get the response from
- * {@link #requestIsSatelliteDemoModeEnabled(Executor, OutcomeReceiver)}.
+ * {@link #requestIsDemoModeEnabled(Executor, OutcomeReceiver)}.
* @hide
*/
public static final String KEY_DEMO_MODE_ENABLED = "demo_mode_enabled";
@@ -137,14 +152,6 @@
/**
* Bundle key to get the response from
- * {@link #requestMaxSizePerSendingDatagram(Executor, OutcomeReceiver)} .
- * @hide
- */
- public static final String KEY_MAX_CHARACTERS_PER_SATELLITE_TEXT =
- "max_characters_per_satellite_text";
-
- /**
- * Bundle key to get the response from
* {@link #requestIsSatelliteProvisioned(Executor, OutcomeReceiver)}.
* @hide
*/
@@ -319,23 +326,25 @@
public @interface NTRadioTechnology {}
/**
- * Request to enable or disable the satellite modem. If the satellite modem is enabled, this
- * will also disable the cellular modem, and if the satellite modem is disabled, this will also
- * re-enable the cellular modem.
+ * Request to enable or disable the satellite modem and demo mode. If the satellite modem is
+ * enabled, this may also disable the cellular modem, and if the satellite modem is disabled,
+ * this may also re-enable the cellular modem.
*
- * @param enable {@code true} to enable the satellite modem and {@code false} to disable.
+ * @param enableSatellite {@code true} to enable the satellite modem and
+ * {@code false} to disable.
+ * @param enableDemoMode {@code true} to enable demo mode and {@code false} to disable.
* @param executor The executor on which the error code listener will be called.
- * @param errorCodeListener Listener for the {@link SatelliteError} result of the operation.
+ * @param resultListener Listener for the {@link SatelliteError} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
* @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
- public void requestSatelliteEnabled(
- boolean enable, @NonNull @CallbackExecutor Executor executor,
- @NonNull Consumer<Integer> errorCodeListener) {
+ public void requestSatelliteEnabled(boolean enableSatellite, boolean enableDemoMode,
+ @NonNull @CallbackExecutor Executor executor,
+ @SatelliteError @NonNull Consumer<Integer> resultListener) {
Objects.requireNonNull(executor);
- Objects.requireNonNull(errorCodeListener);
+ Objects.requireNonNull(resultListener);
try {
ITelephony telephony = getITelephony();
@@ -344,10 +353,11 @@
@Override
public void accept(int result) {
executor.execute(() -> Binder.withCleanCallingIdentity(
- () -> errorCodeListener.accept(result)));
+ () -> resultListener.accept(result)));
}
};
- telephony.requestSatelliteEnabled(mSubId, enable, errorCallback);
+ telephony.requestSatelliteEnabled(mSubId, enableSatellite, enableDemoMode,
+ errorCallback);
} else {
throw new IllegalStateException("telephony service is null.");
}
@@ -412,50 +422,13 @@
}
/**
- * Request to enable or disable the satellite service demo mode.
- *
- * @param enable {@code true} to enable the satellite demo mode and {@code false} to disable.
- * @param executor The executor on which the error code listener will be called.
- * @param errorCodeListener Listener for the {@link SatelliteError} result of the operation.
- *
- * @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
- */
- @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
- public void requestSatelliteDemoModeEnabled(boolean enable,
- @NonNull @CallbackExecutor Executor executor,
- @NonNull Consumer<Integer> errorCodeListener) {
- Objects.requireNonNull(executor);
- Objects.requireNonNull(errorCodeListener);
-
- try {
- ITelephony telephony = getITelephony();
- if (telephony != null) {
- IIntegerConsumer errorCallback = new IIntegerConsumer.Stub() {
- @Override
- public void accept(int result) {
- executor.execute(() -> Binder.withCleanCallingIdentity(
- () -> errorCodeListener.accept(result)));
- }
- };
- telephony.requestSatelliteDemoModeEnabled(mSubId, enable, errorCallback);
- } else {
- throw new IllegalStateException("telephony service is null.");
- }
- } catch (RemoteException ex) {
- Rlog.e(TAG, "requestSatelliteDemoModeEnabled() RemoteException: ", ex);
- ex.rethrowFromSystemServer();
- }
- }
-
- /**
* Request to get whether the satellite service demo mode is enabled.
*
* @param executor The executor on which the callback will be called.
* @param callback The callback object to which the result will be delivered.
* If the request is successful, {@link OutcomeReceiver#onResult(Object)}
- * will return a {@code boolean} with value {@code true} if the satellite
- * demo mode is enabled and {@code false} otherwise.
+ * will return a {@code boolean} with value {@code true} if demo mode is enabled
+ * and {@code false} otherwise.
* If the request is not successful, {@link OutcomeReceiver#onError(Throwable)}
* will return a {@link SatelliteException} with the {@link SatelliteError}.
*
@@ -463,7 +436,7 @@
* @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
- public void requestIsSatelliteDemoModeEnabled(@NonNull @CallbackExecutor Executor executor,
+ public void requestIsDemoModeEnabled(@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Boolean, SatelliteException> callback) {
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
@@ -492,12 +465,12 @@
}
}
};
- telephony.requestIsSatelliteDemoModeEnabled(mSubId, receiver);
+ telephony.requestIsDemoModeEnabled(mSubId, receiver);
} else {
throw new IllegalStateException("telephony service is null.");
}
} catch (RemoteException ex) {
- loge("requestIsSatelliteDemoModeEnabled() RemoteException: " + ex);
+ loge("requestIsDemoModeEnabled() RemoteException: " + ex);
ex.rethrowFromSystemServer();
}
}
@@ -742,145 +715,117 @@
public @interface DatagramType {}
/**
- * Start receiving satellite position updates.
+ * Start receiving satellite transmission updates.
* This can be called by the pointing UI when the user starts pointing to the satellite.
* Modem should continue to report the pointing input as the device or satellite moves.
- * Satellite position updates are started only on {@link #SATELLITE_ERROR_NONE}.
+ * Satellite transmission updates are started only on {@link #SATELLITE_ERROR_NONE}.
* All other results indicate that this operation failed.
- * Once satellite position updates begin, datagram transfer state updates will be sent
- * through {@link SatellitePositionUpdateCallback}.
+ * Once satellite transmission updates begin, position and datagram transfer state updates
+ * will be sent through {@link SatelliteTransmissionUpdateCallback}.
*
* @param executor The executor on which the callback and error code listener will be called.
- * @param errorCodeListener Listener for the {@link SatelliteError} result of the operation.
- * @param callback The callback to notify of changes in satellite position.
+ * @param resultListener Listener for the {@link SatelliteError} result of the operation.
+ * @param callback The callback to notify of satellite transmission updates.
*
* @throws SecurityException if the caller doesn't have required permission.
* @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
- public void startSatellitePositionUpdates(@NonNull @CallbackExecutor Executor executor,
- @NonNull Consumer<Integer> errorCodeListener,
- @NonNull SatellitePositionUpdateCallback callback) {
+ public void startSatelliteTransmissionUpdates(@NonNull @CallbackExecutor Executor executor,
+ @SatelliteError @NonNull Consumer<Integer> resultListener,
+ @NonNull SatelliteTransmissionUpdateCallback callback) {
Objects.requireNonNull(executor);
- Objects.requireNonNull(errorCodeListener);
+ Objects.requireNonNull(resultListener);
Objects.requireNonNull(callback);
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
- callback.setExecutor(executor);
IIntegerConsumer errorCallback = new IIntegerConsumer.Stub() {
@Override
public void accept(int result) {
executor.execute(() -> Binder.withCleanCallingIdentity(
- () -> errorCodeListener.accept(result)));
+ () -> resultListener.accept(result)));
}
};
- telephony.startSatellitePositionUpdates(
- mSubId, errorCallback, callback.getBinder());
+ ISatelliteTransmissionUpdateCallback internalCallback =
+ new ISatelliteTransmissionUpdateCallback.Stub() {
+ @Override
+ public void onDatagramTransferStateChanged(int state,
+ int sendPendingCount, int receivePendingCount, int errorCode) {
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> callback.onDatagramTransferStateChanged(
+ state, sendPendingCount, receivePendingCount,
+ errorCode)));
+ }
+
+ @Override
+ public void onSatellitePositionChanged(PointingInfo pointingInfo) {
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> callback.onSatellitePositionChanged(pointingInfo)));
+ }
+ };
+ sSatelliteTransmissionUpdateCallbackMap.put(callback, internalCallback);
+ telephony.startSatelliteTransmissionUpdates(mSubId, errorCallback,
+ internalCallback);
} else {
throw new IllegalStateException("telephony service is null.");
}
} catch (RemoteException ex) {
- loge("startSatellitePositionUpdates() RemoteException: " + ex);
+ loge("startSatelliteTransmissionUpdates() RemoteException: " + ex);
ex.rethrowFromSystemServer();
}
}
/**
- * Stop receiving satellite position updates.
+ * Stop receiving satellite transmission updates.
* This can be called by the pointing UI when the user stops pointing to the satellite.
- * Satellite position updates are stopped and the callback is unregistered only on
+ * Satellite transmission updates are stopped and the callback is unregistered only on
* {@link #SATELLITE_ERROR_NONE}. All other results that this operation failed.
*
- * @param callback The callback that was passed to
- * {@link #startSatellitePositionUpdates(Executor, Consumer, SatellitePositionUpdateCallback)}.
+ * @param callback The callback that was passed to {@link
+ * #startSatelliteTransmissionUpdates(Executor, Consumer, SatelliteTransmissionUpdateCallback)}.
* @param executor The executor on which the error code listener will be called.
- * @param errorCodeListener Listener for the {@link SatelliteError} result of the operation.
+ * @param resultListener Listener for the {@link SatelliteError} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
* @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
- public void stopSatellitePositionUpdates(@NonNull SatellitePositionUpdateCallback callback,
+ public void stopSatelliteTransmissionUpdates(
+ @NonNull SatelliteTransmissionUpdateCallback callback,
@NonNull @CallbackExecutor Executor executor,
- @NonNull Consumer<Integer> errorCodeListener) {
+ @SatelliteError @NonNull Consumer<Integer> resultListener) {
Objects.requireNonNull(callback);
Objects.requireNonNull(executor);
- Objects.requireNonNull(errorCodeListener);
+ Objects.requireNonNull(resultListener);
+ ISatelliteTransmissionUpdateCallback internalCallback =
+ sSatelliteTransmissionUpdateCallbackMap.remove(callback);
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
- IIntegerConsumer errorCallback = new IIntegerConsumer.Stub() {
- @Override
- public void accept(int result) {
- executor.execute(() -> Binder.withCleanCallingIdentity(
- () -> errorCodeListener.accept(result)));
- }
- };
- telephony.stopSatellitePositionUpdates(mSubId, errorCallback,
- callback.getBinder());
- // TODO: Notify SmsHandler that pointing UI stopped
- } else {
- throw new IllegalStateException("telephony service is null.");
- }
- } catch (RemoteException ex) {
- loge("stopSatellitePositionUpdates() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
- }
- }
-
- /**
- * Request to get the maximum number of bytes per datagram that can be sent to satellite.
- *
- * @param executor The executor on which the callback will be called.
- * @param callback The callback object to which the result will be delivered.
- * If the request is successful, {@link OutcomeReceiver#onResult(Object)}
- * will return the maximum number of bytes per datagram that can be sent to
- * satellite.
- * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)}
- *
- * @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
- */
- @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
- public void requestMaxSizePerSendingDatagram(
- @NonNull @CallbackExecutor Executor executor,
- @NonNull OutcomeReceiver<Integer, SatelliteException> callback) {
- Objects.requireNonNull(executor);
- Objects.requireNonNull(callback);
-
- try {
- ITelephony telephony = getITelephony();
- if (telephony != null) {
- ResultReceiver receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- if (resultCode == SATELLITE_ERROR_NONE) {
- if (resultData.containsKey(KEY_MAX_CHARACTERS_PER_SATELLITE_TEXT)) {
- int maxCharacters =
- resultData.getInt(KEY_MAX_CHARACTERS_PER_SATELLITE_TEXT);
- executor.execute(() -> Binder.withCleanCallingIdentity(() ->
- callback.onResult(maxCharacters)));
- } else {
- loge("KEY_MAX_CHARACTERS_PER_SATELLITE_TEXT does not exist.");
- executor.execute(() -> Binder.withCleanCallingIdentity(() ->
- callback.onError(
- new SatelliteException(SATELLITE_REQUEST_FAILED))));
- }
- } else {
- executor.execute(() -> Binder.withCleanCallingIdentity(() ->
- callback.onError(new SatelliteException(resultCode))));
+ if (internalCallback != null) {
+ IIntegerConsumer errorCallback = new IIntegerConsumer.Stub() {
+ @Override
+ public void accept(int result) {
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(result)));
}
- }
- };
- telephony.requestMaxSizePerSendingDatagram(mSubId, receiver);
+ };
+ telephony.stopSatelliteTransmissionUpdates(mSubId, errorCallback,
+ internalCallback);
+ // TODO: Notify SmsHandler that pointing UI stopped
+ } else {
+ loge("stopSatelliteTransmissionUpdates: No internal callback.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_INVALID_ARGUMENTS)));
+ }
} else {
throw new IllegalStateException("telephony service is null.");
}
} catch (RemoteException ex) {
- loge("requestMaxCharactersPerSatelliteTextMessage() RemoteException: " + ex);
+ loge("stopSatelliteTransmissionUpdates() RemoteException: " + ex);
ex.rethrowFromSystemServer();
}
}
@@ -891,23 +836,24 @@
*
* @param token The token to be used as a unique identifier for provisioning with satellite
* gateway.
+ * @param regionId The region ID for the device's current location.
* @param cancellationSignal The optional signal used by the caller to cancel the provision
* request. Even when the cancellation is signaled, Telephony will
* still trigger the callback to return the result of this request.
* @param executor The executor on which the error code listener will be called.
- * @param errorCodeListener Listener for the {@link SatelliteError} result of the operation.
+ * @param resultListener Listener for the {@link SatelliteError} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
* @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
- public void provisionSatelliteService(@NonNull String token,
+ public void provisionSatelliteService(@NonNull String token, @NonNull String regionId,
@Nullable CancellationSignal cancellationSignal,
@NonNull @CallbackExecutor Executor executor,
- @SatelliteError @NonNull Consumer<Integer> errorCodeListener) {
+ @SatelliteError @NonNull Consumer<Integer> resultListener) {
Objects.requireNonNull(token);
Objects.requireNonNull(executor);
- Objects.requireNonNull(errorCodeListener);
+ Objects.requireNonNull(resultListener);
ICancellationSignal cancelRemote = null;
try {
@@ -917,10 +863,11 @@
@Override
public void accept(int result) {
executor.execute(() -> Binder.withCleanCallingIdentity(
- () -> errorCodeListener.accept(result)));
+ () -> resultListener.accept(result)));
}
};
- cancelRemote = telephony.provisionSatelliteService(mSubId, token, errorCallback);
+ cancelRemote = telephony.provisionSatelliteService(mSubId, token, regionId,
+ errorCallback);
} else {
throw new IllegalStateException("telephony service is null.");
}
@@ -942,7 +889,7 @@
* {@link #provisionSatelliteService(String, CancellationSignal, Executor, Consumer)}.
*
* @param token The token of the device/subscription to be deprovisioned.
- * @param errorCodeListener Listener for the {@link SatelliteError} result of the operation.
+ * @param resultListener Listener for the {@link SatelliteError} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
* @throws IllegalStateException if the Telephony process is not currently available.
@@ -950,10 +897,10 @@
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
public void deprovisionSatelliteService(@NonNull String token,
@NonNull @CallbackExecutor Executor executor,
- @SatelliteError @NonNull Consumer<Integer> errorCodeListener) {
+ @SatelliteError @NonNull Consumer<Integer> resultListener) {
Objects.requireNonNull(token);
Objects.requireNonNull(executor);
- Objects.requireNonNull(errorCodeListener);
+ Objects.requireNonNull(resultListener);
try {
ITelephony telephony = getITelephony();
@@ -962,7 +909,7 @@
@Override
public void accept(int result) {
executor.execute(() -> Binder.withCleanCallingIdentity(
- () -> errorCodeListener.accept(result)));
+ () -> resultListener.accept(result)));
}
};
telephony.deprovisionSatelliteService(mSubId, token, errorCallback);
@@ -996,9 +943,18 @@
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
- callback.setExecutor(executor);
+ ISatelliteProvisionStateCallback internalCallback =
+ new ISatelliteProvisionStateCallback.Stub() {
+ @Override
+ public void onSatelliteProvisionStateChanged(boolean provisioned) {
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> callback.onSatelliteProvisionStateChanged(
+ provisioned)));
+ }
+ };
+ sSatelliteProvisionStateCallbackMap.put(callback, internalCallback);
return telephony.registerForSatelliteProvisionStateChanged(
- mSubId, callback.getBinder());
+ mSubId, internalCallback);
} else {
throw new IllegalStateException("telephony service is null.");
}
@@ -1023,11 +979,17 @@
public void unregisterForSatelliteProvisionStateChanged(
@NonNull SatelliteProvisionStateCallback callback) {
Objects.requireNonNull(callback);
+ ISatelliteProvisionStateCallback internalCallback =
+ sSatelliteProvisionStateCallbackMap.remove(callback);
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
- telephony.unregisterForSatelliteProvisionStateChanged(mSubId, callback.getBinder());
+ if (internalCallback != null) {
+ telephony.unregisterForSatelliteProvisionStateChanged(mSubId, internalCallback);
+ } else {
+ loge("unregisterForSatelliteProvisionStateChanged: No internal callback.");
+ }
} else {
throw new IllegalStateException("telephony service is null.");
}
@@ -1112,9 +1074,15 @@
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
- callback.setExecutor(executor);
- return telephony.registerForSatelliteModemStateChanged(mSubId,
- callback.getBinder());
+ ISatelliteStateCallback internalCallback = new ISatelliteStateCallback.Stub() {
+ @Override
+ public void onSatelliteModemStateChanged(int state) {
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onSatelliteModemStateChanged(state)));
+ }
+ };
+ sSatelliteStateCallbackMap.put(callback, internalCallback);
+ return telephony.registerForSatelliteModemStateChanged(mSubId, internalCallback);
} else {
throw new IllegalStateException("telephony service is null.");
}
@@ -1138,11 +1106,16 @@
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
public void unregisterForSatelliteModemStateChanged(@NonNull SatelliteStateCallback callback) {
Objects.requireNonNull(callback);
+ ISatelliteStateCallback internalCallback = sSatelliteStateCallbackMap.remove(callback);
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
- telephony.unregisterForSatelliteModemStateChanged(mSubId, callback.getBinder());
+ if (internalCallback != null) {
+ telephony.unregisterForSatelliteModemStateChanged(mSubId, internalCallback);
+ } else {
+ loge("unregisterForSatelliteModemStateChanged: No internal callback.");
+ }
} else {
throw new IllegalStateException("telephony service is null.");
}
@@ -1155,8 +1128,6 @@
/**
* Register to receive incoming datagrams over satellite.
*
- * @param datagramType datagram type indicating whether the datagram is of type
- * SOS_SMS or LOCATION_SHARING.
* @param executor The executor on which the callback will be called.
* @param callback The callback to handle incoming datagrams over satellite.
* This callback with be invoked when a new datagram is received from satellite.
@@ -1167,7 +1138,7 @@
* @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
- @SatelliteError public int registerForSatelliteDatagram(@DatagramType int datagramType,
+ @SatelliteError public int registerForSatelliteDatagram(
@NonNull @CallbackExecutor Executor executor,
@NonNull SatelliteDatagramCallback callback) {
Objects.requireNonNull(executor);
@@ -1176,9 +1147,19 @@
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
- callback.setExecutor(executor);
- return telephony.registerForSatelliteDatagram(mSubId, datagramType,
- callback.getBinder());
+ ISatelliteDatagramCallback internalCallback =
+ new ISatelliteDatagramCallback.Stub() {
+ @Override
+ public void onSatelliteDatagramReceived(long datagramId,
+ @NonNull SatelliteDatagram datagram, int pendingCount,
+ @NonNull ILongConsumer ack) {
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> callback.onSatelliteDatagramReceived(
+ datagramId, datagram, pendingCount, ack)));
+ }
+ };
+ sSatelliteDatagramCallbackMap.put(callback, internalCallback);
+ return telephony.registerForSatelliteDatagram(mSubId, internalCallback);
} else {
throw new IllegalStateException("telephony service is null.");
}
@@ -1194,7 +1175,7 @@
* If callback was not registered before, the request will be ignored.
*
* @param callback The callback that was passed to
- * {@link #registerForSatelliteDatagram(int, Executor, SatelliteDatagramCallback)}.
+ * {@link #registerForSatelliteDatagram(Executor, SatelliteDatagramCallback)}.
*
* @throws SecurityException if the caller doesn't have required permission.
* @throws IllegalStateException if the Telephony process is not currently available.
@@ -1202,11 +1183,17 @@
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
public void unregisterForSatelliteDatagram(@NonNull SatelliteDatagramCallback callback) {
Objects.requireNonNull(callback);
+ ISatelliteDatagramCallback internalCallback =
+ sSatelliteDatagramCallbackMap.remove(callback);
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
- telephony.unregisterForSatelliteDatagram(mSubId, callback.getBinder());
+ if (internalCallback != null) {
+ telephony.unregisterForSatelliteDatagram(mSubId, internalCallback);
+ } else {
+ loge("unregisterForSatelliteDatagram: No internal callback.");
+ }
} else {
throw new IllegalStateException("telephony service is null.");
}
@@ -1222,7 +1209,7 @@
* This method requests modem to check if there are any pending datagrams to be received over
* satellite. If there are any incoming datagrams, they will be received via
* {@link SatelliteDatagramCallback#onSatelliteDatagramReceived(long, SatelliteDatagram, int,
- * ISatelliteDatagramReceiverAck)}
+ * ILongConsumer)}
*
* @param executor The executor on which the result listener will be called.
* @param resultListener Listener for the {@link SatelliteError} result of the operation.
@@ -1371,9 +1358,8 @@
}
/**
- * Request to get the time after which the satellite will be visible. This is an
- * {@code int} representing the duration in seconds after which the satellite will be visible.
- * This will return {@code 0} if the satellite is currently visible.
+ * Request to get the duration in seconds after which the satellite will be visible.
+ * This will be {@link Duration#ZERO} if the satellite is currently visible.
*
* @param executor The executor on which the callback will be called.
* @param callback The callback object to which the result will be delivered.
@@ -1387,7 +1373,7 @@
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
public void requestTimeForNextSatelliteVisibility(@NonNull @CallbackExecutor Executor executor,
- @NonNull OutcomeReceiver<Integer, SatelliteException> callback) {
+ @NonNull OutcomeReceiver<Duration, SatelliteException> callback) {
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
@@ -1402,7 +1388,8 @@
int nextVisibilityDuration =
resultData.getInt(KEY_SATELLITE_NEXT_VISIBILITY);
executor.execute(() -> Binder.withCleanCallingIdentity(() ->
- callback.onResult(nextVisibilityDuration)));
+ callback.onResult(
+ Duration.ofSeconds(nextVisibilityDuration))));
} else {
loge("KEY_SATELLITE_NEXT_VISIBILITY does not exist.");
executor.execute(() -> Binder.withCleanCallingIdentity(() ->
diff --git a/telephony/java/android/telephony/satellite/SatellitePositionUpdateCallback.java b/telephony/java/android/telephony/satellite/SatellitePositionUpdateCallback.java
deleted file mode 100644
index d44a84d..0000000
--- a/telephony/java/android/telephony/satellite/SatellitePositionUpdateCallback.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.telephony.satellite;
-
-import android.annotation.NonNull;
-import android.os.Binder;
-
-import java.util.concurrent.Executor;
-
-/**
- * A callback class for monitoring satellite position update and datagram transfer state change
- * events.
- *
- * @hide
- */
-public class SatellitePositionUpdateCallback {
- private final CallbackBinder mBinder = new CallbackBinder(this);
-
- private static class CallbackBinder extends ISatellitePositionUpdateCallback.Stub {
- private final SatellitePositionUpdateCallback mLocalCallback;
- private Executor mExecutor;
-
- private CallbackBinder(SatellitePositionUpdateCallback localCallback) {
- mLocalCallback = localCallback;
- }
-
- @Override
- public void onSatellitePositionChanged(@NonNull PointingInfo pointingInfo) {
- final long callingIdentity = Binder.clearCallingIdentity();
- try {
- mExecutor.execute(() ->
- mLocalCallback.onSatellitePositionChanged(pointingInfo));
- } finally {
- restoreCallingIdentity(callingIdentity);
- }
- }
-
- @Override
- public void onDatagramTransferStateChanged(
- @SatelliteManager.SatelliteDatagramTransferState int state, int sendPendingCount,
- int receivePendingCount, @SatelliteManager.SatelliteError int errorCode) {
- final long callingIdentity = Binder.clearCallingIdentity();
- try {
- mExecutor.execute(() ->
- mLocalCallback.onDatagramTransferStateChanged(
- state, sendPendingCount, receivePendingCount, errorCode));
- } finally {
- restoreCallingIdentity(callingIdentity);
- }
- }
-
- private void setExecutor(Executor executor) {
- mExecutor = executor;
- }
- }
-
- /**
- * Called when the satellite position changed.
- *
- * @param pointingInfo The pointing info containing the satellite location.
- */
- public void onSatellitePositionChanged(@NonNull PointingInfo pointingInfo) {
- // Base Implementation
- }
-
- /**
- * Called when satellite datagram transfer state changed.
- *
- * @param state The new datagram transfer state.
- * @param sendPendingCount The number of datagrams that are currently being sent.
- * @param receivePendingCount The number of datagrams that are currently being received.
- * @param errorCode If datagram transfer failed, the reason for failure.
- */
- public void onDatagramTransferStateChanged(
- @SatelliteManager.SatelliteDatagramTransferState int state, int sendPendingCount,
- int receivePendingCount, @SatelliteManager.SatelliteError int errorCode) {
- // Base Implementation
- }
-
- /**@hide*/
- @NonNull
- final ISatellitePositionUpdateCallback getBinder() {
- return mBinder;
- }
-
- /**@hide*/
- public void setExecutor(@NonNull Executor executor) {
- mBinder.setExecutor(executor);
- }
-}
diff --git a/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java
index 2b6a5d9..a62eb8b 100644
--- a/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java
@@ -16,61 +16,17 @@
package android.telephony.satellite;
-import android.annotation.NonNull;
-import android.os.Binder;
-
-import java.util.concurrent.Executor;
-
/**
* A callback class for monitoring satellite provision state change events.
*
* @hide
*/
-public class SatelliteProvisionStateCallback {
- private final CallbackBinder mBinder = new CallbackBinder(this);
-
- private static class CallbackBinder extends ISatelliteProvisionStateCallback.Stub {
- private final SatelliteProvisionStateCallback mLocalCallback;
- private Executor mExecutor;
-
- private CallbackBinder(SatelliteProvisionStateCallback localCallback) {
- mLocalCallback = localCallback;
- }
-
- @Override
- public void onSatelliteProvisionStateChanged(boolean provisioned) {
- final long callingIdentity = Binder.clearCallingIdentity();
- try {
- mExecutor.execute(() ->
- mLocalCallback.onSatelliteProvisionStateChanged(provisioned));
- } finally {
- restoreCallingIdentity(callingIdentity);
- }
- }
-
- private void setExecutor(Executor executor) {
- mExecutor = executor;
- }
- }
-
+public interface SatelliteProvisionStateCallback {
/**
* Called when satellite provision state changes.
*
* @param provisioned The new provision state. {@code true} means satellite is provisioned
* {@code false} means satellite is not provisioned.
*/
- public void onSatelliteProvisionStateChanged(boolean provisioned) {
- // Base Implementation
- }
-
- /**@hide*/
- @NonNull
- final ISatelliteProvisionStateCallback getBinder() {
- return mBinder;
- }
-
- /**@hide*/
- public void setExecutor(@NonNull Executor executor) {
- mBinder.setExecutor(executor);
- }
+ void onSatelliteProvisionStateChanged(boolean provisioned);
}
diff --git a/telephony/java/android/telephony/satellite/SatelliteStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteStateCallback.java
index 17d05b7..d9ecaa3 100644
--- a/telephony/java/android/telephony/satellite/SatelliteStateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteStateCallback.java
@@ -16,80 +16,15 @@
package android.telephony.satellite;
-import android.annotation.NonNull;
-import android.os.Binder;
-
-import java.util.concurrent.Executor;
-
/**
* A callback class for monitoring satellite modem state change events.
*
* @hide
*/
-public class SatelliteStateCallback {
- private final CallbackBinder mBinder = new CallbackBinder(this);
-
- private static class CallbackBinder extends ISatelliteStateCallback.Stub {
- private final SatelliteStateCallback mLocalCallback;
- private Executor mExecutor;
-
- private CallbackBinder(SatelliteStateCallback localCallback) {
- mLocalCallback = localCallback;
- }
-
- @Override
- public void onSatelliteModemStateChanged(@SatelliteManager.SatelliteModemState int state) {
- final long callingIdentity = Binder.clearCallingIdentity();
- try {
- mExecutor.execute(() ->
- mLocalCallback.onSatelliteModemStateChanged(state));
- } finally {
- restoreCallingIdentity(callingIdentity);
- }
- }
-
- @Override
- public void onPendingDatagramCount(int count) {
- final long callingIdentity = Binder.clearCallingIdentity();
- try {
- mExecutor.execute(() ->
- mLocalCallback.onPendingDatagramCount(count));
- } finally {
- restoreCallingIdentity(callingIdentity);
- }
- }
-
- private void setExecutor(Executor executor) {
- mExecutor = executor;
- }
- }
-
+public interface SatelliteStateCallback {
/**
* Called when satellite modem state changes.
* @param state The new satellite modem state.
*/
- public void onSatelliteModemStateChanged(@SatelliteManager.SatelliteModemState int state) {
- // Base Implementation
- }
-
- /**
- * Called when there are pending datagrams to be received from satellite.
- * @param count Pending datagram count.
- */
- public void onPendingDatagramCount(int count) {
- // Base Implementation
- }
-
- //TODO: Add an API for datagram transfer state update here.
-
- /**@hide*/
- @NonNull
- final ISatelliteStateCallback getBinder() {
- return mBinder;
- }
-
- /**@hide*/
- public void setExecutor(@NonNull Executor executor) {
- mBinder.setExecutor(executor);
- }
+ void onSatelliteModemStateChanged(@SatelliteManager.SatelliteModemState int state);
}
diff --git a/telephony/java/android/telephony/satellite/ISatellitePositionUpdateCallback.aidl b/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java
similarity index 68%
copy from telephony/java/android/telephony/satellite/ISatellitePositionUpdateCallback.aidl
copy to telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java
index d3f1091..0efbd1f 100644
--- a/telephony/java/android/telephony/satellite/ISatellitePositionUpdateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,13 +16,22 @@
package android.telephony.satellite;
-import android.telephony.satellite.PointingInfo;
+import android.annotation.NonNull;
/**
- * Interface for position update and datagram transfer state change callback.
+ * A callback class for monitoring satellite position update and datagram transfer state change
+ * events.
+ *
* @hide
*/
-oneway interface ISatellitePositionUpdateCallback {
+public interface SatelliteTransmissionUpdateCallback {
+ /**
+ * Called when the satellite position changed.
+ *
+ * @param pointingInfo The pointing info containing the satellite location.
+ */
+ void onSatellitePositionChanged(@NonNull PointingInfo pointingInfo);
+
/**
* Called when satellite datagram transfer state changed.
*
@@ -31,13 +40,7 @@
* @param receivePendingCount The number of datagrams that are currently being received.
* @param errorCode If datagram transfer failed, the reason for failure.
*/
- void onDatagramTransferStateChanged(in int state, in int sendPendingCount,
- in int receivePendingCount, in int errorCode);
-
- /**
- * Called when the satellite position changed.
- *
- * @param pointingInfo The pointing info containing the satellite location.
- */
- void onSatellitePositionChanged(in PointingInfo pointingInfo);
+ void onDatagramTransferStateChanged(@SatelliteManager.SatelliteDatagramTransferState int state,
+ int sendPendingCount, int receivePendingCount,
+ @SatelliteManager.SatelliteError int errorCode);
}
diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
index d93ee21..a780cb9 100644
--- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
@@ -49,10 +49,9 @@
* Listening mode allows the satellite service to listen for incoming pages.
*
* @param enable True to enable satellite listening mode and false to disable.
- * @param isDemoMode Whether demo mode is enabled.
* @param timeout How long the satellite modem should wait for the next incoming page before
* disabling listening mode.
- * @param errorCallback The callback to receive the error code result of the operation.
+ * @param resultCallback The callback to receive the error code result of the operation.
*
* Valid error codes returned:
* SatelliteError:ERROR_NONE
@@ -64,16 +63,17 @@
* SatelliteError:REQUEST_NOT_SUPPORTED
* SatelliteError:NO_RESOURCES
*/
- void requestSatelliteListeningEnabled(in boolean enable, in boolean isDemoMode, in int timeout,
- in IIntegerConsumer errorCallback);
+ void requestSatelliteListeningEnabled(in boolean enable, in int timeout,
+ in IIntegerConsumer resultCallback);
/**
- * Request to enable or disable the satellite modem. If the satellite modem is enabled,
- * this will also disable the cellular modem, and if the satellite modem is disabled,
- * this will also re-enable the cellular modem.
+ * Request to enable or disable the satellite modem and demo mode. If the satellite modem
+ * is enabled, this may also disable the cellular modem, and if the satellite modem is disabled,
+ * this may also re-enable the cellular modem.
*
- * @param enable True to enable the satellite modem and false to disable.
- * @param errorCallback The callback to receive the error code result of the operation.
+ * @param enableSatellite True to enable the satellite modem and false to disable.
+ * @param enableDemoMode True to enable demo mode and false to disable.
+ * @param resultCallback The callback to receive the error code result of the operation.
*
* Valid error codes returned:
* SatelliteError:ERROR_NONE
@@ -85,13 +85,14 @@
* SatelliteError:REQUEST_NOT_SUPPORTED
* SatelliteError:NO_RESOURCES
*/
- void requestSatelliteEnabled(in boolean enabled, in IIntegerConsumer errorCallback);
+ void requestSatelliteEnabled(in boolean enableSatellite, in boolean enableDemoMode,
+ in IIntegerConsumer resultCallback);
/**
* Request to get whether the satellite modem is enabled.
*
- * @param errorCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not SatelliteError#ERROR_NONE.
+ * @param resultCallback The callback to receive the error code result of the operation.
+ * This must only be sent when the error is not SatelliteError#ERROR_NONE.
* @param callback If the result is SatelliteError#ERROR_NONE, the callback to receive
* whether the satellite modem is enabled.
*
@@ -105,13 +106,13 @@
* SatelliteError:REQUEST_NOT_SUPPORTED
* SatelliteError:NO_RESOURCES
*/
- void requestIsSatelliteEnabled(in IIntegerConsumer errorCallback, in IBooleanConsumer callback);
+ void requestIsSatelliteEnabled(in IIntegerConsumer resultCallback, in IBooleanConsumer callback);
/**
* Request to get whether the satellite service is supported on the device.
*
- * @param errorCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not SatelliteError#ERROR_NONE.
+ * @param resultCallback The callback to receive the error code result of the operation.
+ * This must only be sent when the error is not SatelliteError#ERROR_NONE.
* @param callback If the result is SatelliteError#ERROR_NONE, the callback to receive
* whether the satellite service is supported on the device.
*
@@ -125,14 +126,14 @@
* SatelliteError:REQUEST_NOT_SUPPORTED
* SatelliteError:NO_RESOURCES
*/
- void requestIsSatelliteSupported(in IIntegerConsumer errorCallback,
+ void requestIsSatelliteSupported(in IIntegerConsumer resultCallback,
in IBooleanConsumer callback);
/**
* Request to get the SatelliteCapabilities of the satellite service.
*
- * @param errorCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not SatelliteError#ERROR_NONE.
+ * @param resultCallback The callback to receive the error code result of the operation.
+ * This must only be sent when the error is not SatelliteError#ERROR_NONE.
* @param callback If the result is SatelliteError#ERROR_NONE, the callback to receive
* the SatelliteCapabilities of the satellite service.
*
@@ -146,7 +147,7 @@
* SatelliteError:REQUEST_NOT_SUPPORTED
* SatelliteError:NO_RESOURCES
*/
- void requestSatelliteCapabilities(in IIntegerConsumer errorCallback,
+ void requestSatelliteCapabilities(in IIntegerConsumer resultCallback,
in ISatelliteCapabilitiesConsumer callback);
/**
@@ -154,7 +155,7 @@
* The satellite service should report the satellite pointing info via
* ISatelliteListener#onSatellitePositionChanged as the user device/satellite moves.
*
- * @param errorCallback The callback to receive the error code result of the operation.
+ * @param resultCallback The callback to receive the error code result of the operation.
*
* Valid error codes returned:
* SatelliteError:ERROR_NONE
@@ -166,13 +167,13 @@
* SatelliteError:REQUEST_NOT_SUPPORTED
* SatelliteError:NO_RESOURCES
*/
- void startSendingSatellitePointingInfo(in IIntegerConsumer errorCallback);
+ void startSendingSatellitePointingInfo(in IIntegerConsumer resultCallback);
/**
* User stopped pointing to the satellite.
* The satellite service should stop reporting satellite pointing info to the framework.
*
- * @param errorCallback The callback to receive the error code result of the operation.
+ * @param resultCallback The callback to receive the error code result of the operation.
*
* Valid error codes returned:
* SatelliteError:ERROR_NONE
@@ -184,28 +185,7 @@
* SatelliteError:REQUEST_NOT_SUPPORTED
* SatelliteError:NO_RESOURCES
*/
- void stopSendingSatellitePointingInfo(in IIntegerConsumer errorCallback);
-
- /**
- * Request to get the maximum number of characters per MO text message on satellite.
- *
- * @param errorCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not SatelliteError#ERROR_NONE.
- * @param callback If the result is SatelliteError#ERROR_NONE, the callback to receive
- * the maximum number of characters per MO text message on satellite.
- *
- * Valid error codes returned:
- * SatelliteError:ERROR_NONE
- * SatelliteError:SERVICE_ERROR
- * SatelliteError:MODEM_ERROR
- * SatelliteError:INVALID_MODEM_STATE
- * SatelliteError:INVALID_ARGUMENTS
- * SatelliteError:RADIO_NOT_AVAILABLE
- * SatelliteError:REQUEST_NOT_SUPPORTED
- * SatelliteError:NO_RESOURCES
- */
- void requestMaxCharactersPerMOTextMessage(in IIntegerConsumer errorCallback,
- in IIntegerConsumer callback);
+ void stopSendingSatellitePointingInfo(in IIntegerConsumer resultCallback);
/**
* Provision the device with a satellite provider.
@@ -214,7 +194,8 @@
*
* @param token The token to be used as a unique identifier for provisioning with satellite
* gateway.
- * @param errorCallback The callback to receive the error code result of the operation.
+ * @param regionId The region ID for the device's current location.
+ * @param resultCallback The callback to receive the error code result of the operation.
*
* Valid error codes returned:
* SatelliteError:ERROR_NONE
@@ -229,7 +210,8 @@
* SatelliteError:REQUEST_ABORTED
* SatelliteError:NETWORK_TIMEOUT
*/
- void provisionSatelliteService(in String token, in IIntegerConsumer errorCallback);
+ void provisionSatelliteService(in String token, in String regionId,
+ in IIntegerConsumer resultCallback);
/**
* Deprovision the device with the satellite provider.
@@ -237,7 +219,7 @@
* Once deprovisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report false.
*
* @param token The token of the device/subscription to be deprovisioned.
- * @param errorCallback The callback to receive the error code result of the operation.
+ * @param resultCallback The callback to receive the error code result of the operation.
*
* Valid error codes returned:
* SatelliteError:ERROR_NONE
@@ -252,13 +234,13 @@
* SatelliteError:REQUEST_ABORTED
* SatelliteError:NETWORK_TIMEOUT
*/
- void deprovisionSatelliteService(in String token, in IIntegerConsumer errorCallback);
+ void deprovisionSatelliteService(in String token, in IIntegerConsumer resultCallback);
/**
* Request to get whether this device is provisioned with a satellite provider.
*
- * @param errorCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not SatelliteError#ERROR_NONE.
+ * @param resultCallback The callback to receive the error code result of the operation.
+ * This must only be sent when the error is not SatelliteError#ERROR_NONE.
* @param callback If the result is SatelliteError#ERROR_NONE, the callback to receive
* whether this device is provisioned with a satellite provider.
*
@@ -272,7 +254,7 @@
* SatelliteError:REQUEST_NOT_SUPPORTED
* SatelliteError:NO_RESOURCES
*/
- void requestIsSatelliteProvisioned(in IIntegerConsumer errorCallback,
+ void requestIsSatelliteProvisioned(in IIntegerConsumer resultCallback,
in IBooleanConsumer callback);
/**
@@ -280,7 +262,7 @@
* The satellite service should check if there are any pending datagrams to be received over
* satellite and report them via ISatelliteListener#onSatelliteDatagramsReceived.
*
- * @param errorCallback The callback to receive the error code result of the operation.
+ * @param resultCallback The callback to receive the error code result of the operation.
*
* Valid error codes returned:
* SatelliteError:ERROR_NONE
@@ -297,15 +279,14 @@
* SatelliteError:SATELLITE_NOT_REACHABLE
* SatelliteError:NOT_AUTHORIZED
*/
- void pollPendingSatelliteDatagrams(in IIntegerConsumer errorCallback);
+ void pollPendingSatelliteDatagrams(in IIntegerConsumer resultCallback);
/**
* Send datagram over satellite.
*
* @param datagram Datagram to send in byte format.
- * @param isDemoMode Whether demo mode is enabled.
* @param isEmergency Whether this is an emergency datagram.
- * @param errorCallback The callback to receive the error code result of the operation.
+ * @param resultCallback The callback to receive the error code result of the operation.
*
* Valid error codes returned:
* SatelliteError:ERROR_NONE
@@ -323,16 +304,16 @@
* SatelliteError:SATELLITE_NOT_REACHABLE
* SatelliteError:NOT_AUTHORIZED
*/
- void sendSatelliteDatagram(in SatelliteDatagram datagram, in boolean isDemoMode,
- in boolean isEmergency, in IIntegerConsumer errorCallback);
+ void sendSatelliteDatagram(in SatelliteDatagram datagram, in boolean isEmergency,
+ in IIntegerConsumer resultCallback);
/**
* Request the current satellite modem state.
* The satellite service should report the current satellite modem state via
* ISatelliteListener#onSatelliteModemStateChanged.
*
- * @param errorCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not SatelliteError#ERROR_NONE.
+ * @param resultCallback The callback to receive the error code result of the operation.
+ * This must only be sent when the error is not SatelliteError#ERROR_NONE.
* @param callback If the result is SatelliteError#ERROR_NONE, the callback to receive
* the current satellite modem state.
*
@@ -346,14 +327,14 @@
* SatelliteError:REQUEST_NOT_SUPPORTED
* SatelliteError:NO_RESOURCES
*/
- void requestSatelliteModemState(in IIntegerConsumer errorCallback,
+ void requestSatelliteModemState(in IIntegerConsumer resultCallback,
in IIntegerConsumer callback);
/**
* Request to get whether satellite communication is allowed for the current location.
*
- * @param errorCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not SatelliteError#ERROR_NONE.
+ * @param resultCallback The callback to receive the error code result of the operation.
+ * This must only be sent when the error is not SatelliteError#ERROR_NONE.
* @param callback If the result is SatelliteError#ERROR_NONE, the callback to receive
* whether satellite communication is allowed for the current location.
*
@@ -368,15 +349,15 @@
* SatelliteError:NO_RESOURCES
*/
void requestIsSatelliteCommunicationAllowedForCurrentLocation(
- in IIntegerConsumer errorCallback, in IBooleanConsumer callback);
+ in IIntegerConsumer resultCallback, in IBooleanConsumer callback);
/**
* Request to get the time after which the satellite will be visible. This is an int
* representing the duration in seconds after which the satellite will be visible.
* This will return 0 if the satellite is currently visible.
*
- * @param errorCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not SatelliteError#ERROR_NONE.
+ * @param resultCallback The callback to receive the error code result of the operation.
+ * This must only be sent when the error is not SatelliteError#ERROR_NONE.
* @param callback If the result is SatelliteError#ERROR_NONE, the callback to receive
* the time after which the satellite will be visible.
*
@@ -390,6 +371,6 @@
* SatelliteError:REQUEST_NOT_SUPPORTED
* SatelliteError:NO_RESOURCES
*/
- void requestTimeForNextSatelliteVisibility(in IIntegerConsumer errorCallback,
+ void requestTimeForNextSatelliteVisibility(in IIntegerConsumer resultCallback,
in IIntegerConsumer callback);
}
diff --git a/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl b/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl
index d966868..5e69215 100644
--- a/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl
@@ -43,10 +43,8 @@
/**
* Indicates that the satellite has pending datagrams for the device to be pulled.
- *
- * @param count Number of pending datagrams.
*/
- void onPendingDatagramCount(in int count);
+ void onPendingDatagrams();
/**
* Indicates that the satellite pointing input has changed.
@@ -61,11 +59,4 @@
* @param state The current satellite modem state.
*/
void onSatelliteModemStateChanged(in SatelliteModemState state);
-
- /**
- * Indicates that the satellite radio technology has changed.
- *
- * @param technology The current satellite radio technology.
- */
- void onSatelliteRadioTechnologyChanged(in NTRadioTechnology technology);
}
diff --git a/telephony/java/android/telephony/satellite/stub/PointingInfo.aidl b/telephony/java/android/telephony/satellite/stub/PointingInfo.aidl
index 83392dd..52a36d8 100644
--- a/telephony/java/android/telephony/satellite/stub/PointingInfo.aidl
+++ b/telephony/java/android/telephony/satellite/stub/PointingInfo.aidl
@@ -29,21 +29,4 @@
* Satellite elevation in degrees.
*/
float satelliteElevation;
-
- /**
- * Antenna azimuth in degrees.
- */
- float antennaAzimuth;
-
- /**
- * Angle of rotation about the x axis. This value represents the angle between a plane
- * parallel to the device's screen and a plane parallel to the ground.
- */
- float antennaPitch;
-
- /**
- * Angle of rotation about the y axis. This value represents the angle between a plane
- * perpendicular to the device's screen and a plane parallel to the ground.
- */
- float antennaRoll;
}
diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteCapabilities.aidl b/telephony/java/android/telephony/satellite/stub/SatelliteCapabilities.aidl
index 10c2ea3..cd69da1 100644
--- a/telephony/java/android/telephony/satellite/stub/SatelliteCapabilities.aidl
+++ b/telephony/java/android/telephony/satellite/stub/SatelliteCapabilities.aidl
@@ -28,18 +28,12 @@
NTRadioTechnology[] supportedRadioTechnologies;
/**
- * Whether satellite modem is always on.
- * This indicates the power impact of keeping it on is very minimal.
- */
- boolean isAlwaysOn;
-
- /**
* Whether UE needs to point to a satellite to send and receive data.
*/
- boolean needsPointingToSatellite;
+ boolean isPointingRequired;
/**
- * Whether UE needs a separate SIM profile to communicate with the satellite network.
+ * The maximum number of bytes per datagram that can be sent over satellite.
*/
- boolean needsSeparateSimProfile;
+ int maxBytesPerOutgoingDatagram;
}
diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
index 711dcbe..debb394 100644
--- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
+++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
@@ -70,20 +70,21 @@
}
@Override
- public void requestSatelliteListeningEnabled(boolean enable, boolean isDemoMode,
- int timeout, IIntegerConsumer errorCallback) throws RemoteException {
+ public void requestSatelliteListeningEnabled(boolean enable, int timeout,
+ IIntegerConsumer errorCallback) throws RemoteException {
executeMethodAsync(
() -> SatelliteImplBase.this
- .requestSatelliteListeningEnabled(
- enable, isDemoMode, timeout, errorCallback),
+ .requestSatelliteListeningEnabled(enable, timeout, errorCallback),
"requestSatelliteListeningEnabled");
}
@Override
- public void requestSatelliteEnabled(boolean enable, IIntegerConsumer errorCallback)
- throws RemoteException {
+ public void requestSatelliteEnabled(boolean enableSatellite, boolean enableDemoMode,
+ IIntegerConsumer errorCallback) throws RemoteException {
executeMethodAsync(
- () -> SatelliteImplBase.this.requestSatelliteEnabled(enable, errorCallback),
+ () -> SatelliteImplBase.this
+ .requestSatelliteEnabled(
+ enableSatellite, enableDemoMode, errorCallback),
"requestSatelliteEnabled");
}
@@ -131,19 +132,11 @@
}
@Override
- public void requestMaxCharactersPerMOTextMessage(IIntegerConsumer errorCallback,
- IIntegerConsumer callback) throws RemoteException {
+ public void provisionSatelliteService(String token, String regionId,
+ IIntegerConsumer errorCallback) throws RemoteException {
executeMethodAsync(
() -> SatelliteImplBase.this
- .requestMaxCharactersPerMOTextMessage(errorCallback, callback),
- "requestMaxCharactersPerMOTextMessage");
- }
-
- @Override
- public void provisionSatelliteService(String token, IIntegerConsumer errorCallback)
- throws RemoteException {
- executeMethodAsync(
- () -> SatelliteImplBase.this.provisionSatelliteService(token, errorCallback),
+ .provisionSatelliteService(token, regionId, errorCallback),
"provisionSatelliteService");
}
@@ -173,12 +166,11 @@
}
@Override
- public void sendSatelliteDatagram(SatelliteDatagram datagram, boolean isDemoMode,
- boolean isEmergency, IIntegerConsumer errorCallback) throws RemoteException {
+ public void sendSatelliteDatagram(SatelliteDatagram datagram, boolean isEmergency,
+ IIntegerConsumer errorCallback) throws RemoteException {
executeMethodAsync(
() -> SatelliteImplBase.this
- .sendSatelliteDatagram(
- datagram, isDemoMode, isEmergency, errorCallback),
+ .sendSatelliteDatagram(datagram, isEmergency, errorCallback),
"sendSatelliteDatagram");
}
@@ -249,7 +241,6 @@
* Listening mode allows the satellite service to listen for incoming pages.
*
* @param enable True to enable satellite listening mode and false to disable.
- * @param isDemoMode Whether demo mode is enabled.
* @param timeout How long the satellite modem should wait for the next incoming page before
* disabling listening mode.
* @param errorCallback The callback to receive the error code result of the operation.
@@ -264,17 +255,18 @@
* SatelliteError:REQUEST_NOT_SUPPORTED
* SatelliteError:NO_RESOURCES
*/
- public void requestSatelliteListeningEnabled(boolean enable, boolean isDemoMode, int timeout,
+ public void requestSatelliteListeningEnabled(boolean enable, int timeout,
@NonNull IIntegerConsumer errorCallback) {
// stub implementation
}
/**
- * Request to enable or disable the satellite modem. If the satellite modem is enabled,
- * this will also disable the cellular modem, and if the satellite modem is disabled,
- * this will also re-enable the cellular modem.
+ * Request to enable or disable the satellite modem and demo mode. If the satellite modem is
+ * enabled, this may also disable the cellular modem, and if the satellite modem is disabled,
+ * this may also re-enable the cellular modem.
*
- * @param enable True to enable the satellite modem and false to disable.
+ * @param enableSatellite True to enable the satellite modem and false to disable.
+ * @param enableDemoMode True to enable demo mode and false to disable.
* @param errorCallback The callback to receive the error code result of the operation.
*
* Valid error codes returned:
@@ -287,7 +279,8 @@
* SatelliteError:REQUEST_NOT_SUPPORTED
* SatelliteError:NO_RESOURCES
*/
- public void requestSatelliteEnabled(boolean enable, @NonNull IIntegerConsumer errorCallback) {
+ public void requestSatelliteEnabled(boolean enableSatellite, boolean enableDemoMode,
+ @NonNull IIntegerConsumer errorCallback) {
// stub implementation
}
@@ -402,35 +395,13 @@
}
/**
- * Request to get the maximum number of characters per MO text message on satellite.
- *
- * @param errorCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not SatelliteError#ERROR_NONE.
- * @param callback If the result is SatelliteError#ERROR_NONE, the callback to receive
- * the maximum number of characters per MO text message on satellite.
- *
- * Valid error codes returned:
- * SatelliteError:ERROR_NONE
- * SatelliteError:SERVICE_ERROR
- * SatelliteError:MODEM_ERROR
- * SatelliteError:INVALID_MODEM_STATE
- * SatelliteError:INVALID_ARGUMENTS
- * SatelliteError:RADIO_NOT_AVAILABLE
- * SatelliteError:REQUEST_NOT_SUPPORTED
- * SatelliteError:NO_RESOURCES
- */
- public void requestMaxCharactersPerMOTextMessage(@NonNull IIntegerConsumer errorCallback,
- @NonNull IIntegerConsumer callback) {
- // stub implementation
- }
-
- /**
* Provision the device with a satellite provider.
* This is needed if the provider allows dynamic registration.
* Once provisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report true.
*
* @param token The token to be used as a unique identifier for provisioning with satellite
* gateway.
+ * @param regionId The region ID for the device's current location.
* @param errorCallback The callback to receive the error code result of the operation.
*
* Valid error codes returned:
@@ -446,7 +417,7 @@
* SatelliteError:REQUEST_ABORTED
* SatelliteError:NETWORK_TIMEOUT
*/
- public void provisionSatelliteService(@NonNull String token,
+ public void provisionSatelliteService(@NonNull String token, @NonNull String regionId,
@NonNull IIntegerConsumer errorCallback) {
// stub implementation
}
@@ -530,7 +501,6 @@
* Send datagram over satellite.
*
* @param datagram Datagram to send in byte format.
- * @param isDemoMode Whether demo mode is enabled.
* @param isEmergency Whether this is an emergency datagram.
* @param errorCallback The callback to receive the error code result of the operation.
*
@@ -550,8 +520,8 @@
* SatelliteError:SATELLITE_NOT_REACHABLE
* SatelliteError:NOT_AUTHORIZED
*/
- public void sendSatelliteDatagram(@NonNull SatelliteDatagram datagram, boolean isDemoMode,
- boolean isEmergency, @NonNull IIntegerConsumer errorCallback) {
+ public void sendSatelliteDatagram(@NonNull SatelliteDatagram datagram, boolean isEmergency,
+ @NonNull IIntegerConsumer errorCallback) {
// stub implementation
}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 323fa6f..d0de3ac 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -68,7 +68,7 @@
import android.telephony.ims.aidl.IImsRegistrationCallback;
import android.telephony.ims.aidl.IRcsConfigCallback;
import android.telephony.satellite.ISatelliteDatagramCallback;
-import android.telephony.satellite.ISatellitePositionUpdateCallback;
+import android.telephony.satellite.ISatelliteTransmissionUpdateCallback;
import android.telephony.satellite.ISatelliteProvisionStateCallback;
import android.telephony.satellite.ISatelliteStateCallback;
import android.telephony.satellite.SatelliteCapabilities;
@@ -2726,11 +2726,13 @@
*
* @param subId The subId of the subscription to enable or disable the satellite modem for.
* @param enable True to enable the satellite modem and false to disable.
- * @param callback The callback to get the error code of the request.
+ * @param isDemoModeEnabled True if demo mode is enabled and false otherwise.
+ * @param callback The callback to get the result of the request.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- void requestSatelliteEnabled(int subId, boolean enable, in IIntegerConsumer callback);
+ void requestSatelliteEnabled(int subId, boolean enable, boolean isDemoModeEnabled,
+ in IIntegerConsumer callback);
/**
* Request to get whether the satellite modem is enabled.
@@ -2744,17 +2746,6 @@
void requestIsSatelliteEnabled(int subId, in ResultReceiver receiver);
/**
- * Request to enable or disable the satellite service demo mode.
- *
- * @param subId The subId of the subscription to enable or disable the satellite demo mode for.
- * @param enable True to enable the satellite demo mode and false to disable.
- * @param callback The callback to get the error code of the request.
- */
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
- + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- void requestSatelliteDemoModeEnabled(int subId, boolean enable, in IIntegerConsumer callback);
-
- /**
* Request to get whether the satellite service demo mode is enabled.
*
* @param subId The subId of the subscription to request whether the satellite demo mode is
@@ -2764,7 +2755,7 @@
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- void requestIsSatelliteDemoModeEnabled(int subId, in ResultReceiver receiver);
+ void requestIsDemoModeEnabled(int subId, in ResultReceiver receiver);
/**
* Request to get whether the satellite service is supported on the device.
@@ -2787,39 +2778,28 @@
void requestSatelliteCapabilities(int subId, in ResultReceiver receiver);
/**
- * Start receiving satellite pointing updates.
+ * Start receiving satellite transmission updates.
*
- * @param subId The subId of the subscription to stop satellite position updates for.
- * @param errorCallback The callback to get the error code of the request.
- * @param callback The callback to handle position updates.
+ * @param subId The subId of the subscription to stop satellite transmission updates for.
+ * @param resultCallback The callback to get the result of the request.
+ * @param callback The callback to handle transmission updates.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- void startSatellitePositionUpdates(int subId, in IIntegerConsumer errorCallback,
- in ISatellitePositionUpdateCallback callback);
+ void startSatelliteTransmissionUpdates(int subId, in IIntegerConsumer resultCallback,
+ in ISatelliteTransmissionUpdateCallback callback);
/**
- * Stop receiving satellite pointing updates.
+ * Stop receiving satellite transmission updates.
*
- * @param subId The subId of the subscritpion to stop satellite position updates for.
- * @param errorCallback The callback to get the error code of the request.
- * @param callback The callback that was passed to startSatellitePositionUpdates.
+ * @param subId The subId of the subscritpion to stop satellite transmission updates for.
+ * @param resultCallback The callback to get the result of the request.
+ * @param callback The callback that was passed to startSatelliteTransmissionUpdates.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- void stopSatellitePositionUpdates(int subId, in IIntegerConsumer errorCallback,
- in ISatellitePositionUpdateCallback callback);
-
- /**
- * Request to get the maximum number of bytes per datagram that can be sent to satellite.
- *
- * @param subId The subId of the subscription to get the maximum number of characters for.
- * @param receiver Result receiver to get the error code of the request and the requested
- * maximum number of bytes per datagram that can be sent to satellite.
- */
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
- + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- void requestMaxSizePerSendingDatagram(int subId, in ResultReceiver receiver);
+ void stopSatelliteTransmissionUpdates(int subId, in IIntegerConsumer resultCallback,
+ in ISatelliteTransmissionUpdateCallback callback);
/**
* Register the subscription with a satellite provider.
@@ -2828,13 +2808,14 @@
* @param subId The subId of the subscription to be provisioned.
* @param token The token to be used as a unique identifier for provisioning with satellite
* gateway.
- * @param callback The callback to get the error code of the request.
+ * @param regionId The region ID for the device's current location.
+ * @param callback The callback to get the result of the request.
*
* @return The signal transport used by callers to cancel the provision request.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- ICancellationSignal provisionSatelliteService(int subId, in String token,
+ ICancellationSignal provisionSatelliteService(int subId, in String token, in String regionId,
in IIntegerConsumer callback);
/**
@@ -2846,7 +2827,7 @@
*
* @param subId The subId of the subscription to be deprovisioned.
* @param token The token of the device/subscription to be deprovisioned.
- * @param callback The callback to get the error code of the request.
+ * @param callback The callback to get the result of the request.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
@@ -2916,15 +2897,13 @@
* Register to receive incoming datagrams over satellite.
*
* @param subId The subId of the subscription to register for incoming satellite datagrams.
- * @param datagramType Type of datagram.
* @param callback The callback to handle the incoming datagrams.
*
* @return The {@link SatelliteError} result of the operation.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- int registerForSatelliteDatagram(
- int subId, int datagramType, ISatelliteDatagramCallback callback);
+ int registerForSatelliteDatagram(int subId, ISatelliteDatagramCallback callback);
/**
* Unregister to stop receiving incoming datagrams over satellite.
@@ -2941,7 +2920,7 @@
* Poll pending satellite datagrams over satellite.
*
* @param subId The subId of the subscription used for receiving datagrams.
- * @param callback The callback to get the error code of the request.
+ * @param callback The callback to get the result of the request.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
@@ -2955,7 +2934,7 @@
* @param datagram Datagram to send over satellite.
* @param needFullScreenPointingUI this is used to indicate pointingUI app to open in
* full screen mode.
- * @param callback The callback to get the error code of the request.
+ * @param callback The callback to get the result of the request.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
diff --git a/tests/EnforcePermission/Android.bp b/tests/EnforcePermission/Android.bp
new file mode 100644
index 0000000..719a898
--- /dev/null
+++ b/tests/EnforcePermission/Android.bp
@@ -0,0 +1,22 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "frameworks-enforce-permission-test-aidl",
+ srcs: ["aidl/**/*.aidl"],
+}
diff --git a/tests/EnforcePermission/TEST_MAPPING b/tests/EnforcePermission/TEST_MAPPING
new file mode 100644
index 0000000..a1bf42a
--- /dev/null
+++ b/tests/EnforcePermission/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "EnforcePermissionTests"
+ }
+ ]
+}
diff --git a/tests/EnforcePermission/aidl/android/tests/enforcepermission/INested.aidl b/tests/EnforcePermission/aidl/android/tests/enforcepermission/INested.aidl
new file mode 100644
index 0000000..1eb773d
--- /dev/null
+++ b/tests/EnforcePermission/aidl/android/tests/enforcepermission/INested.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tests.enforcepermission;
+
+interface INested {
+ @EnforcePermission("ACCESS_NETWORK_STATE")
+ void ProtectedByAccessNetworkState();
+
+ @EnforcePermission("READ_SYNC_SETTINGS")
+ void ProtectedByReadSyncSettings();
+}
diff --git a/tests/EnforcePermission/aidl/android/tests/enforcepermission/IProtected.aidl b/tests/EnforcePermission/aidl/android/tests/enforcepermission/IProtected.aidl
new file mode 100644
index 0000000..18e3aec
--- /dev/null
+++ b/tests/EnforcePermission/aidl/android/tests/enforcepermission/IProtected.aidl
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tests.enforcepermission;
+
+interface IProtected {
+ @EnforcePermission("INTERNET")
+ void ProtectedByInternet();
+
+ @EnforcePermission("VIBRATE")
+ void ProtectedByVibrate();
+
+ @EnforcePermission("INTERNET")
+ void ProtectedByInternetAndVibrateImplicitly();
+
+ @EnforcePermission("INTERNET")
+ void ProtectedByInternetAndAccessNetworkStateImplicitly();
+
+ @EnforcePermission("INTERNET")
+ void ProtectedByInternetAndReadSyncSettingsImplicitly();
+}
diff --git a/tests/EnforcePermission/service-app/Android.bp b/tests/EnforcePermission/service-app/Android.bp
new file mode 100644
index 0000000..a4ac1d7
--- /dev/null
+++ b/tests/EnforcePermission/service-app/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ // 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_test_helper_app {
+ name: "EnforcePermissionTestHelper",
+ srcs: [
+ "src/**/*.java",
+ ":frameworks-enforce-permission-test-aidl",
+ ],
+ platform_apis: true,
+ certificate: "platform",
+}
diff --git a/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r1000.xml b/tests/EnforcePermission/service-app/AndroidManifest.xml
similarity index 64%
copy from services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r1000.xml
copy to tests/EnforcePermission/service-app/AndroidManifest.xml
index 9bf9254..ddafe15 100644
--- a/services/tests/PackageManagerServiceTests/apks/install_uses_sdk/AndroidManifest-r1000.xml
+++ b/tests/EnforcePermission/service-app/AndroidManifest.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2010 The Android Open Source Project
+<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,13 +14,14 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.frameworks.servicestests.install_uses_sdk">
-
- <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="29">
- <!-- This will fail to install, because minExtensionVersion is not met -->
- <extension-sdk android:sdkVersion="30" android:minExtensionVersion="1000" />
- </uses-sdk>
-
+ package="android.tests.enforcepermission.service">
<application>
+ <service
+ android:name=".TestService"
+ android:exported="true" />
+
+ <service
+ android:name=".NestedTestService"
+ android:exported="true" />
</application>
</manifest>
diff --git a/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/NestedTestService.java b/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/NestedTestService.java
new file mode 100644
index 0000000..7879a12
--- /dev/null
+++ b/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/NestedTestService.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tests.enforcepermission.service;
+
+import android.annotation.EnforcePermission;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.tests.enforcepermission.INested;
+import android.util.Log;
+
+public class NestedTestService extends Service {
+ private static final String TAG = "EnforcePermission.NestedTestService";
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.i(TAG, "onBind");
+ return mBinder;
+ }
+
+ private final INested.Stub mBinder = new INested.Stub() {
+ @Override
+ @EnforcePermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+ public void ProtectedByAccessNetworkState() {
+ ProtectedByAccessNetworkState_enforcePermission();
+ }
+
+ @Override
+ @EnforcePermission(android.Manifest.permission.READ_SYNC_SETTINGS)
+ public void ProtectedByReadSyncSettings() {
+ ProtectedByReadSyncSettings_enforcePermission();
+ }
+ };
+}
diff --git a/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/TestService.java b/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/TestService.java
new file mode 100644
index 0000000..e9b897d
--- /dev/null
+++ b/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/TestService.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tests.enforcepermission.service;
+
+import android.annotation.EnforcePermission;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.tests.enforcepermission.INested;
+import android.tests.enforcepermission.IProtected;
+import android.util.Log;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class TestService extends Service {
+
+ private static final String TAG = "EnforcePermission.TestService";
+ private volatile ServiceConnection mNestedServiceConnection;
+
+ @Override
+ public void onCreate() {
+ mNestedServiceConnection = new ServiceConnection();
+ Intent intent = new Intent(this, NestedTestService.class);
+ boolean bound = bindService(intent, mNestedServiceConnection, Context.BIND_AUTO_CREATE);
+ if (!bound) {
+ Log.wtf(TAG, "bindService() on NestedTestService failed");
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ unbindService(mNestedServiceConnection);
+ }
+
+ private static final class ServiceConnection implements android.content.ServiceConnection {
+ private volatile CompletableFuture<INested> mFuture = new CompletableFuture<>();
+
+ public INested get() {
+ try {
+ return mFuture.get(1, TimeUnit.SECONDS);
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ throw new RuntimeException("Unable to reach NestedTestService: " + e.getMessage());
+ }
+ }
+
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mFuture.complete(INested.Stub.asInterface(service));
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ mFuture = new CompletableFuture<>();
+ }
+ };
+
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ private final IProtected.Stub mBinder = new IProtected.Stub() {
+ @Override
+ @EnforcePermission(android.Manifest.permission.INTERNET)
+ public void ProtectedByInternet() {
+ ProtectedByInternet_enforcePermission();
+ }
+
+ @Override
+ @EnforcePermission(android.Manifest.permission.VIBRATE)
+ public void ProtectedByVibrate() {
+ ProtectedByVibrate_enforcePermission();
+ }
+
+ @Override
+ @EnforcePermission(android.Manifest.permission.INTERNET)
+ public void ProtectedByInternetAndVibrateImplicitly() {
+ ProtectedByInternetAndVibrateImplicitly_enforcePermission();
+
+ ProtectedByVibrate();
+ }
+
+ @Override
+ @EnforcePermission(android.Manifest.permission.INTERNET)
+ public void ProtectedByInternetAndAccessNetworkStateImplicitly() throws RemoteException {
+ ProtectedByInternetAndAccessNetworkStateImplicitly_enforcePermission();
+
+ mNestedServiceConnection.get().ProtectedByAccessNetworkState();
+
+ }
+
+ @Override
+ @EnforcePermission(android.Manifest.permission.INTERNET)
+ public void ProtectedByInternetAndReadSyncSettingsImplicitly() throws RemoteException {
+ ProtectedByInternetAndReadSyncSettingsImplicitly_enforcePermission();
+
+ mNestedServiceConnection.get().ProtectedByReadSyncSettings();
+ }
+ };
+}
diff --git a/tests/EnforcePermission/test-app/Android.bp b/tests/EnforcePermission/test-app/Android.bp
new file mode 100644
index 0000000..cd53854
--- /dev/null
+++ b/tests/EnforcePermission/test-app/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "EnforcePermissionTests",
+ srcs: [
+ "src/**/*.java",
+ ":frameworks-enforce-permission-test-aidl",
+ ],
+ static_libs: [
+ "androidx.test.rules",
+ ],
+ libs: [
+ "android.test.base",
+ "android.test.runner",
+ ],
+ data: [
+ ":EnforcePermissionTestHelper",
+ ],
+ platform_apis: true,
+ certificate: "platform",
+ test_suites: ["device-tests"],
+}
diff --git a/tests/EnforcePermission/test-app/AndroidManifest.xml b/tests/EnforcePermission/test-app/AndroidManifest.xml
new file mode 100644
index 0000000..4a0c6a8
--- /dev/null
+++ b/tests/EnforcePermission/test-app/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.tests.enforcepermission.tests">
+
+ <!-- Expected for the tests (not actually used) -->
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+
+ <queries>
+ <package android:name="android.tests.enforcepermission.service" />
+ </queries>
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.tests.enforcepermission.tests"/>
+</manifest>
diff --git a/tests/EnforcePermission/test-app/AndroidTest.xml b/tests/EnforcePermission/test-app/AndroidTest.xml
new file mode 100644
index 0000000..120381a
--- /dev/null
+++ b/tests/EnforcePermission/test-app/AndroidTest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Runs EnforcePermission End-to-End Tests">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="EnforcePermissionTestHelper.apk"/>
+ <option name="test-file-name" value="EnforcePermissionTests.apk"/>
+ <option name="cleanup-apks" value="true" />
+ </target_preparer>
+
+ <option name="test-tag" value="EnforcePermissionTests"/>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="android.tests.enforcepermission.tests"/>
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/>
+ </test>
+</configuration>
diff --git a/tests/EnforcePermission/test-app/src/android/tests/enforcepermission/tests/ServiceTest.java b/tests/EnforcePermission/test-app/src/android/tests/enforcepermission/tests/ServiceTest.java
new file mode 100644
index 0000000..d2a4a03
--- /dev/null
+++ b/tests/EnforcePermission/test-app/src/android/tests/enforcepermission/tests/ServiceTest.java
@@ -0,0 +1,129 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tests.enforcepermission.tests;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.tests.enforcepermission.IProtected;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@RunWith(AndroidJUnit4.class)
+public class ServiceTest {
+
+ private static final String TAG = "EnforcePermission.Tests";
+ private static final String SERVICE_NAME = "android.tests.enforcepermission.service";
+ private static final int SERVICE_TIMEOUT_SEC = 5;
+
+ private Context mContext;
+ private volatile ServiceConnection mServiceConnection;
+
+ @Before
+ public void bindTestService() throws Exception {
+ Log.d(TAG, "bindTestService");
+ mContext = InstrumentationRegistry.getTargetContext();
+ mServiceConnection = new ServiceConnection();
+ Intent intent = new Intent();
+ intent.setClassName(SERVICE_NAME, SERVICE_NAME + ".TestService");
+ assertTrue(mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE));
+ }
+
+ @After
+ public void unbindTestService() throws Exception {
+ mContext.unbindService(mServiceConnection);
+ }
+
+ private static final class ServiceConnection implements android.content.ServiceConnection {
+ private volatile CompletableFuture<IProtected> mFuture = new CompletableFuture<>();
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mFuture.complete(IProtected.Stub.asInterface(service));
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ mFuture = new CompletableFuture<>();
+ }
+
+ public IProtected get() {
+ try {
+ return mFuture.get(SERVICE_TIMEOUT_SEC, TimeUnit.SECONDS);
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ throw new RuntimeException("Unable to reach TestService: " + e.toString());
+ }
+ }
+ }
+
+ @Test
+ public void testImmediatePermissionGranted_succeeds()
+ throws RemoteException {
+ mServiceConnection.get().ProtectedByInternet();
+ }
+
+ @Test
+ public void testImmediatePermissionNotGranted_fails()
+ throws RemoteException {
+ final Exception ex = assertThrows(SecurityException.class,
+ () -> mServiceConnection.get().ProtectedByVibrate());
+ assertThat(ex.getMessage(), containsString("VIBRATE"));
+ }
+
+ @Test
+ public void testImmediatePermissionGrantedButImplicitLocalNotGranted_fails()
+ throws RemoteException {
+ final Exception ex = assertThrows(SecurityException.class,
+ () -> mServiceConnection.get().ProtectedByInternetAndVibrateImplicitly());
+ assertThat(ex.getMessage(), containsString("VIBRATE"));
+ }
+
+ @Test
+ public void testImmediatePermissionGrantedButImplicitNestedNotGranted_fails()
+ throws RemoteException {
+ final Exception ex = assertThrows(SecurityException.class,
+ () -> mServiceConnection.get()
+ .ProtectedByInternetAndAccessNetworkStateImplicitly());
+ assertThat(ex.getMessage(), containsString("ACCESS_NETWORK_STATE"));
+ }
+
+ @Test
+ public void testImmediatePermissionGrantedAndImplicitNestedGranted_succeeds()
+ throws RemoteException {
+ mServiceConnection.get().ProtectedByInternetAndReadSyncSettingsImplicitly();
+ }
+}
diff --git a/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java
index e704cc2..46ebfed 100644
--- a/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java
+++ b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java
@@ -35,6 +35,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.IllformedLocaleException;
import java.util.List;
import java.util.Locale;
@@ -141,14 +142,118 @@
HashMap<String, LocaleInfo> result =
LocaleStore.convertExplicitLocales(locales, supportedLocale);
-
assertEquals("en", result.get("en").getId());
assertEquals("en-US", result.get("en-US").getId());
assertNull(result.get("en-Latn-US"));
}
+ @Test
+ public void getLevelLocales_languageTier_returnAllSupportLanguages() {
+ LocaleList testSupportedLocales =
+ LocaleList.forLanguageTags(
+ "en-US,zh-Hant-TW,ja-JP,en-GB,bn-IN-u-nu-arab,ks-Arab-IN,bn-IN");
+
+ Set<String> ignorableLocales = new HashSet<>();
+ ignorableLocales.add("zh-Hant-HK");
+ LocaleInfo parent = null;
+
+ Set<LocaleInfo> localeInfos = LocaleStore.getLevelLocales(
+ null, ignorableLocales, parent, false, testSupportedLocales);
+
+ assertEquals(5, localeInfos.size());
+ localeInfos.forEach(localeInfo -> {
+ assertTrue(localeInfo.getLocale().getCountry().isEmpty());
+ });
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("en")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("zh-Hant")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("ja")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("bn")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("ks-Arab")));
+ }
+
+ @Test
+ public void getLevelLocales_regionTierAndParentIsEn_returnEnLocales() {
+ LocaleList testSupportedLocales =
+ LocaleList.forLanguageTags(
+ "en-US,en-GB,bn-IN-u-nu-arab,ks-Arab-IN,en-ZA,bn-IN");
+ Set<String> ignorableLocales = new HashSet<>();
+ ignorableLocales.add("zh-Hant-HK");
+ LocaleInfo parent = LocaleStore.fromLocale(Locale.forLanguageTag("en"));
+
+ Set<LocaleInfo> localeInfos = LocaleStore.getLevelLocales(
+ null, ignorableLocales, parent, false, testSupportedLocales);
+
+ assertEquals(3, localeInfos.size());
+ localeInfos.forEach(localeInfo -> {
+ assertEquals("en", localeInfo.getLocale().getLanguage());
+ });
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("en-US")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("en-GB")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("en-ZA")));
+ }
+
+ @Test
+ public void getLevelLocales_numberingTierAndParentIsBnIn_returnBnInLocales() {
+ LocaleList testSupportedLocales =
+ LocaleList.forLanguageTags(
+ "en-US,zh-Hant-TW,bn-IN-u-nu-arab,ks-Arab-IN,en-ZA,bn-IN,bn-IN-u-nu-adlm");
+ Set<String> ignorableLocales = new HashSet<>();
+ ignorableLocales.add("zh-Hant-HK");
+ LocaleInfo parent = LocaleStore.fromLocale(Locale.forLanguageTag("bn"));
+
+ Set<LocaleInfo> localeInfos = LocaleStore.getLevelLocales(
+ null, ignorableLocales, parent, false, testSupportedLocales);
+
+ assertEquals(1, localeInfos.size());
+ assertEquals("bn-IN", localeInfos.iterator().next().getLocale().toLanguageTag());
+ }
+
+ @Test
+ public void getLevelLocales_regionTierAndParentIsBnInAndIgnoreBn_returnEmpty() {
+ LocaleList testSupportedLocales =
+ LocaleList.forLanguageTags(
+ "en-US,zh-Hant-TW,bn-IN-u-nu-arab,ks-Arab-IN,en-ZA,bn-IN,bn-IN-u-nu-adlm");
+ Set<String> ignorableLocales = new HashSet<>();
+ ignorableLocales.add("bn-IN");
+ LocaleInfo parent = LocaleStore.fromLocale(Locale.forLanguageTag("bn-IN"));
+
+ Set<LocaleInfo> localeInfos = LocaleStore.getLevelLocales(
+ null, ignorableLocales, parent, false, testSupportedLocales);
+
+ assertEquals(0, localeInfos.size());
+ }
+
+ @Test
+ public void getLevelLocales_regionTierAndParentIsBnIn_returnBnLocaleFamily() {
+ LocaleList testSupportedLocales =
+ LocaleList.forLanguageTags(
+ "en-US,zh-Hant-TW,bn-IN-u-nu-arab,ks-Arab-IN,en-ZA,bn-IN,bn-IN-u-nu-adlm");
+ Set<String> ignorableLocales = new HashSet<>();
+ ignorableLocales.add("en-US");
+ LocaleInfo parent = LocaleStore.fromLocale(Locale.forLanguageTag("bn-IN"));
+
+ Set<LocaleInfo> localeInfos = LocaleStore.getLevelLocales(
+ null, ignorableLocales, parent, false, testSupportedLocales);
+
+ assertEquals(3, localeInfos.size());
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("bn-IN-u-nu-adlm")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("bn-IN-u-nu-arab")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("bn-IN")));
+ }
+
private ArrayList<LocaleInfo> getFakeSupportedLocales() {
- String[] locales = {"en-US", "zh-Hant-TW", "ja-JP", "en-GB"};
+ String[] locales = {"en-US", "zh-Hant-TW", "ja-JP", "en-GB", "en-US-u-nu-arab"};
ArrayList<LocaleInfo> supportedLocales = new ArrayList<>();
for (String localeTag : locales) {
supportedLocales.add(LocaleStore.fromLocale(Locale.forLanguageTag(localeTag)));