Merge "Define a new permission to guard role-based application executing action"
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index 18ddffb..86e6183 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -330,7 +330,6 @@
* @see JobScheduler#forNamespace(String)
* @return The namespace this job was scheduled in. Will be {@code null} if there was no
* explicit namespace set and this job is therefore in the default namespace.
- * @hide
*/
@Nullable
public String getJobNamespace() {
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index 33668c7..4aec484 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -271,7 +271,6 @@
* but will instead create or update the job inside the current namespace. A JobScheduler
* instance dedicated to a namespace must be used to schedule or update jobs in that namespace.
* @see #getNamespace()
- * @hide
*/
@NonNull
public JobScheduler forNamespace(@NonNull String namespace) {
@@ -282,7 +281,6 @@
* Get the namespace this JobScheduler instance is operating in. A {@code null} value means
* that the app has not specified a namespace for this instance, and it is therefore using the
* default namespace.
- * @hide
*/
@Nullable
public String getNamespace() {
@@ -395,14 +393,21 @@
public abstract void cancel(int jobId);
/**
- * Cancel <em>all</em> jobs that have been scheduled by the calling application.
+ * Cancel all jobs that have been scheduled in the current namespace by the
+ * calling application.
+ *
+ * <p>
+ * Starting with Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, this
+ * will only cancel within the current namespace. If a namespace hasn't been explicitly set
+ * with {@link #forNamespace(String)}, then this will cancel jobs in the default namespace.
+ * To cancel all jobs scheduled by the application,
+ * use {@link #cancelInAllNamespaces()} instead.
*/
public abstract void cancelAll();
/**
* Cancel <em>all</em> jobs that have been scheduled by the calling application, regardless of
* namespace.
- * @hide
*/
public void cancelInAllNamespaces() {
throw new RuntimeException("Not implemented. Must override in a subclass.");
@@ -424,7 +429,6 @@
* If a namespace hasn't been explicitly set with {@link #forNamespace(String)},
* then this will return jobs in the default namespace.
* This includes jobs that are currently started as well as those that are still waiting to run.
- * @hide
*/
@NonNull
public Map<String, List<JobInfo>> getPendingJobsInAllNamespaces() {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index ce7da86..b89337f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -1193,7 +1193,7 @@
completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_CONTENT_TRIGGER));
if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler",
- completedJob.getTag(), getId());
+ getId());
}
try {
mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(), mRunningJob.getSourceUid(),
diff --git a/core/api/current.txt b/core/api/current.txt
index 0627709..a9b9476d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -745,6 +745,7 @@
field public static final int focusableInTouchMode = 16842971; // 0x10100db
field public static final int focusedByDefault = 16844100; // 0x1010544
field @Deprecated public static final int focusedMonthDateColor = 16843587; // 0x1010343
+ field public static final int focusedSearchResultHighlightColor;
field public static final int font = 16844082; // 0x1010532
field public static final int fontFamily = 16843692; // 0x10103ac
field public static final int fontFeatureSettings = 16843959; // 0x10104b7
@@ -920,6 +921,7 @@
field public static final int isAlwaysSyncable = 16843571; // 0x1010333
field public static final int isAsciiCapable = 16843753; // 0x10103e9
field public static final int isAuxiliary = 16843647; // 0x101037f
+ field public static final int isCredential;
field public static final int isDefault = 16843297; // 0x1010221
field public static final int isFeatureSplit = 16844123; // 0x101055b
field public static final int isGame = 16843764; // 0x10103f4
@@ -1355,6 +1357,7 @@
field public static final int searchHintIcon = 16843988; // 0x10104d4
field public static final int searchIcon = 16843907; // 0x1010483
field public static final int searchMode = 16843221; // 0x10101d5
+ field public static final int searchResultHighlightColor;
field public static final int searchSettingsDescription = 16843402; // 0x101028a
field public static final int searchSuggestAuthority = 16843222; // 0x10101d6
field public static final int searchSuggestIntentAction = 16843225; // 0x10101d9
@@ -8563,6 +8566,7 @@
method public int getClipGrantFlags();
method @NonNull public android.os.PersistableBundle getExtras();
method public int getJobId();
+ method @Nullable public String getJobNamespace();
method @Nullable public android.net.Network getNetwork();
method public int getStopReason();
method @NonNull public android.os.Bundle getTransientExtras();
@@ -8596,10 +8600,14 @@
method public boolean canRunLongJobs();
method public abstract void cancel(int);
method public abstract void cancelAll();
+ method public void cancelInAllNamespaces();
method public abstract int enqueue(@NonNull android.app.job.JobInfo, @NonNull android.app.job.JobWorkItem);
+ method @NonNull public android.app.job.JobScheduler forNamespace(@NonNull String);
method @NonNull public abstract java.util.List<android.app.job.JobInfo> getAllPendingJobs();
+ method @Nullable public String getNamespace();
method @Nullable public abstract android.app.job.JobInfo getPendingJob(int);
method public int getPendingJobReason(int);
+ method @NonNull public java.util.Map<java.lang.String,java.util.List<android.app.job.JobInfo>> getPendingJobsInAllNamespaces();
method public abstract int schedule(@NonNull android.app.job.JobInfo);
field public static final int PENDING_JOB_REASON_APP = 1; // 0x1
field public static final int PENDING_JOB_REASON_APP_STANDBY = 2; // 0x2
@@ -11992,7 +12000,7 @@
method @NonNull public int[] getChildSessionIds();
method @NonNull public String[] getNames() throws java.io.IOException;
method public int getParentSessionId();
- method public boolean isKeepApplicationEnabledSetting();
+ method public boolean isApplicationEnabledSettingPersistent();
method public boolean isMultiPackage();
method public boolean isRequestUpdateOwnership();
method public boolean isStaged();
@@ -12047,8 +12055,8 @@
method @NonNull public android.os.UserHandle getUser();
method public boolean hasParentSessionId();
method public boolean isActive();
+ method public boolean isApplicationEnabledSettingPersistent();
method public boolean isCommitted();
- method public boolean isKeepApplicationEnabledSetting();
method public boolean isMultiPackage();
method public boolean isRequestUpdateOwnership();
method public boolean isSealed();
@@ -12078,12 +12086,12 @@
method public void setAppIcon(@Nullable android.graphics.Bitmap);
method public void setAppLabel(@Nullable CharSequence);
method public void setAppPackageName(@Nullable String);
+ method public void setApplicationEnabledSettingPersistent();
method @Deprecated public void setAutoRevokePermissionsMode(boolean);
method public void setInstallLocation(int);
method public void setInstallReason(int);
method public void setInstallScenario(int);
method public void setInstallerPackageName(@Nullable String);
- method public void setKeepApplicationEnabledSetting();
method public void setMultiPackage();
method public void setOriginatingUid(int);
method public void setOriginatingUri(@Nullable android.net.Uri);
@@ -12702,7 +12710,7 @@
field public static final int FLAG_STOP_WITH_TASK = 1; // 0x1
field public static final int FLAG_USE_APP_ZYGOTE = 8; // 0x8
field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CAMERA}, anyOf={android.Manifest.permission.CAMERA}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40
- field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE}, anyOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.CHANGE_NETWORK_STATE, android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, android.Manifest.permission.NFC, android.Manifest.permission.TRANSMIT_IR}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10
+ field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE}, anyOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.CHANGE_NETWORK_STATE, android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, android.Manifest.permission.NFC, android.Manifest.permission.TRANSMIT_IR, android.Manifest.permission.UWB_RANGING}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10
field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1
field @RequiresPermission(android.Manifest.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT) public static final int FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT = 4096; // 0x1000
field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.BODY_SENSORS, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS}) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100
@@ -13366,9 +13374,8 @@
public final class GetCredentialResponse implements android.os.Parcelable {
ctor public GetCredentialResponse(@NonNull android.credentials.Credential);
- ctor public GetCredentialResponse();
method public int describeContents();
- method @Nullable public android.credentials.Credential getCredential();
+ method @NonNull public android.credentials.Credential getCredential();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.credentials.GetCredentialResponse> CREATOR;
}
@@ -18230,7 +18237,7 @@
method public int capture(@NonNull android.hardware.camera2.CaptureRequest, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback) throws android.hardware.camera2.CameraAccessException;
method public void close() throws android.hardware.camera2.CameraAccessException;
method @NonNull public android.hardware.camera2.CameraDevice getDevice();
- method @Nullable public android.util.Pair<java.lang.Long,java.lang.Long> getRealtimeStillCaptureLatency() throws android.hardware.camera2.CameraAccessException;
+ method @Nullable public android.hardware.camera2.CameraExtensionSession.StillCaptureLatency getRealtimeStillCaptureLatency() throws android.hardware.camera2.CameraAccessException;
method public int setRepeatingRequest(@NonNull android.hardware.camera2.CaptureRequest, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback) throws android.hardware.camera2.CameraAccessException;
method public void stopRepeating() throws android.hardware.camera2.CameraAccessException;
}
@@ -18253,6 +18260,12 @@
method public abstract void onConfigured(@NonNull android.hardware.camera2.CameraExtensionSession);
}
+ public static final class CameraExtensionSession.StillCaptureLatency {
+ ctor public CameraExtensionSession.StillCaptureLatency(long, long);
+ method public long getCaptureLatency();
+ method public long getProcessingLatency();
+ }
+
public final class CameraManager {
method @NonNull public android.hardware.camera2.CameraCharacteristics getCameraCharacteristics(@NonNull String) throws android.hardware.camera2.CameraAccessException;
method @NonNull public android.hardware.camera2.CameraExtensionCharacteristics getCameraExtensionCharacteristics(@NonNull String) throws android.hardware.camera2.CameraAccessException;
@@ -28371,24 +28384,15 @@
public final class NfcAdapter {
method public void disableForegroundDispatch(android.app.Activity);
- method @Deprecated public void disableForegroundNdefPush(android.app.Activity);
method public void disableReaderMode(android.app.Activity);
method public void enableForegroundDispatch(android.app.Activity, android.app.PendingIntent, android.content.IntentFilter[], String[][]);
- method @Deprecated public void enableForegroundNdefPush(android.app.Activity, android.nfc.NdefMessage);
method public void enableReaderMode(android.app.Activity, android.nfc.NfcAdapter.ReaderCallback, int, android.os.Bundle);
method public static android.nfc.NfcAdapter getDefaultAdapter(android.content.Context);
method @Nullable public android.nfc.NfcAntennaInfo getNfcAntennaInfo();
method public boolean ignore(android.nfc.Tag, int, android.nfc.NfcAdapter.OnTagRemovedListener, android.os.Handler);
- method @Deprecated public boolean invokeBeam(android.app.Activity);
method public boolean isEnabled();
- method @Deprecated public boolean isNdefPushEnabled();
method public boolean isSecureNfcEnabled();
method public boolean isSecureNfcSupported();
- method @Deprecated public void setBeamPushUris(android.net.Uri[], android.app.Activity);
- method @Deprecated public void setBeamPushUrisCallback(android.nfc.NfcAdapter.CreateBeamUrisCallback, android.app.Activity);
- method @Deprecated public void setNdefPushMessage(android.nfc.NdefMessage, android.app.Activity, android.app.Activity...);
- method @Deprecated public void setNdefPushMessageCallback(android.nfc.NfcAdapter.CreateNdefMessageCallback, android.app.Activity, android.app.Activity...);
- method @Deprecated public void setOnNdefPushCompleteCallback(android.nfc.NfcAdapter.OnNdefPushCompleteCallback, android.app.Activity, android.app.Activity...);
field public static final String ACTION_ADAPTER_STATE_CHANGED = "android.nfc.action.ADAPTER_STATE_CHANGED";
field public static final String ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED";
field @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public static final String ACTION_PREFERRED_PAYMENT_CHANGED = "android.nfc.action.PREFERRED_PAYMENT_CHANGED";
@@ -28420,18 +28424,6 @@
field public static final int STATE_TURNING_ON = 2; // 0x2
}
- @Deprecated public static interface NfcAdapter.CreateBeamUrisCallback {
- method @Deprecated public android.net.Uri[] createBeamUris(android.nfc.NfcEvent);
- }
-
- @Deprecated public static interface NfcAdapter.CreateNdefMessageCallback {
- method @Deprecated public android.nfc.NdefMessage createNdefMessage(android.nfc.NfcEvent);
- }
-
- @Deprecated public static interface NfcAdapter.OnNdefPushCompleteCallback {
- method @Deprecated public void onNdefPushComplete(android.nfc.NfcEvent);
- }
-
public static interface NfcAdapter.OnTagRemovedListener {
method public void onTagRemoved();
}
@@ -36405,7 +36397,6 @@
field public static final String ACTION_MANAGE_ALL_SIM_PROFILES_SETTINGS = "android.settings.MANAGE_ALL_SIM_PROFILES_SETTINGS";
field public static final String ACTION_MANAGE_APPLICATIONS_SETTINGS = "android.settings.MANAGE_APPLICATIONS_SETTINGS";
field public static final String ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION = "android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION";
- field public static final String ACTION_MANAGE_APP_LONG_RUNNING_JOBS = "android.settings.MANAGE_APP_LONG_RUNNING_JOBS";
field public static final String ACTION_MANAGE_DEFAULT_APPS_SETTINGS = "android.settings.MANAGE_DEFAULT_APPS_SETTINGS";
field public static final String ACTION_MANAGE_OVERLAY_PERMISSION = "android.settings.action.MANAGE_OVERLAY_PERMISSION";
field public static final String ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING = "android.settings.MANAGE_SUPERVISOR_RESTRICTED_SETTING";
@@ -40529,7 +40520,9 @@
method public android.os.IBinder onBind(android.content.Intent);
method @NonNull public java.util.Set<java.lang.String> onGetSupportedVoiceActions(@NonNull java.util.Set<java.lang.String>);
method public void onLaunchVoiceAssistFromKeyguard();
+ method public void onPrepareToShowSession(@NonNull android.os.Bundle, int);
method public void onReady();
+ method public void onShowSessionFailed();
method public void onShutdown();
method public void setDisabledShowContext(int);
method public final void setUiHints(@NonNull android.os.Bundle);
@@ -50570,6 +50563,7 @@
field public static final int AXIS_GENERIC_7 = 38; // 0x26
field public static final int AXIS_GENERIC_8 = 39; // 0x27
field public static final int AXIS_GENERIC_9 = 40; // 0x28
+ field public static final int AXIS_GESTURE_PINCH_SCALE_FACTOR = 52; // 0x34
field public static final int AXIS_GESTURE_SCROLL_X_DISTANCE = 50; // 0x32
field public static final int AXIS_GESTURE_SCROLL_Y_DISTANCE = 51; // 0x33
field public static final int AXIS_GESTURE_X_OFFSET = 48; // 0x30
@@ -50610,6 +50604,7 @@
field public static final int CLASSIFICATION_AMBIGUOUS_GESTURE = 1; // 0x1
field public static final int CLASSIFICATION_DEEP_PRESS = 2; // 0x2
field public static final int CLASSIFICATION_NONE = 0; // 0x0
+ field public static final int CLASSIFICATION_PINCH = 5; // 0x5
field public static final int CLASSIFICATION_TWO_FINGER_SWIPE = 3; // 0x3
field @NonNull public static final android.os.Parcelable.Creator<android.view.MotionEvent> CREATOR;
field public static final int EDGE_BOTTOM = 2; // 0x2
@@ -51426,6 +51421,7 @@
method public boolean isAutoHandwritingEnabled();
method public boolean isClickable();
method public boolean isContextClickable();
+ method public boolean isCredential();
method public boolean isDirty();
method @Deprecated public boolean isDrawingCacheEnabled();
method public boolean isDuplicateParentStateEnabled();
@@ -51659,6 +51655,7 @@
method public void setImportantForAccessibility(int);
method public void setImportantForAutofill(int);
method public void setImportantForContentCapture(int);
+ method public void setIsCredential(boolean);
method public void setKeepScreenOn(boolean);
method public void setKeyboardNavigationCluster(boolean);
method public void setLabelFor(@IdRes int);
@@ -59267,6 +59264,7 @@
method public void append(CharSequence, int, int);
method public void beginBatchEdit();
method public boolean bringPointIntoView(int);
+ method public boolean bringPointIntoView(@IntRange(from=0) int, boolean);
method public void clearComposingText();
method public void debug(int);
method public boolean didTouchFocusSelect();
@@ -59304,6 +59302,8 @@
method public int getExtendedPaddingTop();
method public android.text.InputFilter[] getFilters();
method public int getFirstBaselineToTopHeight();
+ method @ColorInt public int getFocusedSearchResultHighlightColor();
+ method public int getFocusedSearchResultIndex();
method @Nullable public String getFontFeatureSettings();
method @Nullable public String getFontVariationSettings();
method public boolean getFreezesText();
@@ -59348,6 +59348,8 @@
method public android.text.TextPaint getPaint();
method public int getPaintFlags();
method public String getPrivateImeOptions();
+ method @ColorInt public int getSearchResultHighlightColor();
+ method @Nullable public int[] getSearchResultHighlights();
method public int getSelectionEnd();
method public int getSelectionStart();
method @ColorInt public int getShadowColor();
@@ -59432,6 +59434,8 @@
method public void setFallbackLineSpacing(boolean);
method public void setFilters(android.text.InputFilter[]);
method public void setFirstBaselineToTopHeight(@IntRange(from=0) @Px int);
+ method public void setFocusedSearchResultHighlightColor(@ColorInt int);
+ method public void setFocusedSearchResultIndex(int);
method public void setFontFeatureSettings(@Nullable String);
method public boolean setFontVariationSettings(@Nullable String);
method protected boolean setFrame(int, int, int, int);
@@ -59479,6 +59483,8 @@
method public void setPrivateImeOptions(String);
method public void setRawInputType(int);
method public void setScroller(android.widget.Scroller);
+ method public void setSearchResultHighlightColor(@ColorInt int);
+ method public void setSearchResultHighlights(@Nullable int...);
method public void setSelectAllOnFocus(boolean);
method public void setShadowLayer(float, float, float, int);
method public final void setShowSoftInputOnFocus(boolean);
@@ -59518,6 +59524,7 @@
method public void setWidth(int);
field public static final int AUTO_SIZE_TEXT_TYPE_NONE = 0; // 0x0
field public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1; // 0x1
+ field public static final int FOCUSED_SEARCH_RESULT_INDEX_NONE = -1; // 0xffffffff
}
public enum TextView.BufferType {
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 3216bd1..55ef6de 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -82,6 +82,7 @@
}
public abstract class Context {
+ method @NonNull public android.content.Context createContextForSdkInSandbox(@NonNull android.content.pm.ApplicationInfo, int) throws android.content.pm.PackageManager.NameNotFoundException;
method @NonNull public android.os.IBinder getIApplicationThreadBinder();
method @NonNull public android.os.UserHandle getUser();
field public static final String PAC_PROXY_SERVICE = "pac_proxy";
diff --git a/core/api/removed.txt b/core/api/removed.txt
index 1fa1e89..5c4fd10 100644
--- a/core/api/removed.txt
+++ b/core/api/removed.txt
@@ -252,6 +252,34 @@
}
+package android.nfc {
+
+ public final class NfcAdapter {
+ method @Deprecated public void disableForegroundNdefPush(android.app.Activity);
+ method @Deprecated public void enableForegroundNdefPush(android.app.Activity, android.nfc.NdefMessage);
+ method @Deprecated public boolean invokeBeam(android.app.Activity);
+ method @Deprecated public boolean isNdefPushEnabled();
+ method @Deprecated public void setBeamPushUris(android.net.Uri[], android.app.Activity);
+ method @Deprecated public void setBeamPushUrisCallback(android.nfc.NfcAdapter.CreateBeamUrisCallback, android.app.Activity);
+ method @Deprecated public void setNdefPushMessage(android.nfc.NdefMessage, android.app.Activity, android.app.Activity...);
+ method @Deprecated public void setNdefPushMessageCallback(android.nfc.NfcAdapter.CreateNdefMessageCallback, android.app.Activity, android.app.Activity...);
+ method @Deprecated public void setOnNdefPushCompleteCallback(android.nfc.NfcAdapter.OnNdefPushCompleteCallback, android.app.Activity, android.app.Activity...);
+ }
+
+ @Deprecated public static interface NfcAdapter.CreateBeamUrisCallback {
+ method public android.net.Uri[] createBeamUris(android.nfc.NfcEvent);
+ }
+
+ @Deprecated public static interface NfcAdapter.CreateNdefMessageCallback {
+ method public android.nfc.NdefMessage createNdefMessage(android.nfc.NfcEvent);
+ }
+
+ @Deprecated public static interface NfcAdapter.OnNdefPushCompleteCallback {
+ method public void onNdefPushComplete(android.nfc.NfcEvent);
+ }
+
+}
+
package android.os {
public class BatteryManager {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 85f8813..33ea8e1 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -9668,18 +9668,14 @@
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean addNfcUnlockHandler(android.nfc.NfcAdapter.NfcUnlockHandler, String[]);
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable();
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable(boolean);
- method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disableNdefPush();
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable();
- method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableNdefPush();
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableSecureNfc(boolean);
method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean isControllerAlwaysOn();
method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean isControllerAlwaysOnSupported();
method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void registerControllerAlwaysOnListener(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener);
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean removeNfcUnlockHandler(android.nfc.NfcAdapter.NfcUnlockHandler);
method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean setControllerAlwaysOn(boolean);
- method public void setNdefPushMessage(android.nfc.NdefMessage, android.app.Activity, int);
method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void unregisterControllerAlwaysOnListener(@NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener);
- field public static final int FLAG_NDEF_PUSH_NO_CONFIRM = 1; // 0x1
}
public static interface NfcAdapter.ControllerAlwaysOnListener {
@@ -13845,6 +13841,7 @@
method @Deprecated public boolean disableCellBroadcastRange(int, int, int);
method @Deprecated public boolean enableCellBroadcastRange(int, int, int);
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getPremiumSmsConsent(@NonNull String);
+ method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public android.net.Uri getSmscIdentity();
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_CELL_BROADCASTS) public void resetAllCellBroadcastRanges();
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void sendMultipartTextMessageWithoutPersisting(String, String, java.util.List<java.lang.String>, java.util.List<android.app.PendingIntent>, java.util.List<android.app.PendingIntent>);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setPremiumSmsConsent(@NonNull String, int);
diff --git a/core/api/system-removed.txt b/core/api/system-removed.txt
index 2c5acf1..1c10356 100644
--- a/core/api/system-removed.txt
+++ b/core/api/system-removed.txt
@@ -140,6 +140,17 @@
}
+package android.nfc {
+
+ public final class NfcAdapter {
+ method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disableNdefPush();
+ method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableNdefPush();
+ method public void setNdefPushMessage(android.nfc.NdefMessage, android.app.Activity, int);
+ field public static final int FLAG_NDEF_PUSH_NO_CONFIRM = 1; // 0x1
+ }
+
+}
+
package android.os {
public class Build {
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index d25c3f5..84ac868 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3206,6 +3206,14 @@
package android.view.autofill {
+ public class AutofillFeatureFlags {
+ field public static final String DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES = "compat_mode_allowed_packages";
+ field public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED = "autofill_credential_manager_enabled";
+ field public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_IGNORE_VIEWS = "autofill_credential_manager_ignore_views";
+ field public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED = "autofill_dialog_enabled";
+ field public static final String DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES = "smart_suggestion_supported_modes";
+ }
+
public final class AutofillId implements android.os.Parcelable {
ctor public AutofillId(int);
ctor public AutofillId(@NonNull android.view.autofill.AutofillId, int);
@@ -3217,9 +3225,6 @@
}
public final class AutofillManager {
- field public static final String DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES = "compat_mode_allowed_packages";
- field public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED = "autofill_dialog_enabled";
- field public static final String DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES = "smart_suggestion_supported_modes";
field public static final int FLAG_SMART_SUGGESTION_OFF = 0; // 0x0
field public static final int FLAG_SMART_SUGGESTION_SYSTEM = 1; // 0x1
field public static final int MAX_TEMP_AUGMENTED_SERVICE_DURATION_MS = 120000; // 0x1d4c0
@@ -3670,6 +3675,7 @@
public final class WindowContainerTransaction implements android.os.Parcelable {
ctor public WindowContainerTransaction();
+ method @NonNull public android.window.WindowContainerTransaction clearAdjacentTaskFragments(@NonNull android.os.IBinder);
method @NonNull public android.window.WindowContainerTransaction clearLaunchAdjacentFlagRoot(@NonNull android.window.WindowContainerToken);
method @NonNull public android.window.WindowContainerTransaction createTaskFragment(@NonNull android.window.TaskFragmentCreationParams);
method @NonNull public android.window.WindowContainerTransaction deleteTaskFragment(@NonNull android.os.IBinder);
@@ -3684,7 +3690,7 @@
method @NonNull public android.window.WindowContainerTransaction scheduleFinishEnterPip(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
method @NonNull public android.window.WindowContainerTransaction setActivityWindowingMode(@NonNull android.window.WindowContainerToken, int);
method @NonNull public android.window.WindowContainerTransaction setAdjacentRoots(@NonNull android.window.WindowContainerToken, @NonNull android.window.WindowContainerToken);
- method @NonNull public android.window.WindowContainerTransaction setAdjacentTaskFragments(@NonNull android.os.IBinder, @Nullable android.os.IBinder, @Nullable android.window.WindowContainerTransaction.TaskFragmentAdjacentParams);
+ method @NonNull public android.window.WindowContainerTransaction setAdjacentTaskFragments(@NonNull android.os.IBinder, @NonNull android.os.IBinder, @Nullable android.window.WindowContainerTransaction.TaskFragmentAdjacentParams);
method @NonNull public android.window.WindowContainerTransaction setAppBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
method @NonNull public android.window.WindowContainerTransaction setBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
method @NonNull public android.window.WindowContainerTransaction setBoundsChangeTransaction(@NonNull android.window.WindowContainerToken, @NonNull android.view.SurfaceControl.Transaction);
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 3f27964..421ba7c 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -2640,7 +2640,7 @@
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
return getPackageInfo(aInfo, compatInfo, baseLoader, securityViolation, includeCode,
- registerPackage, /*isSdkSandbox=*/false);
+ registerPackage, Process.isSdkSandbox());
}
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 563f6d4..a14f3d3 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -2489,7 +2489,7 @@
"RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO").setDefaultMode(
AppOpsManager.MODE_ALLOWED).build(),
new AppOpInfo.Builder(OP_RUN_LONG_JOBS, OPSTR_RUN_LONG_JOBS, "RUN_LONG_JOBS")
- .setPermission(Manifest.permission.RUN_LONG_JOBS).build(),
+ .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
new AppOpInfo.Builder(OP_READ_MEDIA_VISUAL_USER_SELECTED,
OPSTR_READ_MEDIA_VISUAL_USER_SELECTED, "READ_MEDIA_VISUAL_USER_SELECTED")
.setPermission(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index befe833a..b91fa35 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -2582,6 +2582,22 @@
}
@Override
+ public Context createContextForSdkInSandbox(ApplicationInfo sdkInfo, int flags)
+ throws NameNotFoundException {
+ if (!Process.isSdkSandbox()) {
+ throw new SecurityException("API can only be called from SdkSandbox process");
+ }
+
+ ContextImpl ctx = (ContextImpl) createApplicationContext(sdkInfo, flags);
+
+ // Set sandbox app's context as the application context for sdk context
+ ctx.mPackageInfo.makeApplicationInner(/*forceDefaultAppClass=*/false,
+ /*instrumentation=*/null);
+
+ return ctx;
+ }
+
+ @Override
public Context createPackageContext(String packageName, int flags)
throws NameNotFoundException {
return createPackageContextAsUser(packageName, flags, mUser);
@@ -3032,8 +3048,11 @@
throw new UnsupportedOperationException(
"Cannot update device ID on a Context created with createDeviceContext()");
}
- mDeviceId = updatedDeviceId;
- notifyOnDeviceChangedListeners(updatedDeviceId);
+
+ if (mDeviceId != updatedDeviceId) {
+ mDeviceId = updatedDeviceId;
+ notifyOnDeviceChangedListeners(updatedDeviceId);
+ }
}
@Override
diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java
index f0c39ab..c19a865 100644
--- a/core/java/android/app/ForegroundServiceTypePolicy.java
+++ b/core/java/android/app/ForegroundServiceTypePolicy.java
@@ -259,12 +259,15 @@
new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE)
}, true),
new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+ new RegularPermission(Manifest.permission.BLUETOOTH_ADVERTISE),
new RegularPermission(Manifest.permission.BLUETOOTH_CONNECT),
+ new RegularPermission(Manifest.permission.BLUETOOTH_SCAN),
new RegularPermission(Manifest.permission.CHANGE_NETWORK_STATE),
new RegularPermission(Manifest.permission.CHANGE_WIFI_STATE),
new RegularPermission(Manifest.permission.CHANGE_WIFI_MULTICAST_STATE),
new RegularPermission(Manifest.permission.NFC),
new RegularPermission(Manifest.permission.TRANSMIT_IR),
+ new RegularPermission(Manifest.permission.UWB_RANGING),
new UsbDevicePermission(),
new UsbAccessoryPermission(),
}, false)
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index f2eced3..20869e0 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -47,6 +47,9 @@
# AppOps
per-file *AppOp* = file:/core/java/android/permission/OWNERS
+# Backup and Restore
+per-file IBackupAgent.aidl = file:/services/backup/OWNERS
+
# LocaleManager
per-file *Locale* = file:/services/core/java/com/android/server/locales/OWNERS
diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java
index ad27b33..e485397 100644
--- a/core/java/android/app/Service.java
+++ b/core/java/android/app/Service.java
@@ -1080,7 +1080,6 @@
if (mForegroundServiceTraceTitle != null) {
Trace.asyncTraceForTrackEnd(TRACE_TAG_ACTIVITY_MANAGER,
TRACE_TRACK_NAME_FOREGROUND_SERVICE,
- mForegroundServiceTraceTitle,
System.identityHashCode(this));
mForegroundServiceTraceTitle = null;
}
diff --git a/core/java/android/app/admin/EnterprisePlatformSecurity_OWNERS b/core/java/android/app/admin/EnterprisePlatformSecurity_OWNERS
index 0ec8253..270cb18 100644
--- a/core/java/android/app/admin/EnterprisePlatformSecurity_OWNERS
+++ b/core/java/android/app/admin/EnterprisePlatformSecurity_OWNERS
@@ -1,5 +1,4 @@
rubinxu@google.com
-acjohnston@google.com
pgrafov@google.com
ayushsha@google.com
alexkershaw@google.com #{LAST_RESORT_SUGGESTION}
\ No newline at end of file
diff --git a/core/java/android/app/admin/EnterprisePlatform_OWNERS b/core/java/android/app/admin/EnterprisePlatform_OWNERS
index fb00fe5..6ce25cc 100644
--- a/core/java/android/app/admin/EnterprisePlatform_OWNERS
+++ b/core/java/android/app/admin/EnterprisePlatform_OWNERS
@@ -1,2 +1,5 @@
+# Assign bugs to android-enterprise-triage@google.com
file:WorkDeviceExperience_OWNERS
+file:Provisioning_OWNERS
+file:WorkProfile_OWNERS
file:EnterprisePlatformSecurity_OWNERS
\ No newline at end of file
diff --git a/core/java/android/app/admin/PolicyUpdatesReceiver.java b/core/java/android/app/admin/PolicyUpdatesReceiver.java
index 3ad3157..f7216e7 100644
--- a/core/java/android/app/admin/PolicyUpdatesReceiver.java
+++ b/core/java/android/app/admin/PolicyUpdatesReceiver.java
@@ -21,7 +21,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
-import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -109,8 +108,6 @@
public static final String ACTION_DEVICE_POLICY_CHANGED =
"android.app.admin.action.DEVICE_POLICY_CHANGED";
- // TODO(b/264510719): Remove once API linter is fixed
- @SuppressLint("ActionValue")
/**
* A string extra holding the package name the policy applies to, (see
* {@link PolicyUpdatesReceiver#onPolicyChanged} and
@@ -119,8 +116,6 @@
public static final String EXTRA_PACKAGE_NAME =
"android.app.admin.extra.PACKAGE_NAME";
- // TODO(b/264510719): Remove once API linter is fixed
- @SuppressLint("ActionValue")
/**
* A string extra holding the permission name the policy applies to, (see
* {@link PolicyUpdatesReceiver#onPolicyChanged} and
diff --git a/core/java/android/app/admin/Provisioning_OWNERS b/core/java/android/app/admin/Provisioning_OWNERS
new file mode 100644
index 0000000..c59a9dc
--- /dev/null
+++ b/core/java/android/app/admin/Provisioning_OWNERS
@@ -0,0 +1,5 @@
+# Assign bugs to android-enterprise-triage@google.com
+petuska@google.com
+nupursn@google.com
+shreyacsingh@google.com
+alexkershaw@google.com #{LAST_RESORT_SUGGESTION}
\ No newline at end of file
diff --git a/core/java/android/app/admin/WorkDeviceExperience_OWNERS b/core/java/android/app/admin/WorkDeviceExperience_OWNERS
index 82afddd..2033343 100644
--- a/core/java/android/app/admin/WorkDeviceExperience_OWNERS
+++ b/core/java/android/app/admin/WorkDeviceExperience_OWNERS
@@ -1,5 +1,7 @@
+# Assign bugs to android-enterprise-triage@google.com
work-device-experience+reviews@google.com
-scottjonathan@google.com
-kholoudm@google.com
-eliselliott@google.com
+scottjonathan@google.com #{LAST_RESORT_SUGGESTION}
+eliselliott@google.com #{LAST_RESORT_SUGGESTION}
+kholoudm@google.com #{LAST_RESORT_SUGGESTION}
+acjohnston@google.com #{LAST_RESORT_SUGGESTION}
alexkershaw@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/core/java/android/app/admin/WorkProfile_OWNERS b/core/java/android/app/admin/WorkProfile_OWNERS
new file mode 100644
index 0000000..260b672
--- /dev/null
+++ b/core/java/android/app/admin/WorkProfile_OWNERS
@@ -0,0 +1,5 @@
+# Assign bugs to android-enterprise-triage@google.com
+liahav@google.com
+olit@google.com
+scottjonathan@google.com #{LAST_RESORT_SUGGESTION}
+alexkershaw@google.com #{LAST_RESORT_SUGGESTION}
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/TEST_MAPPING b/core/java/android/companion/virtual/TEST_MAPPING
new file mode 100644
index 0000000..6a67b7f
--- /dev/null
+++ b/core/java/android/companion/virtual/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "imports": [
+ {
+ "path": "frameworks/base/services/companion/java/com/android/server/companion/virtual"
+ }
+ ]
+}
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 74c3f45..c8d48c1 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -6859,6 +6859,26 @@
@CreatePackageOptions int flags) throws PackageManager.NameNotFoundException;
/**
+ * Creates a context given an {@link android.content.pm.ApplicationInfo}.
+ *
+ * Context created is for an sdk library that is being loaded in sdk sandbox.
+ *
+ * @param sdkInfo information regarding the sdk library being loaded.
+ *
+ * @throws PackageManager.NameNotFoundException if there is no application with
+ * the given package name.
+ * @throws SecurityException if caller is not a SdkSandbox process.
+ *
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @NonNull
+ public Context createContextForSdkInSandbox(@NonNull ApplicationInfo sdkInfo,
+ @CreatePackageOptions int flags) throws PackageManager.NameNotFoundException {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+
+ /**
* Return a new Context object for the given split name. The new Context has a ClassLoader and
* Resources object that can access the split's and all of its dependencies' code/resources.
* Each call to this method returns a new instance of a Context object;
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 0dc4adc..e65e91c 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -1073,6 +1073,15 @@
/** @hide */
@Override
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @NonNull
+ public Context createContextForSdkInSandbox(@NonNull ApplicationInfo sdkInfo, int flags)
+ throws PackageManager.NameNotFoundException {
+ return mBase.createContextForSdkInSandbox(sdkInfo, flags);
+ }
+
+ /** @hide */
+ @Override
public Context createContextForSplit(String splitName)
throws PackageManager.NameNotFoundException {
return mBase.createContextForSplit(splitName);
diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl
index 9dd9c0f..9c1318e 100644
--- a/core/java/android/content/pm/IPackageInstallerSession.aidl
+++ b/core/java/android/content/pm/IPackageInstallerSession.aidl
@@ -64,7 +64,7 @@
void requestUserPreapproval(in PackageInstaller.PreapprovalDetails details, in IntentSender statusReceiver);
- boolean isKeepApplicationEnabledSetting();
+ boolean isApplicationEnabledSettingPersistent();
boolean isRequestUpdateOwnership();
ParcelFileDescriptor getAppMetadataFd();
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index df1340d..0b74dd1 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -2010,9 +2010,9 @@
* @return {@code true} if this session will keep the existing application enabled setting
* after installation.
*/
- public boolean isKeepApplicationEnabledSetting() {
+ public boolean isApplicationEnabledSettingPersistent() {
try {
- return mSession.isKeepApplicationEnabledSetting();
+ return mSession.isApplicationEnabledSettingPersistent();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -2265,7 +2265,7 @@
/** {@hide} */
public int requireUserAction = USER_ACTION_UNSPECIFIED;
/** {@hide} */
- public boolean keepApplicationEnabledSetting = false;
+ public boolean applicationEnabledSettingPersistent = false;
/**
* Construct parameters for a new package install session.
@@ -2310,7 +2310,7 @@
rollbackDataPolicy = source.readInt();
requireUserAction = source.readInt();
packageSource = source.readInt();
- keepApplicationEnabledSetting = source.readBoolean();
+ applicationEnabledSettingPersistent = source.readBoolean();
}
/** {@hide} */
@@ -2341,7 +2341,7 @@
ret.rollbackDataPolicy = rollbackDataPolicy;
ret.requireUserAction = requireUserAction;
ret.packageSource = packageSource;
- ret.keepApplicationEnabledSetting = keepApplicationEnabledSetting;
+ ret.applicationEnabledSettingPersistent = applicationEnabledSettingPersistent;
return ret;
}
@@ -2839,8 +2839,8 @@
* Request to keep the original application enabled setting. This will prevent the
* application from being enabled if it was previously in a disabled state.
*/
- public void setKeepApplicationEnabledSetting() {
- this.keepApplicationEnabledSetting = true;
+ public void setApplicationEnabledSettingPersistent() {
+ this.applicationEnabledSettingPersistent = true;
}
/**
@@ -2895,7 +2895,8 @@
pw.printPair("requiredInstalledVersionCode", requiredInstalledVersionCode);
pw.printPair("dataLoaderParams", dataLoaderParams);
pw.printPair("rollbackDataPolicy", rollbackDataPolicy);
- pw.printPair("keepApplicationEnabledSetting", keepApplicationEnabledSetting);
+ pw.printPair("applicationEnabledSettingPersistent",
+ applicationEnabledSettingPersistent);
pw.println();
}
@@ -2936,7 +2937,7 @@
dest.writeInt(rollbackDataPolicy);
dest.writeInt(requireUserAction);
dest.writeInt(packageSource);
- dest.writeBoolean(keepApplicationEnabledSetting);
+ dest.writeBoolean(applicationEnabledSettingPersistent);
}
public static final Parcelable.Creator<SessionParams>
@@ -3139,7 +3140,7 @@
public boolean isPreapprovalRequested;
/** @hide */
- public boolean keepApplicationEnabledSetting;
+ public boolean applicationEnabledSettingPersistent;
/** @hide */
public int pendingUserActionReason;
@@ -3197,7 +3198,7 @@
requireUserAction = source.readInt();
installerUid = source.readInt();
packageSource = source.readInt();
- keepApplicationEnabledSetting = source.readBoolean();
+ applicationEnabledSettingPersistent = source.readBoolean();
pendingUserActionReason = source.readInt();
}
@@ -3732,8 +3733,8 @@
* Returns {@code true} if this session will keep the existing application enabled setting
* after installation.
*/
- public boolean isKeepApplicationEnabledSetting() {
- return keepApplicationEnabledSetting;
+ public boolean isApplicationEnabledSettingPersistent() {
+ return applicationEnabledSettingPersistent;
}
/**
@@ -3811,7 +3812,7 @@
dest.writeInt(requireUserAction);
dest.writeInt(installerUid);
dest.writeInt(packageSource);
- dest.writeBoolean(keepApplicationEnabledSetting);
+ dest.writeBoolean(applicationEnabledSettingPersistent);
dest.writeInt(pendingUserActionReason);
}
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index fe06366..900454d 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -1552,7 +1552,7 @@
*
* @hide
*/
- public static final int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK = 0x00800000;
+ public static final int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK = 0x01000000;
/**
* Flag parameter for {@link PackageInstaller.SessionParams} to indicate that the
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index 4e2acc0..0b503eb 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -214,12 +214,15 @@
* {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission
* {@link android.Manifest.permission#FOREGROUND_SERVICE_CONNECTED_DEVICE} and one of the
* following permissions:
+ * {@link android.Manifest.permission#BLUETOOTH_ADVERTISE},
* {@link android.Manifest.permission#BLUETOOTH_CONNECT},
+ * {@link android.Manifest.permission#BLUETOOTH_SCAN},
* {@link android.Manifest.permission#CHANGE_NETWORK_STATE},
* {@link android.Manifest.permission#CHANGE_WIFI_STATE},
* {@link android.Manifest.permission#CHANGE_WIFI_MULTICAST_STATE},
* {@link android.Manifest.permission#NFC},
* {@link android.Manifest.permission#TRANSMIT_IR},
+ * {@link android.Manifest.permission#UWB_RANGING},
* or has been granted the access to one of the attached USB devices/accessories.
*/
@RequiresPermission(
@@ -227,12 +230,15 @@
Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE,
},
anyOf = {
+ Manifest.permission.BLUETOOTH_ADVERTISE,
Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.CHANGE_NETWORK_STATE,
Manifest.permission.CHANGE_WIFI_STATE,
Manifest.permission.CHANGE_WIFI_MULTICAST_STATE,
Manifest.permission.NFC,
Manifest.permission.TRANSMIT_IR,
+ Manifest.permission.UWB_RANGING,
},
conditional = true
)
diff --git a/core/java/android/credentials/GetCredentialResponse.java b/core/java/android/credentials/GetCredentialResponse.java
index 576da8b..4f8b026 100644
--- a/core/java/android/credentials/GetCredentialResponse.java
+++ b/core/java/android/credentials/GetCredentialResponse.java
@@ -19,7 +19,6 @@
import static java.util.Objects.requireNonNull;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
@@ -33,14 +32,14 @@
/**
* The credential that can be used to authenticate the user.
*/
- @Nullable
+ @NonNull
private final Credential mCredential;
/**
* Returns the credential that can be used to authenticate the user, or {@code null} if no
* credential is available.
*/
- @Nullable
+ @NonNull
public Credential getCredential() {
return mCredential;
}
@@ -69,13 +68,6 @@
mCredential = requireNonNull(credential, "credential must not be null");
}
- /**
- * Constructs a {@link GetCredentialResponse}.
- */
- public GetCredentialResponse() {
- mCredential = null;
- }
-
private GetCredentialResponse(@NonNull Parcel in) {
Credential credential = in.readTypedObject(Credential.CREATOR);
mCredential = credential;
diff --git a/core/java/android/hardware/camera2/CameraExtensionSession.java b/core/java/android/hardware/camera2/CameraExtensionSession.java
index 1542d61..21fead9 100644
--- a/core/java/android/hardware/camera2/CameraExtensionSession.java
+++ b/core/java/android/hardware/camera2/CameraExtensionSession.java
@@ -19,10 +19,7 @@
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.hardware.camera2.impl.PublicKey;
-import android.hardware.camera2.utils.TypeReference;
-import android.util.Pair;
-import android.util.Range;
+import android.hardware.camera2.utils.HashCodeHelpers;
import java.util.concurrent.Executor;
@@ -434,14 +431,66 @@
}
/**
- * Return the realtime still {@link #capture} latency.
+ * Realtime calculated still {@link #capture} latency.
*
- * <p>The pair will be in milliseconds with the first value indicating the capture latency from
- * the {@link ExtensionCaptureCallback#onCaptureStarted} until
- * {@link ExtensionCaptureCallback#onCaptureProcessStarted}
- * and the second value containing the estimated post-processing latency from
- * {@link ExtensionCaptureCallback#onCaptureProcessStarted} until the processed frame returns
- * to the client.</p>
+ * @see #getRealtimeStillCaptureLatency()
+ */
+ public final static class StillCaptureLatency {
+ private final long mCaptureLatency, mProcessingLatency;
+
+ public StillCaptureLatency(long captureLatency, long processingLatency) {
+ mCaptureLatency = captureLatency;
+ mProcessingLatency = processingLatency;
+ }
+ /**
+ * Return the capture latency from
+ * {@link ExtensionCaptureCallback#onCaptureStarted} until
+ * {@link ExtensionCaptureCallback#onCaptureProcessStarted}.
+ *
+ * @return The realtime capture latency in milliseconds.
+ */
+ public long getCaptureLatency() {
+ return mCaptureLatency;
+ }
+
+ /**
+ * Return the estimated post-processing latency from
+ * {@link ExtensionCaptureCallback#onCaptureProcessStarted} until the processed frame
+ * returns to the client.
+ *
+ * @return returns post-processing latency in milliseconds
+ */
+ public long getProcessingLatency() {
+ return mProcessingLatency;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ StillCaptureLatency latency = (StillCaptureLatency) o;
+
+ if (mCaptureLatency != latency.mCaptureLatency) return false;
+ if (mProcessingLatency != latency.mProcessingLatency) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return HashCodeHelpers.hashCode(mCaptureLatency, mProcessingLatency);
+ }
+
+ @Override
+ public String toString() {
+ return "StillCaptureLatency(processingLatency:" + mProcessingLatency +
+ ", captureLatency: " + mCaptureLatency + ")";
+ }
+ }
+
+ /**
+ * Return the realtime still {@link #capture} latency.
*
* <p>The estimations will take into account the current environment conditions, the camera
* state and will include the time spent processing the multi-frame capture request along with
@@ -451,7 +500,7 @@
* or {@code null} if the estimation is not supported.
*/
@Nullable
- public Pair<Long, Long> getRealtimeStillCaptureLatency() throws CameraAccessException {
+ public StillCaptureLatency getRealtimeStillCaptureLatency() throws CameraAccessException {
throw new UnsupportedOperationException("Subclasses must override this method");
}
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index b5a58b2..e2dedd6 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -2342,6 +2342,15 @@
final AvailabilityCallback callback = mCallbackMap.keyAt(i);
postSingleUpdate(callback, executor, id, null /*physicalId*/, status);
+
+ // Send the NOT_PRESENT state for unavailable physical cameras
+ if (isAvailable(status) && mUnavailablePhysicalDevices.containsKey(id)) {
+ ArrayList<String> unavailableIds = mUnavailablePhysicalDevices.get(id);
+ for (String unavailableId : unavailableIds) {
+ postSingleUpdate(callback, executor, id, unavailableId,
+ ICameraServiceListener.STATUS_NOT_PRESENT);
+ }
+ }
}
} // onStatusChangedLocked
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index 9437ea7..709fa60 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -361,7 +361,7 @@
}
@Override
- public Pair<Long, Long> getRealtimeStillCaptureLatency() throws CameraAccessException {
+ public StillCaptureLatency getRealtimeStillCaptureLatency() throws CameraAccessException {
synchronized (mInterfaceLock) {
if (!mInitialized) {
throw new IllegalStateException("Uninitialized component");
@@ -370,7 +370,7 @@
try {
LatencyPair latency = mSessionProcessor.getRealtimeCaptureLatency();
if (latency != null) {
- return new Pair<>(latency.first, latency.second);
+ return new StillCaptureLatency(latency.first, latency.second);
}
return null;
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index ed48a6d..3f85d44 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -513,7 +513,7 @@
}
@Override
- public Pair<Long, Long> getRealtimeStillCaptureLatency() throws CameraAccessException {
+ public StillCaptureLatency getRealtimeStillCaptureLatency() throws CameraAccessException {
synchronized (mInterfaceLock) {
if (!mInitialized) {
throw new IllegalStateException("Uninitialized component");
@@ -522,7 +522,7 @@
try {
LatencyPair latency = mImageExtender.getRealtimeCaptureLatency();
if (latency != null) {
- return new Pair<>(latency.first, latency.second);
+ return new StillCaptureLatency(latency.first, latency.second);
}
return null;
diff --git a/core/java/android/hardware/input/KeyboardLayout.java b/core/java/android/hardware/input/KeyboardLayout.java
index 58f7759..0311da4 100644
--- a/core/java/android/hardware/input/KeyboardLayout.java
+++ b/core/java/android/hardware/input/KeyboardLayout.java
@@ -23,6 +23,7 @@
import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
/**
* Describes a keyboard layout.
@@ -30,6 +31,37 @@
* @hide
*/
public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayout> {
+
+ /** Undefined keyboard layout */
+ public static final String LAYOUT_TYPE_UNDEFINED = "undefined";
+
+ /** Qwerty-based keyboard layout */
+ public static final String LAYOUT_TYPE_QWERTY = "qwerty";
+
+ /** Qwertz-based keyboard layout */
+ public static final String LAYOUT_TYPE_QWERTZ = "qwertz";
+
+ /** Azerty-based keyboard layout */
+ public static final String LAYOUT_TYPE_AZERTY = "azerty";
+
+ /** Dvorak keyboard layout */
+ public static final String LAYOUT_TYPE_DVORAK = "dvorak";
+
+ /** Colemak keyboard layout */
+ public static final String LAYOUT_TYPE_COLEMAK = "colemak";
+
+ /** Workman keyboard layout */
+ public static final String LAYOUT_TYPE_WORKMAN = "workman";
+
+ /** Turkish-F keyboard layout */
+ public static final String LAYOUT_TYPE_TURKISH_F = "turkish_f";
+
+ /** Turkish-Q keyboard layout */
+ public static final String LAYOUT_TYPE_TURKISH_Q = "turkish_q";
+
+ /** Keyboard layout that has been enhanced with a large number of extra characters */
+ public static final String LAYOUT_TYPE_EXTENDED = "extended";
+
private final String mDescriptor;
private final String mLabel;
private final String mCollection;
@@ -42,31 +74,24 @@
/** Currently supported Layout types in the KCM files */
private enum LayoutType {
- UNDEFINED(0, "undefined"),
- QWERTY(1, "qwerty"),
- QWERTZ(2, "qwertz"),
- AZERTY(3, "azerty"),
- DVORAK(4, "dvorak"),
- COLEMAK(5, "colemak"),
- WORKMAN(6, "workman"),
- TURKISH_F(7, "turkish_f"),
- TURKISH_Q(8, "turkish_q"),
- EXTENDED(9, "extended");
+ UNDEFINED(0, LAYOUT_TYPE_UNDEFINED),
+ QWERTY(1, LAYOUT_TYPE_QWERTY),
+ QWERTZ(2, LAYOUT_TYPE_QWERTZ),
+ AZERTY(3, LAYOUT_TYPE_AZERTY),
+ DVORAK(4, LAYOUT_TYPE_DVORAK),
+ COLEMAK(5, LAYOUT_TYPE_COLEMAK),
+ WORKMAN(6, LAYOUT_TYPE_WORKMAN),
+ TURKISH_F(7, LAYOUT_TYPE_TURKISH_F),
+ TURKISH_Q(8, LAYOUT_TYPE_TURKISH_Q),
+ EXTENDED(9, LAYOUT_TYPE_EXTENDED);
private final int mValue;
private final String mName;
private static final Map<Integer, LayoutType> VALUE_TO_ENUM_MAP = new HashMap<>();
static {
- VALUE_TO_ENUM_MAP.put(UNDEFINED.mValue, UNDEFINED);
- VALUE_TO_ENUM_MAP.put(QWERTY.mValue, QWERTY);
- VALUE_TO_ENUM_MAP.put(QWERTZ.mValue, QWERTZ);
- VALUE_TO_ENUM_MAP.put(AZERTY.mValue, AZERTY);
- VALUE_TO_ENUM_MAP.put(DVORAK.mValue, DVORAK);
- VALUE_TO_ENUM_MAP.put(COLEMAK.mValue, COLEMAK);
- VALUE_TO_ENUM_MAP.put(WORKMAN.mValue, WORKMAN);
- VALUE_TO_ENUM_MAP.put(TURKISH_F.mValue, TURKISH_F);
- VALUE_TO_ENUM_MAP.put(TURKISH_Q.mValue, TURKISH_Q);
- VALUE_TO_ENUM_MAP.put(EXTENDED.mValue, EXTENDED);
+ for (LayoutType type : LayoutType.values()) {
+ VALUE_TO_ENUM_MAP.put(type.mValue, type);
+ }
}
private static LayoutType of(int value) {
@@ -207,6 +232,9 @@
// keyboards to be listed before lower priority keyboards.
int result = Integer.compare(another.mPriority, mPriority);
if (result == 0) {
+ result = Integer.compare(mLayoutType.mValue, another.mLayoutType.mValue);
+ }
+ if (result == 0) {
result = mLabel.compareToIgnoreCase(another.mLabel);
}
if (result == 0) {
@@ -226,4 +254,21 @@
+ ", vendorId: " + mVendorId
+ ", productId: " + mProductId;
}
+
+ /**
+ * Check if the provided layout type is supported/valid.
+ *
+ * @param layoutName name of layout type
+ * @return {@code true} if the provided layout type is supported/valid.
+ */
+ public static boolean isLayoutTypeValid(@NonNull String layoutName) {
+ Objects.requireNonNull(layoutName, "Provided layout name should not be null");
+ for (LayoutType layoutType : LayoutType.values()) {
+ if (layoutName.equals(layoutType.getName())) {
+ return true;
+ }
+ }
+ // Layout doesn't match any supported layout types
+ return false;
+ }
}
diff --git a/core/java/android/nfc/BeamShareData.java b/core/java/android/nfc/BeamShareData.java
deleted file mode 100644
index 6a40f98..0000000
--- a/core/java/android/nfc/BeamShareData.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package android.nfc;
-
-import android.net.Uri;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.os.UserHandle;
-
-/**
- * Class to IPC data to be shared over Android Beam.
- * Allows bundling NdefMessage, Uris and flags in a single
- * IPC call. This is important as we want to reduce the
- * amount of IPC calls at "touch time".
- * @hide
- */
-public final class BeamShareData implements Parcelable {
- public final NdefMessage ndefMessage;
- public final Uri[] uris;
- public final UserHandle userHandle;
- public final int flags;
-
- public BeamShareData(NdefMessage msg, Uri[] uris, UserHandle userHandle, int flags) {
- this.ndefMessage = msg;
- this.uris = uris;
- this.userHandle = userHandle;
- this.flags = flags;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- int urisLength = (uris != null) ? uris.length : 0;
- dest.writeParcelable(ndefMessage, 0);
- dest.writeInt(urisLength);
- if (urisLength > 0) {
- dest.writeTypedArray(uris, 0);
- }
- dest.writeParcelable(userHandle, 0);
- dest.writeInt(this.flags);
- }
-
- public static final @android.annotation.NonNull Parcelable.Creator<BeamShareData> CREATOR =
- new Parcelable.Creator<BeamShareData>() {
- @Override
- public BeamShareData createFromParcel(Parcel source) {
- Uri[] uris = null;
- NdefMessage msg = source.readParcelable(NdefMessage.class.getClassLoader(), android.nfc.NdefMessage.class);
- int numUris = source.readInt();
- if (numUris > 0) {
- uris = new Uri[numUris];
- source.readTypedArray(uris, Uri.CREATOR);
- }
- UserHandle userHandle = source.readParcelable(UserHandle.class.getClassLoader(), android.os.UserHandle.class);
- int flags = source.readInt();
-
- return new BeamShareData(msg, uris, userHandle, flags);
- }
-
- @Override
- public BeamShareData[] newArray(int size) {
- return new BeamShareData[size];
- }
- };
-}
diff --git a/core/java/android/nfc/IAppCallback.aidl b/core/java/android/nfc/IAppCallback.aidl
index 133146d..b06bf06 100644
--- a/core/java/android/nfc/IAppCallback.aidl
+++ b/core/java/android/nfc/IAppCallback.aidl
@@ -16,7 +16,6 @@
package android.nfc;
-import android.nfc.BeamShareData;
import android.nfc.Tag;
/**
@@ -24,7 +23,5 @@
*/
interface IAppCallback
{
- BeamShareData createBeamShareData(byte peerLlcpVersion);
- oneway void onNdefPushComplete(byte peerLlcpVersion);
oneway void onTagDiscovered(in Tag tag);
}
diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl
index de107a2..f6aa4b4 100644
--- a/core/java/android/nfc/INfcAdapter.aidl
+++ b/core/java/android/nfc/INfcAdapter.aidl
@@ -18,7 +18,6 @@
import android.app.PendingIntent;
import android.content.IntentFilter;
-import android.nfc.BeamShareData;
import android.nfc.NdefMessage;
import android.nfc.Tag;
import android.nfc.TechListParcel;
@@ -47,24 +46,18 @@
int getState();
boolean disable(boolean saveState);
boolean enable();
- boolean enableNdefPush();
- boolean disableNdefPush();
- boolean isNdefPushEnabled();
void pausePolling(int timeoutInMs);
void resumePolling();
void setForegroundDispatch(in PendingIntent intent,
in IntentFilter[] filters, in TechListParcel techLists);
void setAppCallback(in IAppCallback callback);
- oneway void invokeBeam();
- oneway void invokeBeamInternal(in BeamShareData shareData);
boolean ignore(int nativeHandle, int debounceMs, ITagRemovedCallback callback);
void dispatch(in Tag tag);
void setReaderMode (IBinder b, IAppCallback callback, int flags, in Bundle extras);
- void setP2pModes(int initatorModes, int targetModes);
void addNfcUnlockHandler(INfcUnlockHandler unlockHandler, in int[] techList);
void removeNfcUnlockHandler(INfcUnlockHandler unlockHandler);
diff --git a/core/java/android/nfc/NfcActivityManager.java b/core/java/android/nfc/NfcActivityManager.java
index 911aaf3..8d75cac 100644
--- a/core/java/android/nfc/NfcActivityManager.java
+++ b/core/java/android/nfc/NfcActivityManager.java
@@ -19,9 +19,6 @@
import android.app.Activity;
import android.app.Application;
import android.compat.annotation.UnsupportedAppUsage;
-import android.content.ContentProvider;
-import android.content.Intent;
-import android.net.Uri;
import android.nfc.NfcAdapter.ReaderCallback;
import android.os.Binder;
import android.os.Bundle;
@@ -110,14 +107,8 @@
class NfcActivityState {
boolean resumed = false;
Activity activity;
- NdefMessage ndefMessage = null; // static NDEF message
- NfcAdapter.CreateNdefMessageCallback ndefMessageCallback = null;
- NfcAdapter.OnNdefPushCompleteCallback onNdefPushCompleteCallback = null;
- NfcAdapter.CreateBeamUrisCallback uriCallback = null;
- Uri[] uris = null;
- int flags = 0;
- int readerModeFlags = 0;
NfcAdapter.ReaderCallback readerCallback = null;
+ int readerModeFlags = 0;
Bundle readerModeExtras = null;
Binder token;
@@ -137,24 +128,16 @@
unregisterApplication(activity.getApplication());
resumed = false;
activity = null;
- ndefMessage = null;
- ndefMessageCallback = null;
- onNdefPushCompleteCallback = null;
- uriCallback = null;
- uris = null;
+ readerCallback = null;
readerModeFlags = 0;
+ readerModeExtras = null;
token = null;
}
@Override
public String toString() {
- StringBuilder s = new StringBuilder("[").append(" ");
- s.append(ndefMessage).append(" ").append(ndefMessageCallback).append(" ");
- s.append(uriCallback).append(" ");
- if (uris != null) {
- for (Uri uri : uris) {
- s.append(onNdefPushCompleteCallback).append(" ").append(uri).append("]");
- }
- }
+ StringBuilder s = new StringBuilder("[");
+ s.append(readerCallback);
+ s.append("]");
return s.toString();
}
}
@@ -245,92 +228,6 @@
}
}
- public void setNdefPushContentUri(Activity activity, Uri[] uris) {
- boolean isResumed;
- synchronized (NfcActivityManager.this) {
- NfcActivityState state = getActivityState(activity);
- state.uris = uris;
- isResumed = state.resumed;
- }
- if (isResumed) {
- // requestNfcServiceCallback() verifies permission also
- requestNfcServiceCallback();
- } else {
- // Crash API calls early in case NFC permission is missing
- verifyNfcPermission();
- }
- }
-
-
- public void setNdefPushContentUriCallback(Activity activity,
- NfcAdapter.CreateBeamUrisCallback callback) {
- boolean isResumed;
- synchronized (NfcActivityManager.this) {
- NfcActivityState state = getActivityState(activity);
- state.uriCallback = callback;
- isResumed = state.resumed;
- }
- if (isResumed) {
- // requestNfcServiceCallback() verifies permission also
- requestNfcServiceCallback();
- } else {
- // Crash API calls early in case NFC permission is missing
- verifyNfcPermission();
- }
- }
-
- public void setNdefPushMessage(Activity activity, NdefMessage message, int flags) {
- boolean isResumed;
- synchronized (NfcActivityManager.this) {
- NfcActivityState state = getActivityState(activity);
- state.ndefMessage = message;
- state.flags = flags;
- isResumed = state.resumed;
- }
- if (isResumed) {
- // requestNfcServiceCallback() verifies permission also
- requestNfcServiceCallback();
- } else {
- // Crash API calls early in case NFC permission is missing
- verifyNfcPermission();
- }
- }
-
- public void setNdefPushMessageCallback(Activity activity,
- NfcAdapter.CreateNdefMessageCallback callback, int flags) {
- boolean isResumed;
- synchronized (NfcActivityManager.this) {
- NfcActivityState state = getActivityState(activity);
- state.ndefMessageCallback = callback;
- state.flags = flags;
- isResumed = state.resumed;
- }
- if (isResumed) {
- // requestNfcServiceCallback() verifies permission also
- requestNfcServiceCallback();
- } else {
- // Crash API calls early in case NFC permission is missing
- verifyNfcPermission();
- }
- }
-
- public void setOnNdefPushCompleteCallback(Activity activity,
- NfcAdapter.OnNdefPushCompleteCallback callback) {
- boolean isResumed;
- synchronized (NfcActivityManager.this) {
- NfcActivityState state = getActivityState(activity);
- state.onNdefPushCompleteCallback = callback;
- isResumed = state.resumed;
- }
- if (isResumed) {
- // requestNfcServiceCallback() verifies permission also
- requestNfcServiceCallback();
- } else {
- // Crash API calls early in case NFC permission is missing
- verifyNfcPermission();
- }
- }
-
/**
* Request or unrequest NFC service callbacks.
* Makes IPC call - do not hold lock.
@@ -351,86 +248,6 @@
}
}
- /** Callback from NFC service, usually on binder thread */
- @Override
- public BeamShareData createBeamShareData(byte peerLlcpVersion) {
- NfcAdapter.CreateNdefMessageCallback ndefCallback;
- NfcAdapter.CreateBeamUrisCallback urisCallback;
- NdefMessage message;
- Activity activity;
- Uri[] uris;
- int flags;
- NfcEvent event = new NfcEvent(mAdapter, peerLlcpVersion);
- synchronized (NfcActivityManager.this) {
- NfcActivityState state = findResumedActivityState();
- if (state == null) return null;
-
- ndefCallback = state.ndefMessageCallback;
- urisCallback = state.uriCallback;
- message = state.ndefMessage;
- uris = state.uris;
- flags = state.flags;
- activity = state.activity;
- }
- final long ident = Binder.clearCallingIdentity();
- try {
- // Make callbacks without lock
- if (ndefCallback != null) {
- message = ndefCallback.createNdefMessage(event);
- }
- if (urisCallback != null) {
- uris = urisCallback.createBeamUris(event);
- if (uris != null) {
- ArrayList<Uri> validUris = new ArrayList<Uri>();
- for (Uri uri : uris) {
- if (uri == null) {
- Log.e(TAG, "Uri not allowed to be null.");
- continue;
- }
- String scheme = uri.getScheme();
- if (scheme == null || (!scheme.equalsIgnoreCase("file") &&
- !scheme.equalsIgnoreCase("content"))) {
- Log.e(TAG, "Uri needs to have " +
- "either scheme file or scheme content");
- continue;
- }
- uri = ContentProvider.maybeAddUserId(uri, activity.getUserId());
- validUris.add(uri);
- }
-
- uris = validUris.toArray(new Uri[validUris.size()]);
- }
- }
- if (uris != null && uris.length > 0) {
- for (Uri uri : uris) {
- // Grant the NFC process permission to read these URIs
- activity.grantUriPermission("com.android.nfc", uri,
- Intent.FLAG_GRANT_READ_URI_PERMISSION);
- }
- }
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- return new BeamShareData(message, uris, activity.getUser(), flags);
- }
-
- /** Callback from NFC service, usually on binder thread */
- @Override
- public void onNdefPushComplete(byte peerLlcpVersion) {
- NfcAdapter.OnNdefPushCompleteCallback callback;
- synchronized (NfcActivityManager.this) {
- NfcActivityState state = findResumedActivityState();
- if (state == null) return;
-
- callback = state.onNdefPushCompleteCallback;
- }
- NfcEvent event = new NfcEvent(mAdapter, peerLlcpVersion);
- // Make callback without lock
- if (callback != null) {
- callback.onNdefPushComplete(event);
- }
- }
-
@Override
public void onTagDiscovered(Tag tag) throws RemoteException {
NfcAdapter.ReaderCallback callback;
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index 0c7f529..c4b3c22 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -338,7 +338,10 @@
*/
public static final String EXTRA_READER_PRESENCE_CHECK_DELAY = "presence";
- /** @hide */
+ /**
+ * @hide
+ * @removed
+ */
@SystemApi
public static final int FLAG_NDEF_PUSH_NO_CONFIRM = 0x1;
@@ -374,7 +377,6 @@
// Guarded by NfcAdapter.class
static boolean sIsInitialized = false;
static boolean sHasNfcFeature;
- static boolean sHasBeamFeature;
// Final after first constructor, except for
// attemptDeadServiceRecovery() when NFC crashes - we accept a best effort
@@ -438,7 +440,7 @@
* A callback to be invoked when the system successfully delivers your {@link NdefMessage}
* to another device.
* @see #setOnNdefPushCompleteCallback
- * @deprecated this feature is deprecated. File sharing can work using other technology like
+ * @removed this feature is removed. File sharing can work using other technology like
* Bluetooth.
*/
@java.lang.Deprecated
@@ -464,7 +466,7 @@
* content currently visible to the user. Alternatively, you can call {@link
* #setNdefPushMessage setNdefPushMessage()} if the {@link NdefMessage} always contains the
* same data.
- * @deprecated this feature is deprecated. File sharing can work using other technology like
+ * @removed this feature is removed. File sharing can work using other technology like
* Bluetooth.
*/
@java.lang.Deprecated
@@ -494,7 +496,7 @@
/**
- * @deprecated this feature is deprecated. File sharing can work using other technology like
+ * @removed this feature is removed. File sharing can work using other technology like
* Bluetooth.
*/
@java.lang.Deprecated
@@ -526,26 +528,6 @@
}
/**
- * Helper to check if this device has FEATURE_NFC_BEAM, but without using
- * a context.
- * Equivalent to
- * context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC_BEAM)
- */
- private static boolean hasBeamFeature() {
- IPackageManager pm = ActivityThread.getPackageManager();
- if (pm == null) {
- Log.e(TAG, "Cannot get package manager, assuming no Android Beam feature");
- return false;
- }
- try {
- return pm.hasSystemFeature(PackageManager.FEATURE_NFC_BEAM, 0);
- } catch (RemoteException e) {
- Log.e(TAG, "Package manager query failed, assuming no Android Beam feature", e);
- return false;
- }
- }
-
- /**
* Helper to check if this device has FEATURE_NFC, but without using
* a context.
* Equivalent to
@@ -624,7 +606,6 @@
public static synchronized NfcAdapter getNfcAdapter(Context context) {
if (!sIsInitialized) {
sHasNfcFeature = hasNfcFeature();
- sHasBeamFeature = hasBeamFeature();
boolean hasHceFeature = hasNfcHceFeature();
/* is this device meant to have NFC */
if (!sHasNfcFeature && !hasHceFeature) {
@@ -1117,7 +1098,7 @@
* @param uris an array of Uri(s) to push over Android Beam
* @param activity activity for which the Uri(s) will be pushed
* @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
- * @deprecated this feature is deprecated. File sharing can work using other technology like
+ * @removed this feature is removed. File sharing can work using other technology like
* Bluetooth.
*/
@java.lang.Deprecated
@@ -1126,26 +1107,7 @@
if (!sHasNfcFeature) {
throw new UnsupportedOperationException();
}
- if (!sHasBeamFeature) {
- return;
- }
}
- if (activity == null) {
- throw new NullPointerException("activity cannot be null");
- }
- if (uris != null) {
- for (Uri uri : uris) {
- if (uri == null) throw new NullPointerException("Uri not " +
- "allowed to be null");
- String scheme = uri.getScheme();
- if (scheme == null || (!scheme.equalsIgnoreCase("file") &&
- !scheme.equalsIgnoreCase("content"))) {
- throw new IllegalArgumentException("URI needs to have " +
- "either scheme file or scheme content");
- }
- }
- }
- mNfcActivityManager.setNdefPushContentUri(activity, uris);
}
/**
@@ -1205,7 +1167,7 @@
* @param callback callback, or null to disable
* @param activity activity for which the Uri(s) will be pushed
* @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
- * @deprecated this feature is deprecated. File sharing can work using other technology like
+ * @removed this feature is removed. File sharing can work using other technology like
* Bluetooth.
*/
@java.lang.Deprecated
@@ -1214,14 +1176,7 @@
if (!sHasNfcFeature) {
throw new UnsupportedOperationException();
}
- if (!sHasBeamFeature) {
- return;
- }
}
- if (activity == null) {
- throw new NullPointerException("activity cannot be null");
- }
- mNfcActivityManager.setNdefPushContentUriCallback(activity, callback);
}
/**
@@ -1295,7 +1250,7 @@
* to only register one at a time, and to do so in that activity's
* {@link Activity#onCreate}
* @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
- * @deprecated this feature is deprecated. File sharing can work using other technology like
+ * @removed this feature is removed. File sharing can work using other technology like
* Bluetooth.
*/
@java.lang.Deprecated
@@ -1305,36 +1260,12 @@
if (!sHasNfcFeature) {
throw new UnsupportedOperationException();
}
- if (!sHasBeamFeature) {
- return;
- }
- }
- int targetSdkVersion = getSdkVersion();
- try {
- if (activity == null) {
- throw new NullPointerException("activity cannot be null");
- }
- mNfcActivityManager.setNdefPushMessage(activity, message, 0);
- for (Activity a : activities) {
- if (a == null) {
- throw new NullPointerException("activities cannot contain null");
- }
- mNfcActivityManager.setNdefPushMessage(a, message, 0);
- }
- } catch (IllegalStateException e) {
- if (targetSdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN) {
- // Less strict on old applications - just log the error
- Log.e(TAG, "Cannot call API with Activity that has already " +
- "been destroyed", e);
- } else {
- // Prevent new applications from making this mistake, re-throw
- throw(e);
- }
}
}
/**
* @hide
+ * @removed
*/
@SystemApi
public void setNdefPushMessage(NdefMessage message, Activity activity, int flags) {
@@ -1343,10 +1274,6 @@
throw new UnsupportedOperationException();
}
}
- if (activity == null) {
- throw new NullPointerException("activity cannot be null");
- }
- mNfcActivityManager.setNdefPushMessage(activity, message, flags);
}
/**
@@ -1414,7 +1341,7 @@
* to only register one at a time, and to do so in that activity's
* {@link Activity#onCreate}
* @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
- * @deprecated this feature is deprecated. File sharing can work using other technology like
+ * @removed this feature is removed. File sharing can work using other technology like
* Bluetooth.
*/
@java.lang.Deprecated
@@ -1424,44 +1351,7 @@
if (!sHasNfcFeature) {
throw new UnsupportedOperationException();
}
- if (!sHasBeamFeature) {
- return;
- }
}
- int targetSdkVersion = getSdkVersion();
- try {
- if (activity == null) {
- throw new NullPointerException("activity cannot be null");
- }
- mNfcActivityManager.setNdefPushMessageCallback(activity, callback, 0);
- for (Activity a : activities) {
- if (a == null) {
- throw new NullPointerException("activities cannot contain null");
- }
- mNfcActivityManager.setNdefPushMessageCallback(a, callback, 0);
- }
- } catch (IllegalStateException e) {
- if (targetSdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN) {
- // Less strict on old applications - just log the error
- Log.e(TAG, "Cannot call API with Activity that has already " +
- "been destroyed", e);
- } else {
- // Prevent new applications from making this mistake, re-throw
- throw(e);
- }
- }
- }
-
- /**
- * @hide
- */
- @UnsupportedAppUsage
- public void setNdefPushMessageCallback(CreateNdefMessageCallback callback, Activity activity,
- int flags) {
- if (activity == null) {
- throw new NullPointerException("activity cannot be null");
- }
- mNfcActivityManager.setNdefPushMessageCallback(activity, callback, flags);
}
/**
@@ -1501,7 +1391,7 @@
* to only register one at a time, and to do so in that activity's
* {@link Activity#onCreate}
* @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
- * @deprecated this feature is deprecated. File sharing can work using other technology like
+ * @removed this feature is removed. File sharing can work using other technology like
* Bluetooth.
*/
@java.lang.Deprecated
@@ -1511,31 +1401,6 @@
if (!sHasNfcFeature) {
throw new UnsupportedOperationException();
}
- if (!sHasBeamFeature) {
- return;
- }
- }
- int targetSdkVersion = getSdkVersion();
- try {
- if (activity == null) {
- throw new NullPointerException("activity cannot be null");
- }
- mNfcActivityManager.setOnNdefPushCompleteCallback(activity, callback);
- for (Activity a : activities) {
- if (a == null) {
- throw new NullPointerException("activities cannot contain null");
- }
- mNfcActivityManager.setOnNdefPushCompleteCallback(a, callback);
- }
- } catch (IllegalStateException e) {
- if (targetSdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN) {
- // Less strict on old applications - just log the error
- Log.e(TAG, "Cannot call API with Activity that has already " +
- "been destroyed", e);
- } else {
- // Prevent new applications from making this mistake, re-throw
- throw(e);
- }
}
}
@@ -1718,7 +1583,7 @@
* @param activity the current foreground Activity that has registered data to share
* @return whether the Beam animation was successfully invoked
* @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
- * @deprecated this feature is deprecated. File sharing can work using other technology like
+ * @removed this feature is removed. File sharing can work using other technology like
* Bluetooth.
*/
@java.lang.Deprecated
@@ -1727,37 +1592,8 @@
if (!sHasNfcFeature) {
throw new UnsupportedOperationException();
}
- if (!sHasBeamFeature) {
- return false;
- }
}
- if (activity == null) {
- throw new NullPointerException("activity may not be null.");
- }
- enforceResumed(activity);
- try {
- sService.invokeBeam();
- return true;
- } catch (RemoteException e) {
- Log.e(TAG, "invokeBeam: NFC process has died.");
- attemptDeadServiceRecovery(e);
- return false;
- }
- }
-
- /**
- * @hide
- */
- public boolean invokeBeam(BeamShareData shareData) {
- try {
- Log.e(TAG, "invokeBeamInternal()");
- sService.invokeBeamInternal(shareData);
- return true;
- } catch (RemoteException e) {
- Log.e(TAG, "invokeBeam: NFC process has died.");
- attemptDeadServiceRecovery(e);
- return false;
- }
+ return false;
}
/**
@@ -1783,9 +1619,9 @@
*
* @param activity foreground activity
* @param message a NDEF Message to push over NFC
- * @throws IllegalStateException if the activity is not currently in the foreground
- * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
- * @deprecated use {@link #setNdefPushMessage} instead
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable
+ * @removed this feature is removed. File sharing can work using other technology like
+ * Bluetooth.
*/
@Deprecated
public void enableForegroundNdefPush(Activity activity, NdefMessage message) {
@@ -1793,15 +1629,7 @@
if (!sHasNfcFeature) {
throw new UnsupportedOperationException();
}
- if (!sHasBeamFeature) {
- return;
- }
}
- if (activity == null || message == null) {
- throw new NullPointerException();
- }
- enforceResumed(activity);
- mNfcActivityManager.setNdefPushMessage(activity, message, 0);
}
/**
@@ -1820,9 +1648,9 @@
* <p class="note">Requires the {@link android.Manifest.permission#NFC} permission.
*
* @param activity the Foreground activity
- * @throws IllegalStateException if the Activity has already been paused
- * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
- * @deprecated use {@link #setNdefPushMessage} instead
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable
+ * @removed this feature is removed. File sharing can work using other technology like
+ * Bluetooth.
*/
@Deprecated
public void disableForegroundNdefPush(Activity activity) {
@@ -1830,17 +1658,7 @@
if (!sHasNfcFeature) {
throw new UnsupportedOperationException();
}
- if (!sHasBeamFeature) {
- return;
- }
}
- if (activity == null) {
- throw new NullPointerException();
- }
- enforceResumed(activity);
- mNfcActivityManager.setNdefPushMessage(activity, null, 0);
- mNfcActivityManager.setNdefPushMessageCallback(activity, null, 0);
- mNfcActivityManager.setOnNdefPushCompleteCallback(activity, null);
}
/**
@@ -1965,40 +1783,24 @@
* Enable NDEF Push feature.
* <p>This API is for the Settings application.
* @hide
+ * @removed
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public boolean enableNdefPush() {
- if (!sHasNfcFeature) {
- throw new UnsupportedOperationException();
- }
- try {
- return sService.enableNdefPush();
- } catch (RemoteException e) {
- attemptDeadServiceRecovery(e);
- return false;
- }
+ return false;
}
/**
* Disable NDEF Push feature.
* <p>This API is for the Settings application.
* @hide
+ * @removed
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public boolean disableNdefPush() {
- synchronized (NfcAdapter.class) {
- if (!sHasNfcFeature) {
- throw new UnsupportedOperationException();
- }
- }
- try {
- return sService.disableNdefPush();
- } catch (RemoteException e) {
- attemptDeadServiceRecovery(e);
- return false;
- }
+ return false;
}
/**
@@ -2024,26 +1826,17 @@
* @see android.provider.Settings#ACTION_NFCSHARING_SETTINGS
* @return true if NDEF Push feature is enabled
* @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
- * @deprecated this feature is deprecated. File sharing can work using other technology like
+ * @removed this feature is removed. File sharing can work using other technology like
* Bluetooth.
*/
@java.lang.Deprecated
-
public boolean isNdefPushEnabled() {
synchronized (NfcAdapter.class) {
if (!sHasNfcFeature) {
throw new UnsupportedOperationException();
}
- if (!sHasBeamFeature) {
- return false;
- }
}
- try {
- return sService.isNdefPushEnabled();
- } catch (RemoteException e) {
- attemptDeadServiceRecovery(e);
- return false;
- }
+ return false;
}
/**
@@ -2134,17 +1927,6 @@
}
/**
- * @hide
- */
- public void setP2pModes(int initiatorModes, int targetModes) {
- try {
- sService.setP2pModes(initiatorModes, targetModes);
- } catch (RemoteException e) {
- attemptDeadServiceRecovery(e);
- }
- }
-
- /**
* Registers a new NFC unlock handler with the NFC service.
*
* <p />NFC unlock handlers are intended to unlock the keyguard in the presence of a trusted
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index 34aa7ef..547d406 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -1142,6 +1142,16 @@
@BatteryConsumer.ProcessState int processState);
+
+ /**
+ * Returns the battery consumption (in microcoulombs) of UID's camera usage, derived from
+ * on-device power measurement data.
+ * Will return {@link #POWER_DATA_UNAVAILABLE} if data is unavailable.
+ *
+ * {@hide}
+ */
+ public abstract long getCameraEnergyConsumptionUC();
+
/**
* Returns the battery consumption (in microcoulombs) used by this uid for each
* {@link android.hardware.power.stats.EnergyConsumer.ordinal} of (custom) energy consumer
@@ -2921,6 +2931,15 @@
public abstract long getWifiEnergyConsumptionUC();
/**
+ * Returns the battery consumption (in microcoulombs) of camera, derived from on
+ * device power measurement data.
+ * Will return {@link #POWER_DATA_UNAVAILABLE} if data is unavailable.
+ *
+ * {@hide}
+ */
+ public abstract long getCameraEnergyConsumptionUC();
+
+ /**
* Returns the battery consumption (in microcoulombs) that each
* {@link android.hardware.power.stats.EnergyConsumer.ordinal} of (custom) energy consumer
* type {@link android.hardware.power.stats.EnergyConsumerType#OTHER}) consumed.
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index 249f486..32773a0 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -754,12 +754,9 @@
* PackageManager.setComponentEnabledSetting} will now throw an
* IllegalArgumentException if the given component class name does not
* exist in the application's manifest.
- * <li> {@link android.nfc.NfcAdapter#setNdefPushMessage
- * NfcAdapter.setNdefPushMessage},
- * {@link android.nfc.NfcAdapter#setNdefPushMessageCallback
- * NfcAdapter.setNdefPushMessageCallback} and
- * {@link android.nfc.NfcAdapter#setOnNdefPushCompleteCallback
- * NfcAdapter.setOnNdefPushCompleteCallback} will throw
+ * <li> {@code NfcAdapter.setNdefPushMessage},
+ * {@code NfcAdapter.setNdefPushMessageCallback} and
+ * {@code NfcAdapter.setOnNdefPushCompleteCallback} will throw
* IllegalStateException if called after the Activity has been destroyed.
* <li> Accessibility services must require the new
* {@link android.Manifest.permission#BIND_ACCESSIBILITY_SERVICE} permission or
diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java
index cdde18a..adc73c8 100644
--- a/core/java/android/os/Trace.java
+++ b/core/java/android/os/Trace.java
@@ -395,19 +395,6 @@
}
/**
- * @deprecated use asyncTraceForTrackEnd without methodName argument
- *
- * @hide
- */
- @Deprecated
- public static void asyncTraceForTrackEnd(long traceTag,
- @NonNull String trackName, @NonNull String methodName, int cookie) {
- if (isTagEnabled(traceTag)) {
- nativeAsyncTraceForTrackEnd(traceTag, trackName, cookie);
- }
- }
-
- /**
* Writes a trace message to indicate that a given section of code was invoked.
*
* @param traceTag The trace tag.
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ec3ef9d..76ca20c 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -603,6 +603,8 @@
* Output: When a package data uri is passed as input, the activity result is set to
* {@link android.app.Activity#RESULT_OK} if the permission was granted to the app. Otherwise,
* the result is set to {@link android.app.Activity#RESULT_CANCELED}.
+ *
+ * @hide
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_MANAGE_APP_LONG_RUNNING_JOBS =
@@ -1692,7 +1694,6 @@
* Input: Nothing.
* <p>
* Output: Nothing
- * @see android.nfc.NfcAdapter#isNdefPushEnabled()
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_NFCSHARING_SETTINGS =
diff --git a/core/java/android/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java
index 4892312..13f7e5d 100644
--- a/core/java/android/security/net/config/SystemCertificateSource.java
+++ b/core/java/android/security/net/config/SystemCertificateSource.java
@@ -41,7 +41,7 @@
private static File getDirectory() {
// TODO(miguelaranda): figure out correct code path.
File updatable_dir = new File("/apex/com.android.conscrypt/cacerts");
- if (updatable_dir.exists()) {
+ if (updatable_dir.exists() && !(updatable_dir.list().length == 0)) {
return updatable_dir;
}
return new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts");
diff --git a/core/java/android/service/voice/IVoiceInteractionService.aidl b/core/java/android/service/voice/IVoiceInteractionService.aidl
index 24819a6..efae5c1 100644
--- a/core/java/android/service/voice/IVoiceInteractionService.aidl
+++ b/core/java/android/service/voice/IVoiceInteractionService.aidl
@@ -16,6 +16,8 @@
package android.service.voice;
+import android.os.Bundle;
+
import com.android.internal.app.IVoiceActionCheckCallback;
/**
@@ -28,4 +30,6 @@
void launchVoiceAssistFromKeyguard();
void getActiveServiceSupportedActions(in List<String> voiceActions,
in IVoiceActionCheckCallback callback);
+ void prepareToShowSession(in Bundle args, int flags);
+ void showSessionFailed();
}
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index 9e1518d..d49b5a5 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -160,6 +160,20 @@
voiceActions,
callback));
}
+
+ @Override
+ public void prepareToShowSession(Bundle args, int flags) {
+ Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
+ VoiceInteractionService::onPrepareToShowSession,
+ VoiceInteractionService.this, args, flags));
+ }
+
+ @Override
+ public void showSessionFailed() {
+ Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
+ VoiceInteractionService::onShowSessionFailed,
+ VoiceInteractionService.this));
+ }
};
IVoiceInteractionManagerService mSystemService;
@@ -184,6 +198,31 @@
}
/**
+ * Notify the interactor when the system prepares to show session. The system is going to
+ * bind the session service.
+ *
+ * @param args The arguments that were supplied to {@link #showSession(Bundle, int)}.
+ * @param flags The show flags originally provided to {@link #showSession(Bundle, int)}.
+ * @see #showSession(Bundle, int)
+ * @see #onShowSessionFailed()
+ * @see VoiceInteractionSession#onShow(Bundle, int)
+ * @see VoiceInteractionSession#show(Bundle, int)
+ */
+ public void onPrepareToShowSession(@NonNull Bundle args, int flags) {
+ }
+
+ /**
+ * Called when the show session failed. E.g. When the system bound the session service failed.
+ *
+ * @see #showSession(Bundle, int)
+ * @see #onPrepareToShowSession(Bundle, int)
+ * @see VoiceInteractionSession#onShow(Bundle, int)
+ * @see VoiceInteractionSession#show(Bundle, int)
+ */
+ public void onShowSessionFailed() {
+ }
+
+ /**
* Check whether the given service component is the currently active
* VoiceInteractionService.
*/
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index b91019c..84bbdd1 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -1283,6 +1283,8 @@
* swipe gesture starts at X = 500 then moves to X = 400, this axis would have a value of
* -0.1.
* </ul>
+ * These values are relative to the state from the last event, not accumulated, so developers
+ * should make sure to process this axis value for all batched historical events.
*/
public static final int AXIS_GESTURE_X_OFFSET = 48;
@@ -1300,6 +1302,8 @@
* <li>For a touch pad, reports the distance that should be scrolled in the X axis as a result
* of the user's two-finger scroll gesture, in display pixels.
* </ul>
+ * These values are relative to the state from the last event, not accumulated, so developers
+ * should make sure to process this axis value for all batched historical events.
*/
public static final int AXIS_GESTURE_SCROLL_X_DISTANCE = 50;
@@ -1310,6 +1314,19 @@
*/
public static final int AXIS_GESTURE_SCROLL_Y_DISTANCE = 51;
+ /**
+ * Axis constant: pinch scale factor of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a touch pad, reports the change in distance between the fingers when the user is
+ * making a pinch gesture, as a proportion of the previous distance. For example, if the fingers
+ * were 50 units apart and are now 52 units apart, the scale factor would be 1.04.
+ * </ul>
+ * These values are relative to the state from the last event, not accumulated, so developers
+ * should make sure to process this axis value for all batched historical events.
+ */
+ public static final int AXIS_GESTURE_PINCH_SCALE_FACTOR = 52;
+
// NOTE: If you add a new axis here you must also add it to:
// frameworks/native/include/android/input.h
// frameworks/native/libs/input/InputEventLabels.cpp
@@ -1369,6 +1386,7 @@
names.append(AXIS_GESTURE_Y_OFFSET, "AXIS_GESTURE_Y_OFFSET");
names.append(AXIS_GESTURE_SCROLL_X_DISTANCE, "AXIS_GESTURE_SCROLL_X_DISTANCE");
names.append(AXIS_GESTURE_SCROLL_Y_DISTANCE, "AXIS_GESTURE_SCROLL_Y_DISTANCE");
+ names.append(AXIS_GESTURE_PINCH_SCALE_FACTOR, "AXIS_GESTURE_PINCH_SCALE_FACTOR");
}
/**
@@ -1522,11 +1540,22 @@
*/
public static final int CLASSIFICATION_MULTI_FINGER_SWIPE = 4;
+ /**
+ * Classification constant: touchpad pinch.
+ *
+ * The current event stream represents the user pinching with two fingers on a touchpad. The
+ * gesture is centered around the current cursor position.
+ *
+ * @see #getClassification
+ */
+ public static final int CLASSIFICATION_PINCH = 5;
+
/** @hide */
@Retention(SOURCE)
@IntDef(prefix = { "CLASSIFICATION" }, value = {
CLASSIFICATION_NONE, CLASSIFICATION_AMBIGUOUS_GESTURE, CLASSIFICATION_DEEP_PRESS,
- CLASSIFICATION_TWO_FINGER_SWIPE, CLASSIFICATION_MULTI_FINGER_SWIPE})
+ CLASSIFICATION_TWO_FINGER_SWIPE, CLASSIFICATION_MULTI_FINGER_SWIPE,
+ CLASSIFICATION_PINCH})
public @interface Classification {};
/**
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 0198457..c73cfc2 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -141,6 +141,7 @@
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Transformation;
+import android.view.autofill.AutofillFeatureFlags;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
@@ -3662,6 +3663,12 @@
* Indicates that the view enables auto handwriting initiation.
*/
private static final int PFLAG4_AUTO_HANDWRITING_ENABLED = 0x000010000;
+
+ /**
+ * Indicates that the view is important for Credential Manager.
+ */
+ private static final int PFLAG4_IMPORTANT_FOR_CREDENTIAL_MANAGER = 0x000020000;
+
/* End of masks for mPrivateFlags4 */
/** @hide */
@@ -6130,6 +6137,12 @@
setImportantForContentCapture(a.getInt(attr,
IMPORTANT_FOR_CONTENT_CAPTURE_AUTO));
}
+ break;
+ case R.styleable.View_isCredential:
+ if (a.peekValue(attr) != null) {
+ setIsCredential(a.getBoolean(attr, false));
+ }
+ break;
case R.styleable.View_defaultFocusHighlightEnabled:
if (a.peekValue(attr) != null) {
setDefaultFocusHighlightEnabled(a.getBoolean(attr, true));
@@ -10234,6 +10247,10 @@
private boolean isAutofillable() {
if (getAutofillType() == AUTOFILL_TYPE_NONE) return false;
+ // Disable triggering autofill if the view is integrated with CredentialManager.
+ if (AutofillFeatureFlags.shouldIgnoreCredentialViews()
+ && isCredential()) return false;
+
if (!isImportantForAutofill()) {
// View is not important for "regular" autofill, so we must check if Augmented Autofill
// is enabled for the activity
@@ -31861,6 +31878,37 @@
}
/**
+ * Gets the mode for determining whether this view is a credential.
+ *
+ * <p>See {@link #isCredential()}.
+ *
+ * @param isCredential Whether the view is a credential.
+ *
+ * @attr ref android.R.styleable#View_isCredential
+ */
+ public void setIsCredential(boolean isCredential) {
+ if (isCredential) {
+ mPrivateFlags4 |= PFLAG4_IMPORTANT_FOR_CREDENTIAL_MANAGER;
+ } else {
+ mPrivateFlags4 &= ~PFLAG4_IMPORTANT_FOR_CREDENTIAL_MANAGER;
+ }
+ }
+
+ /**
+ * Gets the mode for determining whether this view is a credential.
+ *
+ * <p>See {@link #setIsCredential(boolean)}.
+ *
+ * @return false by default, or value passed to {@link #setIsCredential(boolean)}.
+ *
+ * @attr ref android.R.styleable#View_isCredential
+ */
+ public boolean isCredential() {
+ return ((mPrivateFlags4 & PFLAG4_IMPORTANT_FOR_CREDENTIAL_MANAGER)
+ == PFLAG4_IMPORTANT_FOR_CREDENTIAL_MANAGER);
+ }
+
+ /**
* Set whether this view enables automatic handwriting initiation.
*
* For a view with an active {@link InputConnection}, if auto handwriting is enabled then
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 2114ce7..74e521a 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -8823,6 +8823,10 @@
mAdded = false;
AnimationHandler.removeRequestor(this);
}
+ if (mActiveSurfaceSyncGroup != null) {
+ mActiveSurfaceSyncGroup.markSyncReady();
+ mActiveSurfaceSyncGroup = null;
+ }
WindowManagerGlobal.getInstance().doRemoveView(this);
}
diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java
new file mode 100644
index 0000000..59ad151
--- /dev/null
+++ b/core/java/android/view/autofill/AutofillFeatureFlags.java
@@ -0,0 +1,224 @@
+/*
+ * 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.view.autofill;
+
+import android.annotation.TestApi;
+import android.provider.DeviceConfig;
+import android.text.TextUtils;
+import android.view.View;
+
+import com.android.internal.util.ArrayUtils;
+
+/**
+ * Feature flags associated with autofill.
+ * @hide
+ */
+@TestApi
+public class AutofillFeatureFlags {
+
+ /**
+ * {@code DeviceConfig} property used to set which Smart Suggestion modes for Augmented Autofill
+ * are available.
+ */
+ public static final String DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES =
+ "smart_suggestion_supported_modes";
+
+ /**
+ * Sets how long (in ms) the augmented autofill service is bound while idle.
+ *
+ * <p>Use {@code 0} to keep it permanently bound.
+ *
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT =
+ "augmented_service_idle_unbind_timeout";
+
+ /**
+ * Sets how long (in ms) the augmented autofill service request is killed if not replied.
+ *
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT =
+ "augmented_service_request_timeout";
+
+ /**
+ * Sets allowed list for the autofill compatibility mode.
+ *
+ * The list of packages is {@code ":"} colon delimited, and each entry has the name of the
+ * package and an optional list of url bar resource ids (the list is delimited by
+ * brackets&mdash{@code [} and {@code ]}&mdash and is also comma delimited).
+ *
+ * <p>For example, a list with 3 packages {@code p1}, {@code p2}, and {@code p3}, where
+ * package {@code p1} have one id ({@code url_bar}, {@code p2} has none, and {@code p3 }
+ * have 2 ids {@code url_foo} and {@code url_bas}) would be
+ * {@code p1[url_bar]:p2:p3[url_foo,url_bas]}
+ */
+ public static final String DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES =
+ "compat_mode_allowed_packages";
+
+ /**
+ * Indicates Fill dialog feature enabled or not.
+ */
+ public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED =
+ "autofill_dialog_enabled";
+
+ /**
+ * Sets the autofill hints allowed list for the fields that can trigger the fill dialog
+ * feature at Activity starting.
+ *
+ * The list of autofill hints is {@code ":"} colon delimited.
+ *
+ * <p>For example, a list with 3 hints {@code password}, {@code phone}, and
+ * { @code emailAddress}, would be {@code password:phone:emailAddress}
+ *
+ * Note: By default the password field is enabled even there is no password hint in the list
+ *
+ * @see View#setAutofillHints(String...)
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_HINTS =
+ "autofill_dialog_hints";
+
+ // START CREDENTIAL MANAGER FLAGS //
+
+ /**
+ * Indicates whether credential manager tagged views should be ignored from autofill structures.
+ * This flag is further gated by {@link #DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED}
+ */
+ public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_IGNORE_VIEWS =
+ "autofill_credential_manager_ignore_views";
+
+ /**
+ * Indicates CredentialManager feature enabled or not.
+ * This is the overall feature flag. Individual behavior of credential manager may be controlled
+ * via a different flag, but gated by this flag.
+ */
+ public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED =
+ "autofill_credential_manager_enabled";
+
+ /**
+ * Indicates whether credential manager tagged views should suppress fill dialog.
+ * This flag is further gated by {@link #DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED}
+ *
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_SUPPRESS_FILL_DIALOG =
+ "autofill_credential_manager_suppress_fill_dialog";
+
+
+
+ /**
+ * Indicates whether credential manager tagged views should suppress save dialog.
+ * This flag is further gated by {@link #DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED}
+ *
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_SUPPRESS_SAVE_DIALOG =
+ "autofill_credential_manager_suppress_save_dialog";
+ // END CREDENTIAL MANAGER FLAGS //
+
+ /**
+ * Sets a value of delay time to show up the inline tooltip view.
+ *
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_AUTOFILL_TOOLTIP_SHOW_UP_DELAY =
+ "autofill_inline_tooltip_first_show_delay";
+
+ private static final String DIALOG_HINTS_DELIMITER = ":";
+
+ private static final boolean DEFAULT_HAS_FILL_DIALOG_UI_FEATURE = false;
+ private static final String DEFAULT_FILL_DIALOG_ENABLED_HINTS = "";
+
+ // CREDENTIAL MANAGER DEFAULTS
+ // Credential manager is enabled by default so as to allow testing by app developers
+ private static final boolean DEFAULT_CREDENTIAL_MANAGER_ENABLED = true;
+ private static final boolean DEFAULT_CREDENTIAL_MANAGER_IGNORE_VIEWS = true;
+ private static final boolean DEFAULT_CREDENTIAL_MANAGER_SUPPRESS_FILL_DIALOG = false;
+ private static final boolean DEFAULT_CREDENTIAL_MANAGER_SUPPRESS_SAVE_DIALOG = false;
+ // END CREDENTIAL MANAGER DEFAULTS
+
+ private AutofillFeatureFlags() {};
+
+ /**
+ * Whether the fill dialog feature is enabled or not
+ *
+ * @hide
+ */
+ public static boolean isFillDialogEnabled() {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_AUTOFILL,
+ DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED,
+ DEFAULT_HAS_FILL_DIALOG_UI_FEATURE);
+ }
+
+ /**
+ * Gets fill dialog enabled hints.
+ *
+ * @hide
+ */
+ public static String[] getFillDialogEnabledHints() {
+ final String dialogHints = DeviceConfig.getString(
+ DeviceConfig.NAMESPACE_AUTOFILL,
+ DEVICE_CONFIG_AUTOFILL_DIALOG_HINTS,
+ DEFAULT_FILL_DIALOG_ENABLED_HINTS);
+ if (TextUtils.isEmpty(dialogHints)) {
+ return new String[0];
+ }
+
+ return ArrayUtils.filter(dialogHints.split(DIALOG_HINTS_DELIMITER), String[]::new,
+ (str) -> !TextUtils.isEmpty(str));
+ }
+
+ /**
+ * Whether the Credential Manager feature is enabled or not
+ *
+ * @hide
+ */
+ public static boolean isCredentialManagerEnabled() {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_AUTOFILL,
+ DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED,
+ DEFAULT_CREDENTIAL_MANAGER_ENABLED);
+ }
+
+ /**
+ * Whether credential manager tagged views should be ignored for autofill structure.
+ *
+ * @hide
+ */
+ public static boolean shouldIgnoreCredentialViews() {
+ return isCredentialManagerEnabled()
+ && DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_AUTOFILL,
+ DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_IGNORE_VIEWS,
+ DEFAULT_CREDENTIAL_MANAGER_IGNORE_VIEWS);
+ }
+
+ /**
+ * Whether credential manager tagged views should not trigger fill dialog requests.
+ *
+ * @hide
+ */
+ public static boolean isFillDialogDisabledForCredentialManager() {
+ return isCredentialManagerEnabled()
+ && DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_AUTOFILL,
+ DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_SUPPRESS_FILL_DIALOG,
+ DEFAULT_CREDENTIAL_MANAGER_SUPPRESS_FILL_DIALOG);
+ }
+}
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index a92bc94..58e7a70 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -60,7 +60,6 @@
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.SystemClock;
-import android.provider.DeviceConfig;
import android.service.autofill.AutofillService;
import android.service.autofill.FillCallback;
import android.service.autofill.FillEventHistory;
@@ -450,88 +449,6 @@
@Retention(RetentionPolicy.SOURCE)
public @interface SmartSuggestionMode {}
- /**
- * {@code DeviceConfig} property used to set which Smart Suggestion modes for Augmented Autofill
- * are available.
- *
- * @hide
- */
- @TestApi
- public static final String DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES =
- "smart_suggestion_supported_modes";
-
- /**
- * Sets how long (in ms) the augmented autofill service is bound while idle.
- *
- * <p>Use {@code 0} to keep it permanently bound.
- *
- * @hide
- */
- public static final String DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT =
- "augmented_service_idle_unbind_timeout";
-
- /**
- * Sets how long (in ms) the augmented autofill service request is killed if not replied.
- *
- * @hide
- */
- public static final String DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT =
- "augmented_service_request_timeout";
-
- /**
- * Sets allowed list for the autofill compatibility mode.
- *
- * The list of packages is {@code ":"} colon delimited, and each entry has the name of the
- * package and an optional list of url bar resource ids (the list is delimited by
- * brackets&mdash{@code [} and {@code ]}&mdash and is also comma delimited).
- *
- * <p>For example, a list with 3 packages {@code p1}, {@code p2}, and {@code p3}, where
- * package {@code p1} have one id ({@code url_bar}, {@code p2} has none, and {@code p3 }
- * have 2 ids {@code url_foo} and {@code url_bas}) would be
- * {@code p1[url_bar]:p2:p3[url_foo,url_bas]}
- *
- * @hide
- */
- @TestApi
- public static final String DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES =
- "compat_mode_allowed_packages";
-
- /**
- * Sets the fill dialog feature enabled or not.
- *
- * @hide
- */
- @TestApi
- public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED =
- "autofill_dialog_enabled";
-
- /**
- * Sets the autofill hints allowed list for the fields that can trigger the fill dialog
- * feature at Activity starting.
- *
- * The list of autofill hints is {@code ":"} colon delimited.
- *
- * <p>For example, a list with 3 hints {@code password}, {@code phone}, and
- * {@code emailAddress}, would be {@code password:phone:emailAddress}
- *
- * Note: By default the password field is enabled even there is no password hint in the list
- *
- * @see View#setAutofillHints(String...)
- * @hide
- */
- public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_HINTS =
- "autofill_dialog_hints";
-
- /**
- * Sets a value of delay time to show up the inline tooltip view.
- *
- * @hide
- */
- public static final String DEVICE_CONFIG_AUTOFILL_TOOLTIP_SHOW_UP_DELAY =
- "autofill_inline_tooltip_first_show_delay";
-
- private static final String DIALOG_HINTS_DELIMITER = ":";
-
/** @hide */
public static final int RESULT_OK = 0;
/** @hide */
@@ -634,9 +551,6 @@
*/
public static final int NO_SESSION = Integer.MAX_VALUE;
- private static final boolean HAS_FILL_DIALOG_UI_FEATURE_DEFAULT = false;
- private static final String FILL_DIALOG_ENABLED_DEFAULT_HINTS = "";
-
private final IAutoFillManager mService;
private final Object mLock = new Object();
@@ -891,11 +805,8 @@
mOptions = context.getAutofillOptions();
mIsFillRequested = new AtomicBoolean(false);
- mIsFillDialogEnabled = DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_AUTOFILL,
- DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED,
- HAS_FILL_DIALOG_UI_FEATURE_DEFAULT);
- mFillDialogEnabledHints = getFillDialogEnabledHints();
+ mIsFillDialogEnabled = AutofillFeatureFlags.isFillDialogEnabled();
+ mFillDialogEnabledHints = AutofillFeatureFlags.getFillDialogEnabledHints();
if (sDebug) {
Log.d(TAG, "Fill dialog is enabled:" + mIsFillDialogEnabled
+ ", hints=" + Arrays.toString(mFillDialogEnabledHints));
@@ -907,19 +818,6 @@
}
}
- private String[] getFillDialogEnabledHints() {
- final String dialogHints = DeviceConfig.getString(
- DeviceConfig.NAMESPACE_AUTOFILL,
- DEVICE_CONFIG_AUTOFILL_DIALOG_HINTS,
- FILL_DIALOG_ENABLED_DEFAULT_HINTS);
- if (TextUtils.isEmpty(dialogHints)) {
- return new String[0];
- }
-
- return ArrayUtils.filter(dialogHints.split(DIALOG_HINTS_DELIMITER), String[]::new,
- (str) -> !TextUtils.isEmpty(str));
- }
-
/**
* @hide
*/
@@ -1190,16 +1088,28 @@
}
/**
- * The {@link #DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED} is {@code true} or the view have
- * the allowed autofill hints, performs a fill request to know there is any field supported
- * fill dialog.
+ * The {@link AutofillFeatureFlags#DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED} is {@code true} or
+ * the view have the allowed autofill hints, performs a fill request to know there is any field
+ * supported fill dialog.
*
* @hide
*/
public void notifyViewEnteredForFillDialog(View v) {
+ if (sDebug) {
+ Log.d(TAG, "notifyViewEnteredForFillDialog:" + v.getAutofillId());
+ }
if (!hasAutofillFeature()) {
return;
}
+ if (AutofillFeatureFlags.isFillDialogDisabledForCredentialManager()
+ && v.isCredential()) {
+ if (sDebug) {
+ Log.d(TAG, "Ignoring Fill Dialog request since important for credMan:"
+ + v.getAutofillId().toString());
+ }
+ return;
+ }
+
synchronized (mLock) {
if (mTrackedViews != null) {
// To support the fill dialog can show for the autofillable Views in
@@ -1227,8 +1137,8 @@
synchronized (mLock) {
// To match the id of the IME served view, used AutofillId.NO_AUTOFILL_ID on prefill
// request, because IME will reset the id of IME served view to 0 when activity
- // start and does not focus on any view. If the id of the prefill request is
- // not match to the IME served view's, Autofill will be blocking to wait inline
+ // start and does not focus on any view. If the id of the prefill request does
+ // not match the IME served view's, Autofill will be blocking to wait inline
// request from the IME.
notifyViewEnteredLocked(/* view= */ null, AutofillId.NO_AUTOFILL_ID,
/* bounds= */ null, /* value= */ null, flags);
@@ -4075,6 +3985,7 @@
}
}
+ @Override
public void notifyFillDialogTriggerIds(List<AutofillId> ids) {
final AutofillManager afm = mAfm.get();
if (afm != null) {
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 4005bc8..d6c2d30 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -937,6 +937,13 @@
private List<Path> mHighlightPaths;
private List<Paint> mHighlightPaints;
private Highlights mHighlights;
+ private int[] mSearchResultHighlights = null;
+ private Paint mSearchResultHighlightPaint = null;
+ private Paint mFocusedSearchResultHighlightPaint = null;
+ private int mFocusedSearchResultHighlightColor = 0xFFFF9632;
+ private int mSearchResultHighlightColor = 0xFFFFFF00;
+
+ private int mFocusedSearchResultIndex = -1;
private int mGesturePreviewHighlightStart = -1;
private int mGesturePreviewHighlightEnd = -1;
private Paint mGesturePreviewHighlightPaint;
@@ -4030,6 +4037,8 @@
*/
private static class TextAppearanceAttributes {
int mTextColorHighlight = 0;
+ int mSearchResultHighlightColor = 0;
+ int mFocusedSearchResultHighlightColor = 0;
ColorStateList mTextColor = null;
ColorStateList mTextColorHint = null;
ColorStateList mTextColorLink = null;
@@ -4062,6 +4071,9 @@
public String toString() {
return "TextAppearanceAttributes {\n"
+ " mTextColorHighlight:" + mTextColorHighlight + "\n"
+ + " mSearchResultHighlightColor: " + mSearchResultHighlightColor + "\n"
+ + " mFocusedSearchResultHighlightColor: "
+ + mFocusedSearchResultHighlightColor + "\n"
+ " mTextColor:" + mTextColor + "\n"
+ " mTextColorHint:" + mTextColorHint + "\n"
+ " mTextColorLink:" + mTextColorLink + "\n"
@@ -4100,6 +4112,11 @@
static {
sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorHighlight,
com.android.internal.R.styleable.TextAppearance_textColorHighlight);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_searchResultHighlightColor,
+ com.android.internal.R.styleable.TextAppearance_searchResultHighlightColor);
+ sAppearanceValues.put(
+ com.android.internal.R.styleable.TextView_focusedSearchResultHighlightColor,
+ com.android.internal.R.styleable.TextAppearance_focusedSearchResultHighlightColor);
sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColor,
com.android.internal.R.styleable.TextAppearance_textColor);
sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorHint,
@@ -4174,6 +4191,16 @@
attributes.mTextColorHighlight =
appearance.getColor(attr, attributes.mTextColorHighlight);
break;
+ case com.android.internal.R.styleable.TextAppearance_searchResultHighlightColor:
+ attributes.mSearchResultHighlightColor =
+ appearance.getColor(attr, attributes.mSearchResultHighlightColor);
+ break;
+ case com.android.internal.R.styleable
+ .TextAppearance_focusedSearchResultHighlightColor:
+ attributes.mFocusedSearchResultHighlightColor =
+ appearance.getColor(attr,
+ attributes.mFocusedSearchResultHighlightColor);
+ break;
case com.android.internal.R.styleable.TextAppearance_textColor:
attributes.mTextColor = appearance.getColorStateList(attr);
break;
@@ -4290,6 +4317,14 @@
setHighlightColor(attributes.mTextColorHighlight);
}
+ if (attributes.mSearchResultHighlightColor != 0) {
+ setSearchResultHighlightColor(attributes.mSearchResultHighlightColor);
+ }
+
+ if (attributes.mFocusedSearchResultHighlightColor != 0) {
+ setFocusedSearchResultHighlightColor(attributes.mFocusedSearchResultHighlightColor);
+ }
+
if (attributes.mTextSize != -1) {
mTextSizeUnit = attributes.mTextSizeUnit;
setRawTextSize(attributes.mTextSize, true /* shouldRequestLayout */);
@@ -6176,6 +6211,238 @@
}
/**
+ * Sets the search result ranges with flatten range representation.
+ *
+ * Ranges are represented of flattened inclusive start and exclusive end integers array. The
+ * inclusive start offset of the {@code i}-th range is stored in {@code 2 * i}-th of the array.
+ * The exclusive end offset of the {@code i}-th range is stored in {@code 2* i + 1}-th of the
+ * array. For example, the two ranges: (1, 2) and (3, 4) are flattened into single int array
+ * [1, 2, 3, 4].
+ *
+ * TextView will render the search result with the highlights with specified color in the theme.
+ * If there is a focused search result, it is rendered with focused color. By calling this
+ * method, the focused search index will be cleared.
+ *
+ * @attr ref android.R.styleable#TextView_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextView_focusedSearchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_focusedSearchResultHighlightColor
+ *
+ * @see #getSearchResultHighlights()
+ * @see #setFocusedSearchResultIndex(int)
+ * @see #getFocusedSearchResultIndex()
+ * @see #setSearchResultHighlightColor(int)
+ * @see #getSearchResultHighlightColor()
+ * @see #setFocusedSearchResultHighlightColor(int)
+ * @see #getFocusedSearchResultHighlightColor()
+ *
+ * @param ranges the flatten ranges of the search result. null for clear.
+ */
+ public void setSearchResultHighlights(@Nullable int... ranges) {
+ if (ranges == null) {
+ mSearchResultHighlights = null;
+ mHighlightPathsBogus = true;
+ return;
+ }
+ if (ranges.length % 2 == 1) {
+ throw new IllegalArgumentException(
+ "Flatten ranges must have even numbered elements");
+ }
+ for (int j = 0; j < ranges.length / 2; ++j) {
+ int start = ranges[j * 2];
+ int end = ranges[j * 2 + 1];
+ if (start > end) {
+ throw new IllegalArgumentException(
+ "Reverse range found in the flatten range: " + start + ", " + end + ""
+ + " at " + j + "-th range");
+ }
+ }
+ mHighlightPathsBogus = true;
+ mSearchResultHighlights = ranges;
+ mFocusedSearchResultIndex = FOCUSED_SEARCH_RESULT_INDEX_NONE;
+ invalidate();
+ }
+
+ /**
+ * Gets the current search result ranges.
+ *
+ * @see #setSearchResultHighlights(int[])
+ * @see #setFocusedSearchResultIndex(int)
+ * @see #getFocusedSearchResultIndex()
+ * @see #setSearchResultHighlightColor(int)
+ * @see #getSearchResultHighlightColor()
+ * @see #setFocusedSearchResultHighlightColor(int)
+ * @see #getFocusedSearchResultHighlightColor()
+ *
+ * @return a flatten search result ranges. null if not available.
+ */
+ @Nullable
+ public int[] getSearchResultHighlights() {
+ return mSearchResultHighlights;
+ }
+
+ /**
+ * A special index used for {@link #setFocusedSearchResultIndex(int)} and
+ * {@link #getFocusedSearchResultIndex()} inidicating there is no focused search result.
+ */
+ public static final int FOCUSED_SEARCH_RESULT_INDEX_NONE = -1;
+
+ /**
+ * Sets the focused search result index.
+ *
+ * The focused search result is drawn in a focused color.
+ * Calling {@link #FOCUSED_SEARCH_RESULT_INDEX_NONE} for clearing focused search result.
+ *
+ * This method must be called after setting search result ranges by
+ * {@link #setSearchResultHighlights(int[])}.
+ *
+ * @attr ref android.R.styleable#TextView_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextView_focusedSearchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_focusedSearchResultHighlightColor
+ *
+ * @see #setSearchResultHighlights(int[])
+ * @see #getSearchResultHighlights()
+ * @see #setFocusedSearchResultIndex(int)
+ * @see #getFocusedSearchResultIndex()
+ * @see #setSearchResultHighlightColor(int)
+ * @see #getSearchResultHighlightColor()
+ * @see #setFocusedSearchResultHighlightColor(int)
+ * @see #getFocusedSearchResultHighlightColor()
+ *
+ * @param index a focused search index or {@link #FOCUSED_SEARCH_RESULT_INDEX_NONE}
+ */
+ public void setFocusedSearchResultIndex(int index) {
+ if (mSearchResultHighlights == null) {
+ throw new IllegalArgumentException("Search result range must be set beforehand.");
+ }
+ if (index < -1 || index >= mSearchResultHighlights.length / 2) {
+ throw new IllegalArgumentException("Focused index(" + index + ") must be larger than "
+ + "-1 and less than range count(" + (mSearchResultHighlights.length / 2) + ")");
+ }
+ mFocusedSearchResultIndex = index;
+ mHighlightPathsBogus = true;
+ invalidate();
+ }
+
+ /**
+ * Gets the focused search result index.
+ *
+ * @attr ref android.R.styleable#TextView_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextView_focusedSearchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_focusedSearchResultHighlightColor
+ *
+ * @see #setSearchResultHighlights(int[])
+ * @see #getSearchResultHighlights()
+ * @see #setFocusedSearchResultIndex(int)
+ * @see #getFocusedSearchResultIndex()
+ * @see #setSearchResultHighlightColor(int)
+ * @see #getSearchResultHighlightColor()
+ * @see #setFocusedSearchResultHighlightColor(int)
+ * @see #getFocusedSearchResultHighlightColor()
+
+ * @return a focused search index or {@link #FOCUSED_SEARCH_RESULT_INDEX_NONE}
+ */
+ public int getFocusedSearchResultIndex() {
+ return mFocusedSearchResultIndex;
+ }
+
+ /**
+ * Sets the search result highlight color.
+ *
+ * @attr ref android.R.styleable#TextView_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextView_focusedSearchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_focusedSearchResultHighlightColor
+ *
+ * @see #setSearchResultHighlights(int[])
+ * @see #getSearchResultHighlights()
+ * @see #setFocusedSearchResultIndex(int)
+ * @see #getFocusedSearchResultIndex()
+ * @see #setSearchResultHighlightColor(int)
+ * @see #getSearchResultHighlightColor()
+ * @see #setFocusedSearchResultHighlightColor(int)
+ * @see #getFocusedSearchResultHighlightColor()
+
+ * @param color a search result highlight color.
+ */
+ public void setSearchResultHighlightColor(@ColorInt int color) {
+ mSearchResultHighlightColor = color;
+ }
+
+ /**
+ * Gets the search result highlight color.
+ *
+ * @attr ref android.R.styleable#TextView_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextView_focusedSearchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_focusedSearchResultHighlightColor
+ *
+ * @see #setSearchResultHighlights(int[])
+ * @see #getSearchResultHighlights()
+ * @see #setFocusedSearchResultIndex(int)
+ * @see #getFocusedSearchResultIndex()
+ * @see #setSearchResultHighlightColor(int)
+ * @see #getSearchResultHighlightColor()
+ * @see #setFocusedSearchResultHighlightColor(int)
+ * @see #getFocusedSearchResultHighlightColor()
+
+ * @return a search result highlight color.
+ */
+ @ColorInt
+ public int getSearchResultHighlightColor() {
+ return mSearchResultHighlightColor;
+ }
+
+ /**
+ * Sets focused search result highlight color.
+ *
+ * @attr ref android.R.styleable#TextView_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextView_focusedSearchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_focusedSearchResultHighlightColor
+ *
+ * @see #setSearchResultHighlights(int[])
+ * @see #getSearchResultHighlights()
+ * @see #setFocusedSearchResultIndex(int)
+ * @see #getFocusedSearchResultIndex()
+ * @see #setSearchResultHighlightColor(int)
+ * @see #getSearchResultHighlightColor()
+ * @see #setFocusedSearchResultHighlightColor(int)
+ * @see #getFocusedSearchResultHighlightColor()
+
+ * @param color a focused search result highlight color.
+ */
+ public void setFocusedSearchResultHighlightColor(@ColorInt int color) {
+ mFocusedSearchResultHighlightColor = color;
+ }
+
+ /**
+ * Gets focused search result highlight color.
+ *
+ * @attr ref android.R.styleable#TextView_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_searchResultHighlightColor
+ * @attr ref android.R.styleable#TextView_focusedSearchResultHighlightColor
+ * @attr ref android.R.styleable#TextAppearance_focusedSearchResultHighlightColor
+ *
+ * @see #setSearchResultHighlights(int[])
+ * @see #getSearchResultHighlights()
+ * @see #setFocusedSearchResultIndex(int)
+ * @see #getFocusedSearchResultIndex()
+ * @see #setSearchResultHighlightColor(int)
+ * @see #getSearchResultHighlightColor()
+ * @see #setFocusedSearchResultHighlightColor(int)
+ * @see #getFocusedSearchResultHighlightColor()
+
+ * @return a focused search result highlight color.
+ */
+ @ColorInt
+ public int getFocusedSearchResultHighlightColor() {
+ return mFocusedSearchResultHighlightColor;
+ }
+
+ /**
* Highlights the text range (from inclusive start offset to exclusive end offset) to show what
* will be selected by the ongoing select handwriting gesture. While the gesture preview
* highlight is shown, the selection or cursor is hidden. If the text or selection is changed,
@@ -8335,7 +8602,6 @@
for (int i = 0; i < mHighlights.getSize(); ++i) {
final int[] ranges = mHighlights.getRanges(i);
final Paint paint = mHighlights.getPaint(i);
-
final Path path;
if (mPathRecyclePool.isEmpty()) {
path = new Path();
@@ -8363,6 +8629,8 @@
}
}
+ addSearchHighlightPaths();
+
if (hasGesturePreviewHighlight()) {
final Path path;
if (mPathRecyclePool.isEmpty()) {
@@ -8381,6 +8649,67 @@
mHighlightPathsBogus = false;
}
+ private void addSearchHighlightPaths() {
+ if (mSearchResultHighlights != null) {
+ final Path searchResultPath;
+ if (mPathRecyclePool.isEmpty()) {
+ searchResultPath = new Path();
+ } else {
+ searchResultPath = mPathRecyclePool.get(mPathRecyclePool.size() - 1);
+ mPathRecyclePool.remove(mPathRecyclePool.size() - 1);
+ searchResultPath.reset();
+ }
+ final Path focusedSearchResultPath;
+ if (mFocusedSearchResultIndex == FOCUSED_SEARCH_RESULT_INDEX_NONE) {
+ focusedSearchResultPath = null;
+ } else if (mPathRecyclePool.isEmpty()) {
+ focusedSearchResultPath = new Path();
+ } else {
+ focusedSearchResultPath = mPathRecyclePool.get(mPathRecyclePool.size() - 1);
+ mPathRecyclePool.remove(mPathRecyclePool.size() - 1);
+ focusedSearchResultPath.reset();
+ }
+
+ boolean atLeastOnePathAdded = false;
+ for (int j = 0; j < mSearchResultHighlights.length / 2; ++j) {
+ final int start = mSearchResultHighlights[2 * j];
+ final int end = mSearchResultHighlights[2 * j + 1];
+ if (start < end) {
+ if (j == mFocusedSearchResultIndex) {
+ mLayout.getSelection(start, end, (left, top, right, bottom, layout) ->
+ focusedSearchResultPath.addRect(left, top, right, bottom,
+ Path.Direction.CW)
+ );
+ } else {
+ mLayout.getSelection(start, end, (left, top, right, bottom, layout) ->
+ searchResultPath.addRect(left, top, right, bottom,
+ Path.Direction.CW)
+ );
+ atLeastOnePathAdded = true;
+ }
+ }
+ }
+ if (atLeastOnePathAdded) {
+ if (mSearchResultHighlightPaint == null) {
+ mSearchResultHighlightPaint = new Paint();
+ }
+ mSearchResultHighlightPaint.setColor(mSearchResultHighlightColor);
+ mSearchResultHighlightPaint.setStyle(Paint.Style.FILL);
+ mHighlightPaths.add(searchResultPath);
+ mHighlightPaints.add(mSearchResultHighlightPaint);
+ }
+ if (focusedSearchResultPath != null) {
+ if (mFocusedSearchResultHighlightPaint == null) {
+ mFocusedSearchResultHighlightPaint = new Paint();
+ }
+ mFocusedSearchResultHighlightPaint.setColor(mFocusedSearchResultHighlightColor);
+ mFocusedSearchResultHighlightPaint.setStyle(Paint.Style.FILL);
+ mHighlightPaths.add(focusedSearchResultPath);
+ mHighlightPaints.add(mFocusedSearchResultHighlightPaint);
+ }
+ }
+ }
+
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private Path getUpdatedHighlightPath() {
Path highlight = null;
@@ -10826,6 +11155,26 @@
* This has to be called after layout. Returns true if anything changed.
*/
public boolean bringPointIntoView(int offset) {
+ return bringPointIntoView(offset, false);
+ }
+
+ /**
+ * Move the insertion position of the given offset into visible area of the View.
+ *
+ * If the View is focused or {@code requestRectWithoutFocus} is set to true, this API may call
+ * {@link View#requestRectangleOnScreen(Rect)} to bring the point to the visible area if
+ * necessary.
+ *
+ * @param offset an offset of the character.
+ * @param requestRectWithoutFocus True for calling {@link View#requestRectangleOnScreen(Rect)}
+ * in the unfocused state. False for calling it only the View has
+ * the focus.
+ * @return true if anything changed, otherwise false.
+ *
+ * @see #bringPointIntoView(int)
+ */
+ public boolean bringPointIntoView(@IntRange(from = 0) int offset,
+ boolean requestRectWithoutFocus) {
if (isLayoutRequested()) {
mDeferScroll = offset;
return false;
@@ -11002,7 +11351,7 @@
changed = true;
}
- if (isFocused()) {
+ if (requestRectWithoutFocus && isFocused()) {
// This offsets because getInterestingRect() is in terms of viewport coordinates, but
// requestRectangleOnScreen() is in terms of content coordinates.
diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java
index 3272c3b..0d6a58b 100644
--- a/core/java/android/window/TaskFragmentOperation.java
+++ b/core/java/android/window/TaskFragmentOperation.java
@@ -58,14 +58,17 @@
/** Sets two TaskFragments adjacent to each other. */
public static final int OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS = 4;
+ /** Clears the adjacent TaskFragments relationship. */
+ public static final int OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS = 5;
+
/** Requests focus on the top running Activity in the given TaskFragment. */
- public static final int OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT = 5;
+ public static final int OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT = 6;
/** Sets a given TaskFragment to have a companion TaskFragment. */
- public static final int OP_TYPE_SET_COMPANION_TASK_FRAGMENT = 6;
+ public static final int OP_TYPE_SET_COMPANION_TASK_FRAGMENT = 7;
/** Sets the {@link TaskFragmentAnimationParams} for the given TaskFragment. */
- public static final int OP_TYPE_SET_ANIMATION_PARAMS = 7;
+ public static final int OP_TYPE_SET_ANIMATION_PARAMS = 8;
@IntDef(prefix = { "OP_TYPE_" }, value = {
OP_TYPE_UNKNOWN,
@@ -74,6 +77,7 @@
OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT,
OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT,
OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS,
+ OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS,
OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT,
OP_TYPE_SET_COMPANION_TASK_FRAGMENT,
OP_TYPE_SET_ANIMATION_PARAMS
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 80b2161..cc48d46 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -16,6 +16,7 @@
package android.window;
+import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS;
import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT;
@@ -582,16 +583,14 @@
* {@link #setAdjacentRoots(WindowContainerToken, WindowContainerToken)}, but can be used with
* fragmentTokens when that TaskFragments haven't been created (but will be created in the same
* {@link WindowContainerTransaction}).
- * To reset it, pass {@code null} for {@code fragmentToken2}.
* @param fragmentToken1 client assigned unique token to create TaskFragment with specified
* in {@link TaskFragmentCreationParams#getFragmentToken()}.
* @param fragmentToken2 client assigned unique token to create TaskFragment with specified
- * in {@link TaskFragmentCreationParams#getFragmentToken()}. If it is
- * {@code null}, the transaction will reset the adjacent TaskFragment.
+ * in {@link TaskFragmentCreationParams#getFragmentToken()}.
*/
@NonNull
public WindowContainerTransaction setAdjacentTaskFragments(
- @NonNull IBinder fragmentToken1, @Nullable IBinder fragmentToken2,
+ @NonNull IBinder fragmentToken1, @NonNull IBinder fragmentToken2,
@Nullable TaskFragmentAdjacentParams params) {
final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS)
@@ -602,6 +601,21 @@
}
/**
+ * Clears the adjacent TaskFragments relationship that is previously set through
+ * {@link #setAdjacentTaskFragments}. Clear operation on one TaskFragment will also clear its
+ * current adjacent TaskFragment's.
+ * @param fragmentToken client assigned unique token to create TaskFragment with specified
+ * in {@link TaskFragmentCreationParams#getFragmentToken()}.
+ */
+ @NonNull
+ public WindowContainerTransaction clearAdjacentTaskFragments(@NonNull IBinder fragmentToken) {
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS)
+ .build();
+ return addTaskFragmentOperation(fragmentToken, operation);
+ }
+
+ /**
* If `container` was brought to front as a transient-launch (eg. recents), this will reorder
* the container back to where it was prior to the transient-launch. This way if a transient
* launch is "aborted", the z-ordering of containers in WM should be restored to before the
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 02cbd41..9f283d4 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -2953,12 +2953,24 @@
private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
return shouldShowTabs()
- && mMultiProfilePagerAdapter.getListAdapterForUserHandle(
- UserHandle.of(UserHandle.myUserId())).getCount() > 0
+ && (mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ UserHandle.of(UserHandle.myUserId())).getCount() > 0
+ || shouldShowContentPreviewWhenEmpty())
&& shouldShowContentPreview();
}
/**
+ * This method could be used to override the default behavior when we hide the preview area
+ * when the current tab doesn't have any items.
+ *
+ * @return true if we want to show the content preview area even if the tab for the current
+ * user is empty
+ */
+ protected boolean shouldShowContentPreviewWhenEmpty() {
+ return false;
+ }
+
+ /**
* @return true if we want to show the content preview area
*/
protected boolean shouldShowContentPreview() {
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 0706ee5..f098e2c 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -209,7 +209,7 @@
* <p>Can only be used if there is a work profile.
* <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
*/
- static final String EXTRA_SELECTED_PROFILE =
+ protected static final String EXTRA_SELECTED_PROFILE =
"com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE";
/**
@@ -224,8 +224,8 @@
static final String EXTRA_CALLING_USER =
"com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER";
- static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
- static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
+ protected static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
+ protected static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
private BroadcastReceiver mWorkProfileStateReceiver;
private UserHandle mHeaderCreatorUser;
diff --git a/core/java/com/android/internal/app/UnlaunchableAppActivity.java b/core/java/com/android/internal/app/UnlaunchableAppActivity.java
index 0776572..e47c335 100644
--- a/core/java/com/android/internal/app/UnlaunchableAppActivity.java
+++ b/core/java/com/android/internal/app/UnlaunchableAppActivity.java
@@ -28,11 +28,13 @@
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentSender;
+import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.os.UserManager;
+import android.telecom.TelecomManager;
import android.util.Log;
import android.view.Window;
@@ -52,6 +54,7 @@
private int mUserId;
private int mReason;
private IntentSender mTarget;
+ private TelecomManager mTelecomManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -60,9 +63,11 @@
// TODO: Use AlertActivity so we don't need to hide title bar and create a dialog
requestWindowFeature(Window.FEATURE_NO_TITLE);
Intent intent = getIntent();
+ mTelecomManager = getSystemService(TelecomManager.class);
mReason = intent.getIntExtra(EXTRA_UNLAUNCHABLE_REASON, -1);
mUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
- mTarget = intent.getParcelableExtra(Intent.EXTRA_INTENT, android.content.IntentSender.class);
+ mTarget = intent.getParcelableExtra(Intent.EXTRA_INTENT,
+ android.content.IntentSender.class);
if (mUserId == UserHandle.USER_NULL) {
Log.wtf(TAG, "Invalid user id: " + mUserId + ". Stopping.");
@@ -70,29 +75,40 @@
return;
}
- String dialogTitle;
- String dialogMessage = null;
- if (mReason == UNLAUNCHABLE_REASON_QUIET_MODE) {
- dialogTitle = getDialogTitle();
- dialogMessage = getDialogMessage();
- } else {
+ if (mReason != UNLAUNCHABLE_REASON_QUIET_MODE) {
Log.wtf(TAG, "Invalid unlaunchable type: " + mReason);
finish();
return;
}
- AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(dialogTitle)
- .setMessage(dialogMessage)
- .setOnDismissListener(this);
- if (mReason == UNLAUNCHABLE_REASON_QUIET_MODE) {
- builder.setPositiveButton(R.string.work_mode_turn_on, this)
- .setNegativeButton(R.string.cancel, null);
+ String targetPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+ boolean showEmergencyCallButton =
+ (targetPackageName != null && targetPackageName.equals(
+ mTelecomManager.getDefaultDialerPackage(UserHandle.of(mUserId))));
+
+ final AlertDialog.Builder builder;
+ final String dialogMessage;
+ if (showEmergencyCallButton) {
+ builder = new AlertDialog.Builder(this, R.style.AlertDialogWithEmergencyButton);
+ dialogMessage = getDialogMessage(R.string.work_mode_dialer_off_message);
+ builder.setNeutralButton(R.string.work_mode_emergency_call_button, this);
} else {
- builder.setPositiveButton(R.string.ok, null);
+ builder = new AlertDialog.Builder(this);
+ dialogMessage = getDialogMessage(R.string.work_mode_off_message);
}
+ builder.setTitle(getDialogTitle())
+ .setMessage(dialogMessage)
+ .setOnDismissListener(this)
+ .setPositiveButton(R.string.work_mode_turn_on, this)
+ .setNegativeButton(R.string.cancel, null);
+
final AlertDialog dialog = builder.create();
dialog.create();
+ if (showEmergencyCallButton) {
+ dialog.getWindow().findViewById(R.id.parentPanel).setPadding(0, 0, 0, 30);
+ dialog.getWindow().findViewById(R.id.button3).setOutlineProvider(null);
+ }
+
// Prevents screen overlay attack.
getWindow().setHideOverlayWindows(true);
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
@@ -104,10 +120,10 @@
UNLAUNCHABLE_APP_WORK_PAUSED_TITLE, () -> getString(R.string.work_mode_off_title));
}
- private String getDialogMessage() {
+ private String getDialogMessage(int dialogMessageString) {
return getSystemService(DevicePolicyManager.class).getResources().getString(
UNLAUNCHABLE_APP_WORK_PAUSED_MESSAGE,
- () -> getString(R.string.work_mode_off_message));
+ () -> getString(dialogMessageString));
}
@Override
@@ -117,14 +133,27 @@
@Override
public void onClick(DialogInterface dialog, int which) {
- if (mReason == UNLAUNCHABLE_REASON_QUIET_MODE && which == DialogInterface.BUTTON_POSITIVE) {
+ if (mReason != UNLAUNCHABLE_REASON_QUIET_MODE) {
+ return;
+ }
+ if (which == DialogInterface.BUTTON_POSITIVE) {
UserManager userManager = UserManager.get(this);
new Handler(Looper.getMainLooper()).post(
() -> userManager.requestQuietModeEnabled(
/* enableQuietMode= */ false, UserHandle.of(mUserId), mTarget));
+ } else if (which == DialogInterface.BUTTON_NEUTRAL) {
+ launchEmergencyDialer();
}
}
+ private void launchEmergencyDialer() {
+ startActivity(mTelecomManager.createLaunchEmergencyDialerIntent(
+ null /* number*/)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP));
+ }
+
private static final Intent createBaseIntent() {
Intent intent = new Intent();
intent.setComponent(new ComponentName("android", UnlaunchableAppActivity.class.getName()));
@@ -139,9 +168,13 @@
return intent;
}
- public static Intent createInQuietModeDialogIntent(int userId, IntentSender target) {
+ public static Intent createInQuietModeDialogIntent(int userId, IntentSender target,
+ ResolveInfo resolveInfo) {
Intent intent = createInQuietModeDialogIntent(userId);
intent.putExtra(Intent.EXTRA_INTENT, target);
+ if (resolveInfo != null) {
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, resolveInfo.getComponentInfo().packageName);
+ }
return intent;
}
}
diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java
index 25ac1bd..600ae50 100644
--- a/core/java/com/android/internal/policy/TransitionAnimation.java
+++ b/core/java/com/android/internal/policy/TransitionAnimation.java
@@ -1279,10 +1279,15 @@
public static float getBorderLuma(SurfaceControl surfaceControl, int w, int h) {
final ScreenCapture.ScreenshotHardwareBuffer buffer =
ScreenCapture.captureLayers(surfaceControl, new Rect(0, 0, w, h), 1);
- if (buffer != null) {
- return getBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace());
+ if (buffer == null) {
+ return 0;
}
- return 0;
+ final HardwareBuffer hwBuffer = buffer.getHardwareBuffer();
+ final float luma = getBorderLuma(hwBuffer, buffer.getColorSpace());
+ if (hwBuffer != null) {
+ hwBuffer.close();
+ }
+ return luma;
}
/** Returns the luminance in 0~1. */
diff --git a/core/java/com/android/internal/power/EnergyConsumerStats.java b/core/java/com/android/internal/power/EnergyConsumerStats.java
index 325df57..8cf17cda 100644
--- a/core/java/com/android/internal/power/EnergyConsumerStats.java
+++ b/core/java/com/android/internal/power/EnergyConsumerStats.java
@@ -58,7 +58,8 @@
public static final int POWER_BUCKET_BLUETOOTH = 5;
public static final int POWER_BUCKET_GNSS = 6;
public static final int POWER_BUCKET_MOBILE_RADIO = 7;
- public static final int NUMBER_STANDARD_POWER_BUCKETS = 8; // Buckets above this are custom.
+ public static final int POWER_BUCKET_CAMERA = 8;
+ public static final int NUMBER_STANDARD_POWER_BUCKETS = 9; // Buckets above this are custom.
@IntDef(prefix = {"POWER_BUCKET_"}, value = {
POWER_BUCKET_UNKNOWN,
@@ -70,6 +71,7 @@
POWER_BUCKET_BLUETOOTH,
POWER_BUCKET_GNSS,
POWER_BUCKET_MOBILE_RADIO,
+ POWER_BUCKET_CAMERA,
})
@Retention(RetentionPolicy.SOURCE)
public @interface StandardPowerBucket {
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index db288c0..8fb345b 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -53,7 +53,7 @@
boolean showImeSwitcher);
void setWindowState(int display, int window, int state);
- void showRecentApps(boolean triggeredFromAltTab);
+ void showRecentApps(boolean triggeredFromAltTab, boolean forward);
void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey);
void toggleRecentApps();
void toggleSplitScreen();
diff --git a/core/java/com/android/internal/view/inline/InlineTooltipUi.java b/core/java/com/android/internal/view/inline/InlineTooltipUi.java
index 836786d..7e12574 100644
--- a/core/java/com/android/internal/view/inline/InlineTooltipUi.java
+++ b/core/java/com/android/internal/view/inline/InlineTooltipUi.java
@@ -15,7 +15,7 @@
*/
package com.android.internal.view.inline;
-import static android.view.autofill.AutofillManager.DEVICE_CONFIG_AUTOFILL_TOOLTIP_SHOW_UP_DELAY;
+import static android.view.autofill.AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_TOOLTIP_SHOW_UP_DELAY;
import static android.view.autofill.Helper.sVerbose;
import android.annotation.NonNull;
diff --git a/core/java/com/android/server/backup/AccountManagerBackupHelper.java b/core/java/com/android/server/backup/AccountManagerBackupHelper.java
index f76dd09..5677699 100644
--- a/core/java/com/android/server/backup/AccountManagerBackupHelper.java
+++ b/core/java/com/android/server/backup/AccountManagerBackupHelper.java
@@ -18,8 +18,8 @@
import android.accounts.AccountManagerInternal;
import android.app.backup.BlobBackupHelper;
-import android.os.UserHandle;
import android.util.Slog;
+
import com.android.server.LocalServices;
/**
@@ -35,8 +35,11 @@
// key under which the account access grant state blob is committed to backup
private static final String KEY_ACCOUNT_ACCESS_GRANTS = "account_access_grants";
- public AccountManagerBackupHelper() {
+ private final int mUserId;
+
+ public AccountManagerBackupHelper(int userId) {
super(STATE_VERSION, KEY_ACCOUNT_ACCESS_GRANTS);
+ mUserId = userId;
}
@Override
@@ -48,7 +51,7 @@
try {
switch (key) {
case KEY_ACCOUNT_ACCESS_GRANTS: {
- return am.backupAccountAccessPermissions(UserHandle.USER_SYSTEM);
+ return am.backupAccountAccessPermissions(mUserId);
}
default: {
@@ -71,7 +74,7 @@
try {
switch (key) {
case KEY_ACCOUNT_ACCESS_GRANTS: {
- am.restoreAccountAccessPermissions(payload, UserHandle.USER_SYSTEM);
+ am.restoreAccountAccessPermissions(payload, mUserId);
} break;
default: {
diff --git a/core/proto/android/nfc/nfc_service.proto b/core/proto/android/nfc/nfc_service.proto
index 2df1d5d..1dcd5cc 100644
--- a/core/proto/android/nfc/nfc_service.proto
+++ b/core/proto/android/nfc/nfc_service.proto
@@ -60,7 +60,7 @@
optional bool secure_nfc_capable = 13;
optional bool vr_mode_enabled = 14;
optional DiscoveryParamsProto discovery_params = 15;
- optional P2pLinkManagerProto p2p_link_manager = 16;
+ reserved 16;
optional com.android.nfc.cardemulation.CardEmulationManagerProto card_emulation_manager = 17;
optional NfcDispatcherProto nfc_dispatcher = 18;
optional string native_crash_logs = 19 [(.android.privacy).dest = DEST_EXPLICIT];
@@ -77,38 +77,6 @@
optional bool enable_p2p = 5;
}
-// Debugging information for com.android.nfc.P2pLinkManager
-message P2pLinkManagerProto {
- option (.android.msg_privacy).dest = DEST_AUTOMATIC;
-
- enum LinkState {
- LINK_STATE_UNKNOWN = 0;
- LINK_STATE_DOWN = 1;
- LINK_STATE_DEBOUNCE = 2;
- LINK_STATE_UP = 3;
- }
-
- enum SendState {
- SEND_STATE_UNKNOWN = 0;
- SEND_STATE_NOTHING_TO_SEND = 1;
- SEND_STATE_NEED_CONFIRMATION = 2;
- SEND_STATE_SENDING = 3;
- SEND_STATE_COMPLETE = 4;
- SEND_STATE_CANCELED = 5;
- }
-
- optional int32 default_miu = 1;
- optional int32 default_rw_size = 2;
- optional LinkState link_state = 3;
- optional SendState send_state = 4;
- optional int32 send_flags = 5;
- optional bool send_enabled = 6;
- optional bool receive_enabled = 7;
- optional string callback_ndef = 8 [(.android.privacy).dest = DEST_EXPLICIT];
- optional .android.nfc.NdefMessageProto message_to_send = 9;
- repeated string uris_to_send = 10 [(.android.privacy).dest = DEST_EXPLICIT];
-}
-
// Debugging information for com.android.nfc.NfcDispatcher
message NfcDispatcherProto {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
diff --git a/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto b/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto
index 35aae8f..5a18d9e 100644
--- a/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto
+++ b/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto
@@ -31,7 +31,7 @@
optional string cur_focused_window_soft_input_mode = 6;
optional .android.view.inputmethod.EditorInfoProto cur_attribute = 7;
optional string cur_id = 8;
- optional bool show_requested = 9;
+ reserved 9; // deprecated show_requested
optional bool show_explicitly_requested = 10;
optional bool show_forced = 11;
optional bool input_shown = 12;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 7d5be48..8ee88ad 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7003,14 +7003,12 @@
<permission android:name="android.permission.MANAGE_WEARABLE_SENSING_SERVICE"
android:protectionLevel="signature|privileged" />
- <!-- Allows applications to use the long running jobs APIs.
- <p>This is a special access permission that can be revoked by the system or the user.
- <p>Apps need to target API {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or above
- to be able to request this permission.
- <p>Protection level: appop
+ <!-- Allows applications to use the long running jobs APIs. For more details
+ see {@link android.app.job.JobInfo.Builder#setUserInitiated}.
+ <p>Protection level: normal
-->
<permission android:name="android.permission.RUN_LONG_JOBS"
- android:protectionLevel="normal|appop"/>
+ android:protectionLevel="normal"/>
<!-- Allows an app access to the installer provided app metadata.
@SystemApi
@@ -7587,7 +7585,7 @@
</intent-filter>
</service>
- <service android:name="com.android.server.art.BackgroundDexOptJobService"
+ <service android:name="com.android.server.art.BackgroundDexoptJobService"
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
diff --git a/core/res/res/drawable/work_mode_emergency_button_background.xml b/core/res/res/drawable/work_mode_emergency_button_background.xml
new file mode 100644
index 0000000..d9b6879
--- /dev/null
+++ b/core/res/res/drawable/work_mode_emergency_button_background.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 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.
+ -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:insetTop="6dp"
+ android:insetBottom="6dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="18dp"/>
+ <solid android:color="@android:color/system_accent3_100" />
+ </shape>
+</inset>
\ No newline at end of file
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 1d1c02d..1b6f88f 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -2653,6 +2653,10 @@
<flag name="noExcludeDescendants" value="0x8" />
</attr>
+ <!-- Boolean that hints the Android System that the view is credntial and associated with
+ CredentialManager -->
+ <attr name="isCredential" format="boolean" />
+
<!-- Hints the Android System whether the this View should be considered a scroll capture target. -->
<attr name="scrollCaptureHint">
<!-- Let the Android System determine if the view can be a scroll capture target. -->
@@ -5134,6 +5138,15 @@
<attr name="textLocale" format="string" />
<!-- Color of the text selection highlight. -->
<attr name="textColorHighlight" />
+ <!-- Color of search results highlight.
+ This color is typically used when TextView/EditText shows search result in-app text
+ search invoked with Ctrl+F. -->
+ <attr name="searchResultHighlightColor" format="color" />
+ <!-- Color of focused search result highlight.
+ This color is typically used when TextView/EditText shows search result in-app text
+ search invoked with Ctrl+F. -->
+ <attr name="focusedSearchResultHighlightColor" format="color" />
+
<!-- Color of the hint text. -->
<attr name="textColorHint" />
<!-- Color of the links. -->
@@ -5211,6 +5224,14 @@
<attr name="textColor" />
<!-- Color of the text selection highlight. -->
<attr name="textColorHighlight" />
+ <!-- Color of search results highlight.
+ This color is typically used when TextView/EditText shows search result in-app text
+ search invoked with Ctrl+F. -->
+ <attr name="searchResultHighlightColor" format="color" />
+ <!-- Color of focused search result highlight.
+ This color is typically used when TextView/EditText shows search result in-app text
+ search invoked with Ctrl+F. -->
+ <attr name="focusedSearchResultHighlightColor" format="color" />
<!-- Color of the hint text. -->
<attr name="textColorHint" />
<!-- Base text color, typeface, size, and style. -->
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index a9c56f0..f4b49e6 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -125,6 +125,9 @@
<public name="keyboardLocale" />
<public name="keyboardLayoutType" />
<public name="allowUpdateOwnership" />
+ <public name="isCredential"/>
+ <public name="searchResultHighlightColor" />
+ <public name="focusedSearchResultHighlightColor" />
</staging-public-group>
<staging-public-group type="id" first-id="0x01cd0000">
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index b754440..7c6f81d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5329,6 +5329,10 @@
<string name="work_mode_off_message">Get access to your work apps and notifications</string>
<!-- Title for button to turn on work profile. [CHAR LIMIT=NONE] -->
<string name="work_mode_turn_on">Turn on</string>
+ <!-- Title for button to launch the personal safety app to make an emergency call -->
+ <string name="work_mode_emergency_call_button">Emergency</string>
+ <!-- Text shown in a dialog when the user tries to launch a disabled work profile app when work apps are paused-->
+ <string name="work_mode_dialer_off_message">Get access to your work apps and calls</string>
<!-- Title of the dialog that is shown when the user tries to launch a blocked application [CHAR LIMIT=50] -->
<string name="app_blocked_title">App is not available</string>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index 476c18e..0a7ffca 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -30,7 +30,7 @@
-->
<resources>
<!-- Global Theme Styles -->
- <eat-comment />
+ <eat-comment/>
<style name="WindowTitleBackground">
<item name="background">@drawable/title_bar</item>
@@ -69,6 +69,19 @@
<item name="needsDefaultBackgrounds">false</item>
</style>
+ <!-- Base style for the alert dialog with emergency call button -->
+ <style name="AlertDialogWithEmergencyButton" parent="AlertDialog">
+ <item name="buttonBarNeutralButtonStyle">@style/AlertDialogEmergencyButtonStyle</item>
+ </style>
+
+ <style name="AlertDialogEmergencyButtonStyle" parent="AlertDialogWithEmergencyButton">
+ <item name="background">@drawable/work_mode_emergency_button_background</item>
+ <item name="textColor">@color/text_color_on_accent_device_default</item>
+ <item name="paddingLeft">15dip</item>
+ <item name="paddingRight">15dip</item>
+ <item name="layout_marginStart">10dip</item>
+ </style>
+
<style name="Widget.PreferenceFrameLayout">
<item name="borderTop">0dip</item>
<item name="borderBottom">0dip</item>
@@ -77,7 +90,7 @@
</style>
<!-- Base style for animations. This style specifies no animations. -->
- <style name="Animation" />
+ <style name="Animation"/>
<!-- Standard animations for a full-screen window or activity. -->
<style name="Animation.Activity">
@@ -231,7 +244,7 @@
</style>
<!-- A special animation value used internally for popup windows. -->
- <style name="Animation.PopupWindow" />
+ <style name="Animation.PopupWindow"/>
<!-- Window animations used for action mode UI in overlay mode. -->
<style name="Animation.PopupWindow.ActionMode">
@@ -503,7 +516,8 @@
<item name="textEditSidePasteWindowLayout">?attr/textEditSidePasteWindowLayout</item>
<item name="textEditSideNoPasteWindowLayout">?attr/textEditSideNoPasteWindowLayout</item>
<item name="textEditSuggestionItemLayout">?attr/textEditSuggestionItemLayout</item>
- <item name="textEditSuggestionContainerLayout">?attr/textEditSuggestionContainerLayout</item>
+ <item name="textEditSuggestionContainerLayout">?attr/textEditSuggestionContainerLayout
+ </item>
<item name="textEditSuggestionHighlightStyle">?attr/textEditSuggestionHighlightStyle</item>
<item name="textCursorDrawable">?attr/textCursorDrawable</item>
<item name="breakStrategy">high_quality</item>
@@ -593,7 +607,8 @@
<item name="weekNumberColor">#33FFFFFF</item>
<item name="weekSeparatorLineColor">#19FFFFFF</item>
<item name="selectedDateVerticalBar">@drawable/day_picker_week_view_dayline_holo</item>
- <item name="weekDayTextAppearance">@style/TextAppearance.Small.CalendarViewWeekDayView</item>
+ <item name="weekDayTextAppearance">@style/TextAppearance.Small.CalendarViewWeekDayView
+ </item>
<item name="dateTextAppearance">?attr/textAppearanceSmall</item>
<item name="calendarViewMode">holo</item>
</style>
@@ -689,12 +704,12 @@
</style>
<style name="Widget.ListView.DropDown">
- <item name="cacheColorHint">@null</item>
+ <item name="cacheColorHint">@null</item>
<item name="divider">@drawable/divider_horizontal_bright_opaque</item>
</style>
<style name="Widget.ListView.Menu" parent="Widget.Holo.ListView">
- <item name="cacheColorHint">@null</item>
+ <item name="cacheColorHint">@null</item>
<item name="scrollbars">vertical</item>
<item name="fadingEdge">none</item>
<!-- Light background for the list in menus, so the divider for bright themes -->
@@ -819,7 +834,7 @@
</style>
<!-- Text Appearances -->
- <eat-comment />
+ <eat-comment/>
<style name="TextAppearance">
<item name="textColor">?textColorPrimary</item>
@@ -878,9 +893,9 @@
<item name="textColorLink">?textColorLinkInverse</item>
</style>
- <style name="TextAppearance.Theme.Dialog" parent="TextAppearance.Theme" />
+ <style name="TextAppearance.Theme.Dialog" parent="TextAppearance.Theme"/>
- <style name="TextAppearance.Widget" />
+ <style name="TextAppearance.Widget"/>
<style name="TextAppearance.Widget.Button" parent="TextAppearance.Small.Inverse">
<item name="textColor">@color/primary_text_light_nodisable</item>
@@ -946,22 +961,22 @@
</style>
<!-- @hide -->
- <style name="TextAppearance.SearchResult">
- <item name="textStyle">normal</item>
- <item name="textColor">?textColorPrimaryInverse</item>
- <item name="textColorHint">?textColorHintInverse</item>
- </style>
+ <style name="TextAppearance.SearchResult">
+ <item name="textStyle">normal</item>
+ <item name="textColor">?textColorPrimaryInverse</item>
+ <item name="textColorHint">?textColorHintInverse</item>
+ </style>
- <!-- @hide -->
- <style name="TextAppearance.SearchResult.Title">
- <item name="textSize">18sp</item>
- </style>
+ <!-- @hide -->
+ <style name="TextAppearance.SearchResult.Title">
+ <item name="textSize">18sp</item>
+ </style>
- <!-- @hide -->
- <style name="TextAppearance.SearchResult.Subtitle">
- <item name="textSize">14sp</item>
- <item name="textColor">?textColorSecondaryInverse</item>
- </style>
+ <!-- @hide -->
+ <style name="TextAppearance.SearchResult.Subtitle">
+ <item name="textSize">14sp</item>
+ <item name="textColor">?textColorSecondaryInverse</item>
+ </style>
<style name="TextAppearance.WindowTitle">
<item name="textColor">#fff</item>
@@ -1165,7 +1180,7 @@
</style>
<!-- Other Misc Styles -->
- <eat-comment />
+ <eat-comment/>
<style name="MediaButton">
<item name="background">@null</item>
@@ -1298,10 +1313,12 @@
<item name="textColor">?attr/textColorSecondary</item>
</style>
- <style name="TextAppearance.Widget.Toolbar.Title" parent="TextAppearance.Widget.ActionBar.Title">
+ <style name="TextAppearance.Widget.Toolbar.Title"
+ parent="TextAppearance.Widget.ActionBar.Title">
</style>
- <style name="TextAppearance.Widget.Toolbar.Subtitle" parent="TextAppearance.Widget.ActionBar.Subtitle">
+ <style name="TextAppearance.Widget.Toolbar.Subtitle"
+ parent="TextAppearance.Widget.ActionBar.Subtitle">
</style>
<style name="Widget.ActionButton">
@@ -1527,8 +1544,8 @@
<!-- The style for normal action button on notification -->
<style name="NotificationAction" parent="Widget.Material.Light.Button.Borderless.Small">
- <item name="textColor">@color/notification_action_button_text_color</item>
- <item name="background">@drawable/notification_material_action_background</item>
+ <item name="textColor">@color/notification_action_button_text_color</item>
+ <item name="background">@drawable/notification_material_action_background</item>
</style>
<!-- The style for emphasized action button on notification: Colored bordered ink button -->
@@ -1539,6 +1556,6 @@
<!-- The style for disabled action button on notification -->
<style name="NotificationTombstoneAction" parent="NotificationAction">
- <item name="textColor">#555555</item>
+ <item name="textColor">#555555</item>
</style>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 4131887..2abb0d8 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3100,6 +3100,10 @@
<java-symbol type="string" name="language_selection_title" />
<java-symbol type="string" name="search_language_hint" />
+ <!-- Work profile unlaunchable app alert dialog-->
+ <java-symbol type="style" name="AlertDialogWithEmergencyButton"/>
+ <java-symbol type="string" name="work_mode_dialer_off_message" />
+ <java-symbol type="string" name="work_mode_emergency_call_button" />
<java-symbol type="string" name="work_mode_off_title" />
<java-symbol type="string" name="work_mode_off_message" />
<java-symbol type="string" name="work_mode_turn_on" />
diff --git a/core/tests/BroadcastRadioTests/AndroidManifest.xml b/core/tests/BroadcastRadioTests/AndroidManifest.xml
index 869b484..8f655ef 100644
--- a/core/tests/BroadcastRadioTests/AndroidManifest.xml
+++ b/core/tests/BroadcastRadioTests/AndroidManifest.xml
@@ -15,7 +15,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.hardware.radio.tests">
+ package="com.android.frameworks.broadcastradiotests">
<uses-permission android:name="android.permission.ACCESS_BROADCAST_RADIO" />
@@ -25,7 +25,7 @@
<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="android.hardware.radio.tests"
+ android:targetPackage="com.android.frameworks.broadcastradiotests"
android:label="Tests for Broadcast Radio APIs" >
</instrumentation>
</manifest>
diff --git a/core/tests/BroadcastRadioTests/AndroidTest.xml b/core/tests/BroadcastRadioTests/AndroidTest.xml
index ed88537..b7e93cd 100644
--- a/core/tests/BroadcastRadioTests/AndroidTest.xml
+++ b/core/tests/BroadcastRadioTests/AndroidTest.xml
@@ -25,7 +25,7 @@
<option name="test-tag" value="BroadcastRadioTests" />
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
- <option name="package" value="android.hardware.radio.tests" />
+ <option name="package" value="com.android.frameworks.broadcastradiotests" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
<option name="hidden-api-checks" value="false"/>
</test>
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
similarity index 90%
rename from core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java
rename to core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
index 65e55a2..63de759 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
@@ -14,17 +14,13 @@
* limitations under the License.
*/
-package android.hardware.radio.tests.unittests;
+package android.hardware.radio;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
import android.graphics.Bitmap;
-import android.hardware.radio.ProgramList;
-import android.hardware.radio.ProgramSelector;
-import android.hardware.radio.RadioManager;
-import android.hardware.radio.RadioTuner;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -167,9 +163,7 @@
@Test
public void setConfigFlag_forRadioTuner_throwsException() {
UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
- () -> {
- DEFAULT_RADIO_TUNER.setConfigFlag(/* flag= */ 1, /* value= */ false);
- });
+ () -> DEFAULT_RADIO_TUNER.setConfigFlag(/* flag= */ 1, /* value= */ false));
assertWithMessage("Exception for setting config flag on default radio tuner")
.that(thrown).hasMessageThat().contains("Setting config flag is not supported");
@@ -178,9 +172,7 @@
@Test
public void setParameters_forRadioTuner_throwsException() {
UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
- () -> {
- DEFAULT_RADIO_TUNER.setParameters(Map.of("testKey", "testValue"));
- });
+ () -> DEFAULT_RADIO_TUNER.setParameters(Map.of("testKey", "testValue")));
assertWithMessage("Exception for setting parameters from default radio tuner")
.that(thrown).hasMessageThat().contains("Setting parameters is not supported");
@@ -189,9 +181,7 @@
@Test
public void getParameters_forRadioTuner_throwsException() {
UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
- () -> {
- DEFAULT_RADIO_TUNER.getParameters(List.of("testKey"));
- });
+ () -> DEFAULT_RADIO_TUNER.getParameters(List.of("testKey")));
assertWithMessage("Exception for getting parameters from default radio tuner")
.that(thrown).hasMessageThat().contains("Getting parameters is not supported");
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramListTest.java
similarity index 97%
rename from core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java
rename to core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramListTest.java
index 9a999e4..f807bad 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramListTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.hardware.radio.tests.unittests;
+package android.hardware.radio;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -29,14 +29,6 @@
import static org.mockito.Mockito.verify;
import android.content.Context;
-import android.hardware.radio.IRadioService;
-import android.hardware.radio.ITuner;
-import android.hardware.radio.ITunerCallback;
-import android.hardware.radio.ProgramList;
-import android.hardware.radio.ProgramSelector;
-import android.hardware.radio.RadioManager;
-import android.hardware.radio.RadioMetadata;
-import android.hardware.radio.RadioTuner;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.ArraySet;
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramSelectorTest.java
similarity index 99%
rename from core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
rename to core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramSelectorTest.java
index 9399907..ae43a1c 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramSelectorTest.java
@@ -14,15 +14,13 @@
* limitations under the License.
*/
-package android.hardware.radio.tests.unittests;
+package android.hardware.radio;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
import android.annotation.Nullable;
-import android.hardware.radio.ProgramSelector;
-import android.hardware.radio.RadioManager;
import android.os.Parcel;
import org.junit.Test;
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioAnnouncementTest.java
similarity index 96%
rename from core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java
rename to core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioAnnouncementTest.java
index 6e1bb4b4..b0fb26a 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioAnnouncementTest.java
@@ -14,14 +14,12 @@
* limitations under the License.
*/
-package android.hardware.radio.tests.unittests;
+package android.hardware.radio;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
-import android.hardware.radio.Announcement;
-import android.hardware.radio.ProgramSelector;
import android.os.Parcel;
import android.util.ArrayMap;
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java
similarity index 98%
rename from core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
rename to core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java
index afbf8c3..79a6b0d 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.hardware.radio.tests.unittests;
+package android.hardware.radio;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -30,14 +30,6 @@
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ApplicationInfo;
-import android.hardware.radio.Announcement;
-import android.hardware.radio.IAnnouncementListener;
-import android.hardware.radio.ICloseHandle;
-import android.hardware.radio.IRadioService;
-import android.hardware.radio.ProgramSelector;
-import android.hardware.radio.RadioManager;
-import android.hardware.radio.RadioMetadata;
-import android.hardware.radio.RadioTuner;
import android.os.Build;
import android.os.Parcel;
import android.os.RemoteException;
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
similarity index 98%
rename from core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java
rename to core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
index 5771135..e348a51 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
@@ -14,14 +14,13 @@
* limitations under the License.
*/
-package android.hardware.radio.tests.unittests;
+package android.hardware.radio;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
import android.graphics.Bitmap;
-import android.hardware.radio.RadioMetadata;
import android.os.Parcel;
import org.junit.Test;
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
similarity index 98%
rename from core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
rename to core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
index c8b4493..487086c 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.hardware.radio.tests.unittests;
+package android.hardware.radio;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -32,13 +32,6 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.graphics.Bitmap;
-import android.hardware.radio.IRadioService;
-import android.hardware.radio.ITuner;
-import android.hardware.radio.ITunerCallback;
-import android.hardware.radio.ProgramSelector;
-import android.hardware.radio.RadioManager;
-import android.hardware.radio.RadioMetadata;
-import android.hardware.radio.RadioTuner;
import android.os.Build;
import android.os.RemoteException;
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
deleted file mode 100644
index cabeb13..0000000
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
+++ /dev/null
@@ -1,428 +0,0 @@
-/*
- * Copyright (C) 2017 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.hardware.radio.tests.functional;
-
-import static org.junit.Assert.*;
-import static org.junit.Assume.*;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.after;
-import static org.mockito.Mockito.atMost;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.testng.Assert.assertThrows;
-
-import android.Manifest;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.hardware.radio.ProgramSelector;
-import android.hardware.radio.RadioManager;
-import android.hardware.radio.RadioTuner;
-import android.util.Log;
-
-import androidx.test.InstrumentationRegistry;
-
-import org.junit.After;
-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 org.mockito.junit.MockitoJUnitRunner;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * A test for broadcast radio API.
- */
-@RunWith(MockitoJUnitRunner.class)
-public class RadioTunerTest {
- private static final String TAG = "BroadcastRadioTests.RadioTuner";
-
- public final Context mContext = InstrumentationRegistry.getContext();
-
- private final int kConfigCallbackTimeoutMs = 10000;
- private final int kCancelTimeoutMs = 1000;
- private final int kTuneCallbackTimeoutMs = 30000;
- private final int kFullScanTimeoutMs = 60000;
-
- private RadioManager mRadioManager;
- private RadioTuner mRadioTuner;
- private RadioManager.ModuleProperties mModule;
- private final List<RadioManager.ModuleProperties> mModules = new ArrayList<>();
- @Mock private RadioTuner.Callback mCallback;
-
- RadioManager.AmBandDescriptor mAmBandDescriptor;
- RadioManager.FmBandDescriptor mFmBandDescriptor;
-
- RadioManager.BandConfig mAmBandConfig;
- RadioManager.BandConfig mFmBandConfig;
-
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
-
- // check if radio is supported and skip the test if it's not
- PackageManager packageManager = mContext.getPackageManager();
- boolean isRadioSupported = packageManager.hasSystemFeature(
- PackageManager.FEATURE_BROADCAST_RADIO);
- assumeTrue(isRadioSupported);
-
- // Check radio access permission
- int res = mContext.checkCallingOrSelfPermission(Manifest.permission.ACCESS_BROADCAST_RADIO);
- assertEquals("ACCESS_BROADCAST_RADIO permission not granted",
- PackageManager.PERMISSION_GRANTED, res);
-
- mRadioManager = (RadioManager)mContext.getSystemService(Context.RADIO_SERVICE);
- assertNotNull(mRadioManager);
-
- int status = mRadioManager.listModules(mModules);
- assertEquals(RadioManager.STATUS_OK, status);
- assertFalse(mModules.isEmpty());
- }
-
- @After
- public void tearDown() {
- mRadioManager = null;
- mModules.clear();
- if (mRadioTuner != null) {
- mRadioTuner.close();
- mRadioTuner = null;
- }
- resetCallback();
- }
-
- private void openTuner() {
- openTuner(true);
- }
-
- private void resetCallback() {
- verify(mCallback, never()).onError(anyInt());
- verify(mCallback, never()).onTuneFailed(anyInt(), any());
- verify(mCallback, never()).onControlChanged(anyBoolean());
- Mockito.reset(mCallback);
- }
-
- private void openTuner(boolean withAudio) {
- assertNull(mRadioTuner);
-
- // find FM band and build its config
- mModule = mModules.get(0);
-
- for (RadioManager.BandDescriptor band : mModule.getBands()) {
- Log.d(TAG, "Band: " + band);
- int bandType = band.getType();
- if (bandType == RadioManager.BAND_AM || bandType == RadioManager.BAND_AM_HD) {
- mAmBandDescriptor = (RadioManager.AmBandDescriptor)band;
- }
- if (bandType == RadioManager.BAND_FM || bandType == RadioManager.BAND_FM_HD) {
- mFmBandDescriptor = (RadioManager.FmBandDescriptor)band;
- }
- }
- assertNotNull(mAmBandDescriptor);
- assertNotNull(mFmBandDescriptor);
- mAmBandConfig = new RadioManager.AmBandConfig.Builder(mAmBandDescriptor).build();
- mFmBandConfig = new RadioManager.FmBandConfig.Builder(mFmBandDescriptor).build();
-
- mRadioTuner = mRadioManager.openTuner(mModule.getId(),
- mFmBandConfig, withAudio, mCallback, null);
- if (!withAudio) {
- // non-audio sessions might not be supported - if so, then skip the test
- assumeNotNull(mRadioTuner);
- }
- assertNotNull(mRadioTuner);
- verify(mCallback, timeout(kConfigCallbackTimeoutMs)).onConfigurationChanged(any());
- resetCallback();
-
- boolean isAntennaConnected = mRadioTuner.isAntennaConnected();
- assertTrue(isAntennaConnected);
- }
-
- @Test
- public void testOpenTuner() {
- openTuner();
- }
-
- @Test
- public void testReopenTuner() throws Throwable {
- openTuner();
- mRadioTuner.close();
- mRadioTuner = null;
- Thread.sleep(100); // TODO(b/36122635): force reopen
- openTuner();
- }
-
- @Test
- public void testDoubleClose() {
- openTuner();
- mRadioTuner.close();
- mRadioTuner.close();
- }
-
- @Test
- public void testUseAfterClose() {
- openTuner();
- mRadioTuner.close();
- int ret = mRadioTuner.cancel();
- assertEquals(RadioManager.STATUS_INVALID_OPERATION, ret);
- }
-
- @Test
- public void testSetAndGetConfiguration() {
- openTuner();
-
- // set
- int ret = mRadioTuner.setConfiguration(mAmBandConfig);
- assertEquals(RadioManager.STATUS_OK, ret);
- verify(mCallback, timeout(kConfigCallbackTimeoutMs)).onConfigurationChanged(any());
-
- // get
- RadioManager.BandConfig[] config = new RadioManager.BandConfig[1];
- ret = mRadioTuner.getConfiguration(config);
- assertEquals(RadioManager.STATUS_OK, ret);
-
- assertEquals(mAmBandConfig, config[0]);
- }
-
- @Test
- public void testSetBadConfiguration() throws Throwable {
- openTuner();
-
- // set null config
- int ret = mRadioTuner.setConfiguration(null);
- assertEquals(RadioManager.STATUS_BAD_VALUE, ret);
- verify(mCallback, never()).onConfigurationChanged(any());
-
- // setting good config should recover
- ret = mRadioTuner.setConfiguration(mAmBandConfig);
- assertEquals(RadioManager.STATUS_OK, ret);
- verify(mCallback, timeout(kConfigCallbackTimeoutMs)).onConfigurationChanged(any());
- }
-
- @Test
- public void testMute() {
- openTuner();
-
- boolean isMuted = mRadioTuner.getMute();
- assertFalse(isMuted);
-
- int ret = mRadioTuner.setMute(true);
- assertEquals(RadioManager.STATUS_OK, ret);
- isMuted = mRadioTuner.getMute();
- assertTrue(isMuted);
-
- ret = mRadioTuner.setMute(false);
- assertEquals(RadioManager.STATUS_OK, ret);
- isMuted = mRadioTuner.getMute();
- assertFalse(isMuted);
- }
-
- @Test
- public void testMuteNoAudio() {
- openTuner(false);
-
- int ret = mRadioTuner.setMute(false);
- assertEquals(RadioManager.STATUS_ERROR, ret);
-
- boolean isMuted = mRadioTuner.getMute();
- assertTrue(isMuted);
- }
-
- @Test
- public void testStep() {
- openTuner();
-
- int ret = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, true);
- assertEquals(RadioManager.STATUS_OK, ret);
- verify(mCallback, timeout(kTuneCallbackTimeoutMs)).onProgramInfoChanged(any());
-
- resetCallback();
-
- ret = mRadioTuner.step(RadioTuner.DIRECTION_UP, false);
- assertEquals(RadioManager.STATUS_OK, ret);
- verify(mCallback, timeout(kTuneCallbackTimeoutMs)).onProgramInfoChanged(any());
- }
-
- @Test
- public void testStepLoop() {
- openTuner();
-
- for (int i = 0; i < 10; i++) {
- Log.d(TAG, "step loop iteration " + (i + 1));
-
- int ret = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, true);
- assertEquals(RadioManager.STATUS_OK, ret);
- verify(mCallback, timeout(kTuneCallbackTimeoutMs)).onProgramInfoChanged(any());
-
- resetCallback();
- }
- }
-
- @Test
- public void testTuneAndGetPI() {
- openTuner();
-
- int channel = mFmBandConfig.getLowerLimit() + mFmBandConfig.getSpacing();
-
- // test tune
- int ret = mRadioTuner.tune(channel, 0);
- assertEquals(RadioManager.STATUS_OK, ret);
- ArgumentCaptor<RadioManager.ProgramInfo> infoc =
- ArgumentCaptor.forClass(RadioManager.ProgramInfo.class);
- verify(mCallback, timeout(kTuneCallbackTimeoutMs))
- .onProgramInfoChanged(infoc.capture());
- assertEquals(channel, infoc.getValue().getChannel());
-
- // test getProgramInformation
- RadioManager.ProgramInfo[] info = new RadioManager.ProgramInfo[1];
- ret = mRadioTuner.getProgramInformation(info);
- assertEquals(RadioManager.STATUS_OK, ret);
- assertNotNull(info[0]);
- assertEquals(channel, info[0].getChannel());
- Log.d(TAG, "PI: " + info[0].toString());
- }
-
- @Test
- public void testDummyCancel() {
- openTuner();
-
- int ret = mRadioTuner.cancel();
- assertEquals(RadioManager.STATUS_OK, ret);
- }
-
- @Test
- public void testLateCancel() {
- openTuner();
-
- int ret = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, false);
- assertEquals(RadioManager.STATUS_OK, ret);
- verify(mCallback, timeout(kTuneCallbackTimeoutMs)).onProgramInfoChanged(any());
-
- int cancelRet = mRadioTuner.cancel();
- assertEquals(RadioManager.STATUS_OK, cancelRet);
- }
-
- @Test
- public void testScanAndCancel() {
- openTuner();
-
- /* There is a possible race condition between scan and cancel commands - the scan may finish
- * before cancel command is issued. Thus we accept both outcomes in this test.
- */
- int scanRet = mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, true);
- int cancelRet = mRadioTuner.cancel();
-
- assertEquals(RadioManager.STATUS_OK, scanRet);
- assertEquals(RadioManager.STATUS_OK, cancelRet);
-
- verify(mCallback, after(kCancelTimeoutMs).atMost(1))
- .onTuneFailed(eq(RadioTuner.TUNER_RESULT_CANCELED), any());
- verify(mCallback, atMost(1)).onProgramInfoChanged(any());
- Mockito.reset(mCallback);
- }
-
- @Test
- public void testStartBackgroundScan() {
- openTuner();
-
- boolean ret = mRadioTuner.startBackgroundScan();
- boolean isSupported = mModule.isBackgroundScanningSupported();
- assertEquals(isSupported, ret);
- }
-
- @Test
- public void testGetProgramList() {
- openTuner();
-
- try {
- Map<String, String> filter = new HashMap<>();
- filter.put("com.google.dummy", "dummy");
- List<RadioManager.ProgramInfo> list = mRadioTuner.getProgramList(filter);
- assertNotNull(list);
- } catch (IllegalStateException e) {
- // the list may or may not be ready at this point
- Log.i(TAG, "Background list is not ready");
- }
- }
-
- @Test
- public void testTuneFromProgramList() {
- openTuner();
-
- List<RadioManager.ProgramInfo> list;
-
- try {
- list = mRadioTuner.getProgramList(null);
- assertNotNull(list);
- } catch (IllegalStateException e) {
- Log.i(TAG, "Background list is not ready, trying to fix it");
-
- boolean success = mRadioTuner.startBackgroundScan();
- assertTrue(success);
- verify(mCallback, timeout(kFullScanTimeoutMs)).onBackgroundScanComplete();
-
- list = mRadioTuner.getProgramList(null);
- assertNotNull(list);
- }
-
- if (list.isEmpty()) {
- Log.i(TAG, "Program list is empty, can't test tune");
- return;
- }
-
- ProgramSelector sel = list.get(0).getSelector();
- mRadioTuner.tune(sel);
- ArgumentCaptor<RadioManager.ProgramInfo> infoc =
- ArgumentCaptor.forClass(RadioManager.ProgramInfo.class);
- verify(mCallback, timeout(kTuneCallbackTimeoutMs)).onProgramInfoChanged(infoc.capture());
- assertEquals(sel, infoc.getValue().getSelector());
- }
-
- @Test
- public void testForcedAnalog() {
- openTuner();
-
- boolean isSupported = true;
- boolean isForced;
- try {
- isForced = mRadioTuner.isAnalogForced();
- assertFalse(isForced);
- } catch (IllegalStateException ex) {
- Log.i(TAG, "Forced analog switch is not supported by this tuner");
- isSupported = false;
- }
-
- if (isSupported) {
- mRadioTuner.setAnalogForced(true);
- isForced = mRadioTuner.isAnalogForced();
- assertTrue(isForced);
-
- mRadioTuner.setAnalogForced(false);
- isForced = mRadioTuner.isAnalogForced();
- assertFalse(isForced);
- } else {
- assertThrows(IllegalStateException.class, () -> mRadioTuner.setAnalogForced(true));
- }
- }
-}
diff --git a/core/tests/coretests/src/android/companion/virtual/camera/VirtualCameraOutputTest.java b/core/tests/coretests/src/android/companion/virtual/camera/VirtualCameraOutputTest.java
index 694b312..f96d138 100644
--- a/core/tests/coretests/src/android/companion/virtual/camera/VirtualCameraOutputTest.java
+++ b/core/tests/coretests/src/android/companion/virtual/camera/VirtualCameraOutputTest.java
@@ -23,13 +23,16 @@
import android.graphics.PixelFormat;
import android.hardware.camera2.params.InputConfiguration;
import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.Presubmit;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
@@ -38,6 +41,8 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+@Presubmit
+@RunWith(AndroidJUnit4.class)
public class VirtualCameraOutputTest {
private static final String TAG = "VirtualCameraOutputTest";
diff --git a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java
index 11afd04..2a1881e 100644
--- a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java
+++ b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java
@@ -26,6 +26,7 @@
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
import androidx.test.runner.AndroidJUnit4;
@@ -42,6 +43,7 @@
import java.time.Duration;
+@Presubmit
@RunWith(AndroidJUnit4.class)
public class VirtualSensorConfigTest {
diff --git a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java
index a9583fd..c260ef9 100644
--- a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java
+++ b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java
@@ -22,12 +22,14 @@
import android.os.Parcel;
import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
+@Presubmit
@RunWith(AndroidJUnit4.class)
public class VirtualSensorEventTest {
diff --git a/core/tests/coretests/src/android/view/autofill/AutofillFeatureFlagsTest.java b/core/tests/coretests/src/android/view/autofill/AutofillFeatureFlagsTest.java
new file mode 100644
index 0000000..e03b722
--- /dev/null
+++ b/core/tests/coretests/src/android/view/autofill/AutofillFeatureFlagsTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.view.autofill;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.provider.DeviceConfig;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link AutofillFeatureFlags}
+ *
+ * run: atest FrameworksCoreTests:android.view.autofill.AutofillFeatureFlagsTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AutofillFeatureFlagsTest {
+
+ @Test
+ public void testGetFillDialogEnabledHintsEmpty() {
+ setFillDialogHints("");
+ String[] fillDialogHints = AutofillFeatureFlags.getFillDialogEnabledHints();
+ assertThat(fillDialogHints).isEmpty();
+ }
+
+ @Test
+ public void testGetFillDialogEnabledHintsTwoValues() {
+ setFillDialogHints("password:creditCardNumber");
+ String[] fillDialogHints = AutofillFeatureFlags.getFillDialogEnabledHints();
+ assertThat(fillDialogHints.length).isEqualTo(2);
+ assertThat(fillDialogHints[0]).isEqualTo("password");
+ assertThat(fillDialogHints[1]).isEqualTo("creditCardNumber");
+ }
+
+ @Test
+ public void testIsCredentialManagerEnabled() {
+ setCredentialManagerEnabled(false);
+ assertThat(AutofillFeatureFlags.isCredentialManagerEnabled()).isFalse();
+ setCredentialManagerEnabled(true);
+ assertThat(AutofillFeatureFlags.isCredentialManagerEnabled()).isTrue();
+ }
+
+ @Test
+ public void testShouldIgnoreCredentialManagerViews() {
+ setCredentialManagerEnabled(false);
+ setIgnoreCredentialManagerViews(true);
+ // Overall feature is disabled, so we shouldn't ignore views.
+ assertThat(AutofillFeatureFlags.shouldIgnoreCredentialViews()).isFalse();
+ setCredentialManagerEnabled(true);
+ assertThat(AutofillFeatureFlags.shouldIgnoreCredentialViews()).isTrue();
+ }
+
+ private static void setFillDialogHints(String value) {
+ setDeviceConfig(
+ AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_DIALOG_HINTS,
+ value);
+ }
+
+ private static void setCredentialManagerEnabled(boolean value) {
+ setDeviceConfig(
+ AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED,
+ String.valueOf(value));
+ }
+
+ private static void setIgnoreCredentialManagerViews(boolean value) {
+ setDeviceConfig(
+ AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_IGNORE_VIEWS,
+ String.valueOf(value));
+ }
+
+ private static void setDeviceConfig(String key, String value) {
+ DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_AUTOFILL, key, value, /* makeDefault */ false);
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 07d0d96..ee8ec1d 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -239,6 +239,11 @@
void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
@NonNull IBinder primary, @Nullable IBinder secondary, @Nullable SplitRule splitRule) {
+ if (secondary == null) {
+ wct.clearAdjacentTaskFragments(primary);
+ return;
+ }
+
WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = null;
final boolean finishSecondaryWithPrimary =
splitRule != null && SplitContainer.shouldFinishSecondaryWithPrimary(splitRule);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
index cd61dbb..f6d67d8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
@@ -47,7 +47,7 @@
private final @NonNull PipBoundsState mPipBoundsState;
private final PipSnapAlgorithm mSnapAlgorithm;
- private final PipKeepClearAlgorithm mPipKeepClearAlgorithm;
+ private final PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
private float mDefaultSizePercent;
private float mMinAspectRatioForMinSize;
@@ -62,7 +62,7 @@
public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState,
@NonNull PipSnapAlgorithm pipSnapAlgorithm,
- @NonNull PipKeepClearAlgorithm pipKeepClearAlgorithm) {
+ @NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm) {
mPipBoundsState = pipBoundsState;
mSnapAlgorithm = pipSnapAlgorithm;
mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java
similarity index 97%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java
index e3495e1..5045cf9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java
@@ -24,7 +24,7 @@
* Interface for interacting with keep clear algorithm used to move PiP window out of the way of
* keep clear areas.
*/
-public interface PipKeepClearAlgorithm {
+public interface PipKeepClearAlgorithmInterface {
/**
* Adjust the position of picture in picture window based on the registered keep clear areas.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
index 690505e..ed8dc7de 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
@@ -26,14 +26,14 @@
import com.android.wm.shell.R;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import java.util.Set;
/**
* Calculates the adjusted position that does not occlude keep clear areas.
*/
-public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithm {
+public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterface {
private boolean mKeepClearAreaGravityEnabled =
SystemProperties.getBoolean(
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 3153313..e83854e 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
@@ -46,8 +46,6 @@
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.SystemProperties;
-import android.os.UserHandle;
-import android.os.UserManager;
import android.util.Pair;
import android.util.Size;
import android.view.DisplayInfo;
@@ -85,7 +83,7 @@
import com.android.wm.shell.pip.PipAppOpsListener;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipSnapAlgorithm;
@@ -137,7 +135,7 @@
private PipAppOpsListener mAppOpsListener;
private PipMediaController mMediaController;
private PipBoundsAlgorithm mPipBoundsAlgorithm;
- private PipKeepClearAlgorithm mPipKeepClearAlgorithm;
+ private PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
private PipBoundsState mPipBoundsState;
private PipMotionHelper mPipMotionHelper;
private PipTouchHandler mTouchHandler;
@@ -380,7 +378,7 @@
PipAnimationController pipAnimationController,
PipAppOpsListener pipAppOpsListener,
PipBoundsAlgorithm pipBoundsAlgorithm,
- PipKeepClearAlgorithm pipKeepClearAlgorithm,
+ PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
PipBoundsState pipBoundsState,
PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
@@ -419,7 +417,7 @@
PipAnimationController pipAnimationController,
PipAppOpsListener pipAppOpsListener,
PipBoundsAlgorithm pipBoundsAlgorithm,
- PipKeepClearAlgorithm pipKeepClearAlgorithm,
+ PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
@NonNull PipBoundsState pipBoundsState,
PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
index 31490e4..b042063 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
@@ -37,7 +37,7 @@
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -62,7 +62,7 @@
@NonNull TvPipBoundsState tvPipBoundsState,
@NonNull PipSnapAlgorithm pipSnapAlgorithm) {
super(context, tvPipBoundsState, pipSnapAlgorithm,
- new PipKeepClearAlgorithm() {
+ new PipKeepClearAlgorithmInterface() {
});
this.mTvPipBoundsState = tvPipBoundsState;
this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index 66d0a2a..665267f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -170,6 +170,7 @@
if (!isCustomRotate()) {
mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer, colorSpace);
}
+ hardwareBuffer.close();
}
t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE);
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
index ea6c14d7..02d50f4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
@@ -17,7 +17,7 @@
package com.android.wm.shell.flicker.pip
import android.app.Activity
-import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.FlickerBuilder
@@ -27,7 +27,6 @@
import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper
import com.android.server.wm.flicker.helpers.WindowUtils
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd
import com.android.server.wm.flicker.testapp.ActivityOptions.Pip.ACTION_ENTER_PIP
import com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION
import com.android.server.wm.traces.common.ComponentNameMatcher
@@ -116,14 +115,6 @@
}
/**
- * Checks that the [ComponentNameMatcher.NAV_BAR] has the correct position at the start and end
- * of the transition
- */
- @FlakyTest
- @Test
- override fun navBarLayerPositionAtStartAndEnd() = flicker.navBarLayerPositionAtStartAndEnd()
-
- /**
* Checks that all parts of the screen are covered at the start and end of the transition
*
* TODO b/197726599 Prevents all states from being checked
@@ -132,12 +123,6 @@
@Test
fun entireScreenCoveredAtStartAndEnd() = flicker.entireScreenCovered(allStates = false)
- @FlakyTest(bugId = 251219769)
- @Test
- override fun entireScreenCovered() {
- super.entireScreenCovered()
- }
-
/** Checks [pipApp] window remains visible and on top throughout the transition */
@Presubmit
@Test
@@ -180,10 +165,25 @@
@Presubmit
@Test
fun pipAppLayerCoversFullScreenOnStart() {
+ Assume.assumeFalse(tapl.isTablet)
flicker.assertLayersStart { visibleRegion(pipApp).coversExactly(startingBounds) }
}
/**
+ * Checks that the visible region of [pipApp] covers the full display area at the start of the
+ * transition
+ */
+ @Postsubmit
+ @Test
+ fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() {
+ Assume.assumeFalse(tapl.isTablet)
+ flicker.assertLayersStart {
+ visibleRegion(pipApp.or(ComponentNameMatcher.LETTERBOX))
+ .coversExactly(startingBounds)
+ }
+ }
+
+ /**
* Checks that the visible region of [testApp] plus the visible region of [pipApp] cover the
* full display area at the end of the transition
*/
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
index b5a5004..3bfcde3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
@@ -22,8 +22,10 @@
import com.android.server.wm.flicker.FlickerBuilder
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
import com.android.server.wm.traces.common.service.PlatformConsts
+import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -74,10 +76,19 @@
}
/** {@inheritDoc} */
- @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+ @FlakyTest(bugId = 197726610)
+ @Test
+ override fun pipLayerExpands() {
+ Assume.assumeFalse(isShellTransitionsEnabled)
+ super.pipLayerExpands()
+ }
- /** {@inheritDoc} */
- @FlakyTest(bugId = 197726610) @Test override fun pipLayerExpands() = super.pipLayerExpands()
+ @Presubmit
+ @Test
+ fun pipLayerExpands_ShellTransit() {
+ Assume.assumeTrue(isShellTransitionsEnabled)
+ super.pipLayerExpands()
+ }
companion object {
/**
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
index f213cc9..c90c2d4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
@@ -16,7 +16,7 @@
package com.android.wm.shell.flicker.pip
-import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.FlickerBuilder
import com.android.server.wm.flicker.FlickerTest
@@ -62,7 +62,7 @@
* Checks that the pip app window remains inside the display bounds throughout the whole
* animation
*/
- @FlakyTest(bugId = 249308003)
+ @Presubmit
@Test
fun pipWindowRemainInsideVisibleBounds() {
flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
@@ -72,28 +72,28 @@
* Checks that the pip app layer remains inside the display bounds throughout the whole
* animation
*/
- @FlakyTest(bugId = 249308003)
+ @Presubmit
@Test
fun pipLayerRemainInsideVisibleBounds() {
flicker.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
}
/** Checks [pipApp] window remains visible throughout the animation */
- @FlakyTest(bugId = 249308003)
+ @Presubmit
@Test
fun pipWindowIsAlwaysVisible() {
flicker.assertWm { isAppWindowVisible(pipApp) }
}
/** Checks [pipApp] layer remains visible throughout the animation */
- @FlakyTest(bugId = 249308003)
+ @Presubmit
@Test
fun pipLayerIsAlwaysVisible() {
flicker.assertLayers { isVisible(pipApp) }
}
/** Checks that the visible region of [pipApp] always expands during the animation */
- @FlakyTest(bugId = 249308003)
+ @Presubmit
@Test
fun pipLayerExpands() {
flicker.assertLayers {
@@ -104,7 +104,7 @@
}
}
- @FlakyTest(bugId = 249308003)
+ @Presubmit
@Test
fun pipSameAspectRatio() {
flicker.assertLayers {
@@ -116,92 +116,26 @@
}
/** Checks [pipApp] window remains pinned throughout the animation */
- @FlakyTest(bugId = 249308003)
+ @Presubmit
@Test
fun windowIsAlwaysPinned() {
flicker.assertWm { this.invoke("hasPipWindow") { it.isPinned(pipApp) } }
}
- /** Checks [ComponentMatcher.LAUNCHER] layer remains visible throughout the animation */
- @FlakyTest(bugId = 249308003)
+ /** Checks [ComponentNameMatcher.LAUNCHER] layer remains visible throughout the animation */
+ @Presubmit
@Test
fun launcherIsAlwaysVisible() {
flicker.assertLayers { isVisible(ComponentNameMatcher.LAUNCHER) }
}
/** Checks that the focus doesn't change between windows during the transition */
- @FlakyTest(bugId = 216306753)
+ @Presubmit
@Test
fun focusDoesNotChange() {
flicker.assertEventLog { this.focusDoesNotChange() }
}
- @FlakyTest(bugId = 216306753)
- @Test
- override fun navBarLayerIsVisibleAtStartAndEnd() {
- super.navBarLayerIsVisibleAtStartAndEnd()
- }
-
- @FlakyTest(bugId = 216306753)
- @Test
- override fun navBarWindowIsAlwaysVisible() {
- super.navBarWindowIsAlwaysVisible()
- }
-
- @FlakyTest(bugId = 216306753)
- @Test
- override fun statusBarLayerIsVisibleAtStartAndEnd() {
- super.statusBarLayerIsVisibleAtStartAndEnd()
- }
-
- @FlakyTest(bugId = 216306753)
- @Test
- override fun statusBarLayerPositionAtStartAndEnd() {
- super.statusBarLayerPositionAtStartAndEnd()
- }
-
- @FlakyTest(bugId = 216306753)
- @Test
- override fun taskBarLayerIsVisibleAtStartAndEnd() {
- super.taskBarLayerIsVisibleAtStartAndEnd()
- }
-
- @FlakyTest(bugId = 216306753)
- @Test
- override fun taskBarWindowIsAlwaysVisible() {
- super.taskBarWindowIsAlwaysVisible()
- }
-
- @FlakyTest(bugId = 216306753)
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
- }
-
- @FlakyTest(bugId = 216306753)
- @Test
- override fun statusBarWindowIsAlwaysVisible() {
- super.statusBarWindowIsAlwaysVisible()
- }
-
- @FlakyTest(bugId = 216306753)
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
- }
-
- @FlakyTest(bugId = 216306753)
- @Test
- override fun entireScreenCovered() {
- super.entireScreenCovered()
- }
-
- @FlakyTest(bugId = 216306753)
- @Test
- override fun navBarLayerPositionAtStartAndEnd() {
- super.navBarLayerPositionAtStartAndEnd()
- }
-
companion object {
/**
* Creates the test configurations.
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
index 34f6659..cb2326c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
@@ -16,7 +16,7 @@
package com.android.wm.shell.flicker.pip
-import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.FlickerBuilder
import com.android.server.wm.flicker.FlickerTest
@@ -39,7 +39,7 @@
get() = buildTransition { transitions { pipApp.pinchOpenPipWindow(wmHelper, 0.4f, 30) } }
/** Checks that the visible region area of [pipApp] always increases during the animation. */
- @Postsubmit
+ @Presubmit
@Test
fun pipLayerAreaIncreases() {
flicker.assertLayers {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
index eee00bd..4557a15 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
@@ -16,7 +16,6 @@
package com.android.wm.shell.flicker.pip
-import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.Presubmit
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.FlickerBuilder
@@ -24,11 +23,8 @@
import com.android.server.wm.flicker.FlickerTestFactory
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.Assume
-import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -66,11 +62,6 @@
private val screenBoundsStart = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
private val screenBoundsEnd = WindowUtils.getDisplayBounds(flicker.scenario.endRotation)
- @Before
- open fun before() {
- Assume.assumeFalse(isShellTransitionsEnabled)
- }
-
override val transition: FlickerBuilder.() -> Unit
get() = buildTransition {
setup {
@@ -80,11 +71,6 @@
transitions { setRotation(flicker.scenario.endRotation) }
}
- /** Checks the position of the navigation bar at the start and end of the transition */
- @FlakyTest(bugId = 240499181)
- @Test
- override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
/** Checks that [testApp] layer is within [screenBoundsStart] at the start of the transition */
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt
deleted file mode 100644
index d0d9167..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerTest
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.Assume
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test Pip Stack in bounds after rotations.
- *
- * To run this test: `atest WMShellFlickerTests:PipRotationTest_ShellTransit`
- *
- * Actions:
- * ```
- * Launch a [pipApp] in pip mode
- * Launch another app [fixedApp] (appears below pip)
- * Rotate the screen from [flicker.scenario.startRotation] to [flicker.scenario.endRotation]
- * (usually, 0->90 and 90->0)
- * ```
- * Notes:
- * ```
- * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
- * are inherited from [PipTransition]
- * 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
- * including configuring navigation mode, initial orientation and ensuring no
- * apps are running before setup
- * ```
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@FlakyTest(bugId = 239575053)
-class PipRotationTest_ShellTransit(flicker: FlickerTest) : PipRotationTest(flicker) {
- @Before
- override fun before() {
- Assume.assumeTrue(isShellTransitionsEnabled)
- }
-
- /** {@inheritDoc} */
- @FlakyTest
- @Test
- override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
index d7107db..871515b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
@@ -25,7 +25,6 @@
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.FlickerTestFactory
import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
import com.android.server.wm.flicker.testapp.ActivityOptions
import com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION
@@ -104,22 +103,6 @@
flicker.assertWmEnd { hasRotation(PlatformConsts.Rotation.ROTATION_90) }
}
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun statusBarLayerIsVisibleAtStartAndEnd() =
- super.statusBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @FlakyTest
- @Test
- override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
@Presubmit
@Test
fun pipWindowInsideDisplay() {
@@ -132,22 +115,10 @@
flicker.assertWmEnd { isAppWindowOnTop(pipApp) }
}
- private fun pipLayerInsideDisplay_internal() {
- flicker.assertLayersStart { visibleRegion(pipApp).coversAtMost(startingBounds) }
- }
-
@Presubmit
@Test
fun pipLayerInsideDisplay() {
- Assume.assumeFalse(isShellTransitionsEnabled)
- pipLayerInsideDisplay_internal()
- }
-
- @FlakyTest(bugId = 250527829)
- @Test
- fun pipLayerInsideDisplay_shellTransit() {
- Assume.assumeTrue(isShellTransitionsEnabled)
- pipLayerInsideDisplay_internal()
+ flicker.assertLayersStart { visibleRegion(pipApp).coversAtMost(startingBounds) }
}
@Presubmit
@@ -173,7 +144,9 @@
override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+ @FlakyTest(bugId = 264243884)
+ @Test
+ override fun entireScreenCovered() = super.entireScreenCovered()
companion object {
@Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index 65cbea0..c08ad69 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -16,7 +16,6 @@
package com.android.wm.shell.flicker.splitscreen
-import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.IwTest
import android.platform.test.annotations.Presubmit
import androidx.test.filters.RequiresDevice
@@ -116,46 +115,6 @@
/** {@inheritDoc} */
@Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @FlakyTest(bugId = 206753786)
- @Test
- override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun statusBarLayerIsVisibleAtStartAndEnd() =
- super.statusBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
@Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
index fcdad96..514365f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
@@ -18,7 +18,6 @@
import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.IwTest
-import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.FlickerBuilder
@@ -149,60 +148,9 @@
)
/** {@inheritDoc} */
- @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
-
- /** {@inheritDoc} */
- @Postsubmit
+ @FlakyTest(bugId = 263213649)
@Test
- override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun statusBarLayerIsVisibleAtStartAndEnd() =
- super.statusBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+ override fun entireScreenCovered() = super.entireScreenCovered()
companion object {
@Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
index af63f7c..d086f7e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
@@ -16,8 +16,8 @@
package com.android.wm.shell.flicker.splitscreen
+import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.IwTest
-import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.FlickerBuilder
@@ -126,60 +126,9 @@
}
/** {@inheritDoc} */
- @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
-
- /** {@inheritDoc} */
- @Postsubmit
+ @FlakyTest(bugId = 241523824)
@Test
- override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun statusBarLayerIsVisibleAtStartAndEnd() =
- super.statusBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+ override fun entireScreenCovered() = super.entireScreenCovered()
companion object {
@Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
index c09ca91..a9cbb74 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
@@ -18,13 +18,11 @@
import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.IwTest
-import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.FlickerBuilder
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.FlickerTestFactory
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
import com.android.wm.shell.flicker.appWindowBecomesVisible
import com.android.wm.shell.flicker.layerBecomesVisible
@@ -33,7 +31,6 @@
import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
import com.android.wm.shell.flicker.splitScreenEntered
-import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -96,18 +93,6 @@
@Presubmit
@Test
fun secondaryAppBoundsBecomesVisible() {
- Assume.assumeFalse(isShellTransitionsEnabled)
- flicker.splitAppLayerBoundsBecomesVisible(
- secondaryApp,
- landscapePosLeft = !tapl.isTablet,
- portraitPosTop = true
- )
- }
-
- @FlakyTest(bugId = 244407465)
- @Test
- fun secondaryAppBoundsBecomesVisible_shellTransit() {
- Assume.assumeTrue(isShellTransitionsEnabled)
flicker.splitAppLayerBoundsBecomesVisible(
secondaryApp,
landscapePosLeft = !tapl.isTablet,
@@ -129,58 +114,11 @@
override fun entireScreenCovered() = super.entireScreenCovered()
/** {@inheritDoc} */
- @Presubmit
- @Test
- override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun statusBarLayerIsVisibleAtStartAndEnd() =
- super.statusBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
@FlakyTest(bugId = 252736515)
@Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
index 09568b2..c7b81d9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
@@ -196,56 +196,9 @@
/** {@inheritDoc} */
@Postsubmit
@Test
- override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun statusBarLayerIsVisibleAtStartAndEnd() =
- super.statusBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Postsubmit
- @Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
- /** {@inheritDoc} */
- @Postsubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
index 262e429..298d0a6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
@@ -64,7 +64,7 @@
initializeMockResources();
mPipBoundsState = new PipBoundsState(mContext);
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
- new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {});
+ new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {});
mPipBoundsState.setDisplayLayout(
new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
index 9088077..17e7d74 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -98,7 +98,7 @@
mPipBoundsState = new PipBoundsState(mContext);
mPipTransitionState = new PipTransitionState();
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
- new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {});
+ new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {});
mMainExecutor = new TestShellExecutor();
mPipTaskOrganizer = new PipTaskOrganizer(mContext,
mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index 3bd2ae7..c1993b2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -37,7 +37,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
@@ -90,8 +90,8 @@
MockitoAnnotations.initMocks(this);
mPipBoundsState = new PipBoundsState(mContext);
final PipSnapAlgorithm pipSnapAlgorithm = new PipSnapAlgorithm();
- final PipKeepClearAlgorithm pipKeepClearAlgorithm =
- new PipKeepClearAlgorithm() {};
+ final PipKeepClearAlgorithmInterface pipKeepClearAlgorithm =
+ new PipKeepClearAlgorithmInterface() {};
final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext,
mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm);
final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
index 474d6aa..8ad2932 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
@@ -34,7 +34,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
@@ -106,7 +106,7 @@
mPipBoundsState = new PipBoundsState(mContext);
mPipSnapAlgorithm = new PipSnapAlgorithm();
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm,
- new PipKeepClearAlgorithm() {});
+ new PipKeepClearAlgorithmInterface() {});
PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState,
mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm,
mMockPipTransitionController, mFloatingContentCoordinator);
diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h
index 533106d..1524dff 100644
--- a/libs/hwui/SkiaCanvas.h
+++ b/libs/hwui/SkiaCanvas.h
@@ -31,7 +31,6 @@
#include "hwui/Canvas.h"
#include "hwui/Paint.h"
#include "pipeline/skia/AnimatedDrawables.h"
-#include "src/core/SkArenaAlloc.h"
enum class SkBlendMode;
class SkRRect;
diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java
index 273c7af..4323c73 100644
--- a/media/java/android/media/MediaPlayer.java
+++ b/media/java/android/media/MediaPlayer.java
@@ -708,12 +708,10 @@
* It's easier to create it here than in C++.
*/
try (ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState()) {
- native_setup(new WeakReference<MediaPlayer>(this), attributionSourceState.getParcel());
+ native_setup(new WeakReference<>(this), attributionSourceState.getParcel(),
+ resolvePlaybackSessionId(context, sessionId));
}
-
- int effectiveSessionId = resolvePlaybackSessionId(context, sessionId);
- baseRegisterPlayer(effectiveSessionId);
- native_setAudioSessionId(effectiveSessionId);
+ baseRegisterPlayer(getAudioSessionId());
}
private Parcel createPlayerIIdParcel() {
@@ -1022,8 +1020,6 @@
final AudioAttributes aa = audioAttributes != null ? audioAttributes :
new AudioAttributes.Builder().build();
mp.setAudioAttributes(aa);
- mp.native_setAudioSessionId(audioSessionId);
-
mp.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
afd.close();
mp.prepare();
@@ -2521,7 +2517,7 @@
private static native final void native_init();
private native void native_setup(Object mediaplayerThis,
- @NonNull Parcel attributionSource);
+ @NonNull Parcel attributionSource, int audioSessionId);
private native final void native_finalize();
/**
diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java
index 0982132..e1af909 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -23,7 +23,6 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
-import android.util.Log;
import com.android.internal.util.Preconditions;
@@ -117,6 +116,8 @@
mProviderId = src.readString();
mSelectedRoutes = ensureList(src.createStringArrayList());
+ Preconditions.checkArgument(!mSelectedRoutes.isEmpty());
+
mSelectableRoutes = ensureList(src.createStringArrayList());
mDeselectableRoutes = ensureList(src.createStringArrayList());
mTransferableRoutes = ensureList(src.createStringArrayList());
@@ -416,15 +417,21 @@
return result.toString();
}
+ /**
+ * Provides a new list with unique route IDs if {@link #mProviderId} is set, or the original IDs
+ * otherwise.
+ *
+ * @param routeIds list of route IDs to convert
+ * @return new list with unique IDs or original IDs
+ */
+
+ @NonNull
private List<String> convertToUniqueRouteIds(@NonNull List<String> routeIds) {
- if (routeIds == null) {
- Log.w(TAG, "routeIds is null. Returning an empty list");
- return Collections.emptyList();
- }
+ Objects.requireNonNull(routeIds, "RouteIds cannot be null.");
// mProviderId can be null if not set. Return the original list for this case.
if (TextUtils.isEmpty(mProviderId)) {
- return routeIds;
+ return new ArrayList<>(routeIds);
}
List<String> result = new ArrayList<>();
diff --git a/media/java/android/media/tv/TableRequest.java b/media/java/android/media/tv/TableRequest.java
index a1a6b51..a9ea6d3 100644
--- a/media/java/android/media/tv/TableRequest.java
+++ b/media/java/android/media/tv/TableRequest.java
@@ -33,11 +33,54 @@
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({TABLE_NAME_PAT, TABLE_NAME_PMT})
+ @IntDef({TABLE_NAME_PAT, TABLE_NAME_PMT, TABLE_NAME_CAT})
public @interface TableName {}
+ /** Program Association Table */
public static final int TABLE_NAME_PAT = 0;
+ /** Program Mapping Table */
public static final int TABLE_NAME_PMT = 1;
+ /**
+ * Conditional Access Table
+ * @hide
+ */
+ public static final int TABLE_NAME_CAT = 2;
+ /**
+ * Network Information Table
+ * @hide
+ */
+ public static final int TABLE_NAME_NIT = 3;
+ /**
+ * Bouquet Association Table
+ * @hide
+ */
+ public static final int TABLE_NAME_BAT = 4;
+ /**
+ * Service Description Table
+ * @hide
+ */
+ public static final int TABLE_NAME_SDT = 5;
+ /**
+ * Event Information Table
+ * @hide
+ */
+ public static final int TABLE_NAME_EIT = 6;
+ /**
+ * Time and Date Table
+ * @hide
+ */
+ public static final int TABLE_NAME_TDT = 7;
+ /**
+ * Time Offset Table
+ * @hide
+ */
+ public static final int TABLE_NAME_TOT = 8;
+ /**
+ * Selection Information Table
+ * @hide
+ */
+ public static final int TABLE_NAME_SIT = 9;
+
public static final @NonNull Parcelable.Creator<TableRequest> CREATOR =
new Parcelable.Creator<TableRequest>() {
diff --git a/media/java/android/media/tv/TableResponse.java b/media/java/android/media/tv/TableResponse.java
index afc9bee..1c314b0 100644
--- a/media/java/android/media/tv/TableResponse.java
+++ b/media/java/android/media/tv/TableResponse.java
@@ -21,6 +21,7 @@
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.SharedMemory;
/**
* A response for Table from broadcast signal.
@@ -46,6 +47,8 @@
private final Uri mTableUri;
private final int mVersion;
private final int mSize;
+ private final byte[] mTableByteArray;
+ private final SharedMemory mTableSharedMemory;
static TableResponse createFromParcelBody(Parcel in) {
return new TableResponse(in);
@@ -54,9 +57,33 @@
public TableResponse(int requestId, int sequence, @ResponseResult int responseResult,
@Nullable Uri tableUri, int version, int size) {
super(RESPONSE_TYPE, requestId, sequence, responseResult);
- mTableUri = tableUri;
mVersion = version;
mSize = size;
+ mTableUri = tableUri;
+ mTableByteArray = null;
+ mTableSharedMemory = null;
+ }
+
+ /** @hide */
+ public TableResponse(int requestId, int sequence, @ResponseResult int responseResult,
+ @NonNull byte[] tableByteArray, int version, int size) {
+ super(RESPONSE_TYPE, requestId, sequence, responseResult);
+ mVersion = version;
+ mSize = size;
+ mTableUri = null;
+ mTableByteArray = tableByteArray;
+ mTableSharedMemory = null;
+ }
+
+ /** @hide */
+ public TableResponse(int requestId, int sequence, @ResponseResult int responseResult,
+ @NonNull SharedMemory tableSharedMemory, int version, int size) {
+ super(RESPONSE_TYPE, requestId, sequence, responseResult);
+ mVersion = version;
+ mSize = size;
+ mTableUri = null;
+ mTableByteArray = null;
+ mTableSharedMemory = tableSharedMemory;
}
TableResponse(Parcel source) {
@@ -65,6 +92,14 @@
mTableUri = uriString == null ? null : Uri.parse(uriString);
mVersion = source.readInt();
mSize = source.readInt();
+ int arrayLength = source.readInt();
+ if (arrayLength >= 0) {
+ mTableByteArray = new byte[arrayLength];
+ source.readByteArray(mTableByteArray);
+ } else {
+ mTableByteArray = null;
+ }
+ mTableSharedMemory = (SharedMemory) source.readTypedObject(SharedMemory.CREATOR);
}
/**
@@ -76,6 +111,30 @@
}
/**
+ * Gets the data of the table as a byte array.
+ *
+ * @return the table data as a byte array, or {@code null} if the data is not stored as a byte
+ * array.
+ * @hide
+ */
+ @Nullable
+ public byte[] getTableByteArray() {
+ return mTableByteArray;
+ }
+
+ /**
+ * Gets the data of the table as a {@link SharedMemory} object.
+ *
+ * @return the table data as a {@link SharedMemory} object, or {@code null} if the data is not
+ * stored in shared memory.
+ * @hide
+ */
+ @Nullable
+ public SharedMemory getTableSharedMemory() {
+ return mTableSharedMemory;
+ }
+
+ /**
* Gets the version number of requested table. If it is null, value will be -1.
* <p>The consistency of version numbers between request and response depends on
* {@link BroadcastInfoRequest.RequestOption}. If the request has RequestOption value
@@ -106,5 +165,12 @@
dest.writeString(uriString);
dest.writeInt(mVersion);
dest.writeInt(mSize);
+ if (mTableByteArray != null) {
+ dest.writeInt(mTableByteArray.length);
+ dest.writeByteArray(mTableByteArray);
+ } else {
+ dest.writeInt(-1);
+ }
+ dest.writeTypedObject(mTableSharedMemory, flags);
}
}
diff --git a/media/java/android/media/tv/TimelineRequest.java b/media/java/android/media/tv/TimelineRequest.java
index 03c62f0..d04c58a 100644
--- a/media/java/android/media/tv/TimelineRequest.java
+++ b/media/java/android/media/tv/TimelineRequest.java
@@ -42,6 +42,7 @@
};
private final int mIntervalMillis;
+ private final String mSelector;
static TimelineRequest createFromParcelBody(Parcel in) {
return new TimelineRequest(in);
@@ -50,11 +51,21 @@
public TimelineRequest(int requestId, @RequestOption int option, int intervalMillis) {
super(REQUEST_TYPE, requestId, option);
mIntervalMillis = intervalMillis;
+ mSelector = null;
+ }
+
+ /** @hide */
+ public TimelineRequest(int requestId, @RequestOption int option, int intervalMillis,
+ @NonNull String selector) {
+ super(REQUEST_TYPE, requestId, option);
+ mIntervalMillis = intervalMillis;
+ mSelector = selector;
}
TimelineRequest(Parcel source) {
super(REQUEST_TYPE, source);
mIntervalMillis = source.readInt();
+ mSelector = source.readString();
}
/**
@@ -64,6 +75,18 @@
return mIntervalMillis;
}
+ /**
+ * Gets the timeline selector.
+ * <p>The selector describes the type and location of timeline signalling. For example
+ * {@code urn:dvb:css:timeline:pts} is a selector in DVB standard.
+ *
+ * @return the selector if it's set; {@code null} otherwise.
+ * @hide
+ */
+ public String getSelector() {
+ return mSelector;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -73,5 +96,6 @@
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(mIntervalMillis);
+ dest.writeString(mSelector);
}
}
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index 537e711..ad9312f 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -41,7 +41,9 @@
void onTeletextAppStateChanged(int state, int seq);
void onAdBuffer(in AdBuffer buffer, int seq);
void onCommandRequest(in String cmdType, in Bundle parameters, int seq);
+ void onTimeShiftCommandRequest(in String cmdType, in Bundle parameters, int seq);
void onSetVideoBounds(in Rect rect, int seq);
+ void onRequestCurrentVideoBounds(int seq);
void onRequestCurrentChannelUri(int seq);
void onRequestCurrentChannelLcn(int seq);
void onRequestStreamVolume(int seq);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
index 5a0ac84..c0723f7 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -26,6 +26,7 @@
import android.media.tv.interactive.ITvInteractiveAppClient;
import android.media.tv.interactive.ITvInteractiveAppManagerCallback;
import android.media.tv.interactive.TvInteractiveAppServiceInfo;
+import android.media.PlaybackParams;
import android.net.Uri;
import android.os.Bundle;
import android.view.Surface;
@@ -36,6 +37,7 @@
*/
interface ITvInteractiveAppManager {
List<TvInteractiveAppServiceInfo> getTvInteractiveAppServiceList(int userId);
+ List<AppLinkInfo> getAppLinkInfoList(int userId);
void registerAppLinkInfo(String tiasId, in AppLinkInfo info, int userId);
void unregisterAppLinkInfo(String tiasId, in AppLinkInfo info, int userId);
void sendAppLinkCommand(String tiasId, in Bundle command, int userId);
@@ -46,6 +48,7 @@
in IBinder sessionToken, in Uri biIAppUri, in Bundle params, int userId);
void destroyBiInteractiveApp(in IBinder sessionToken, in String biIAppId, int userId);
void setTeletextAppEnabled(in IBinder sessionToken, boolean enable, int userId);
+ void sendCurrentVideoBounds(in IBinder sessionToken, in Rect bounds, int userId);
void sendCurrentChannelUri(in IBinder sessionToken, in Uri channelUri, int userId);
void sendCurrentChannelLcn(in IBinder sessionToken, int lcn, int userId);
void sendStreamVolume(in IBinder sessionToken, float volume, int userId);
@@ -57,6 +60,14 @@
void sendTvRecordingInfoList(in IBinder sessionToken,
in List<TvRecordingInfo> recordingInfoList, int userId);
void notifyError(in IBinder sessionToken, in String errMsg, in Bundle params, int userId);
+ void notifyTimeShiftPlaybackParams(
+ in IBinder sessionToken, in PlaybackParams params, int userId);
+ void notifyTimeShiftStatusChanged(
+ in IBinder sessionToken, in String inputId, int status, int userId);
+ void notifyTimeShiftStartPositionChanged(
+ in IBinder sessionToken, in String inputId, long timeMs, int userId);
+ void notifyTimeShiftCurrentPositionChanged(
+ in IBinder sessionToken, in String inputId, long timeMs, int userId);
void createSession(in ITvInteractiveAppClient client, in String iAppServiceId, int type,
int seq, int userId);
void releaseSession(in IBinder sessionToken, int userId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
index 20ba57b..9ae9ca7 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -20,6 +20,7 @@
import android.media.tv.BroadcastInfoResponse;
import android.net.Uri;
import android.media.tv.AdBuffer;
+import android.media.PlaybackParams;
import android.media.tv.AdResponse;
import android.media.tv.BroadcastInfoResponse;
import android.media.tv.TvTrackInfo;
@@ -39,6 +40,7 @@
void createBiInteractiveApp(in Uri biIAppUri, in Bundle params);
void destroyBiInteractiveApp(in String biIAppId);
void setTeletextAppEnabled(boolean enable);
+ void sendCurrentVideoBounds(in Rect bounds);
void sendCurrentChannelUri(in Uri channelUri);
void sendCurrentChannelLcn(int lcn);
void sendStreamVolume(float volume);
@@ -48,6 +50,10 @@
void sendTvRecordingInfo(in TvRecordingInfo recordingInfo);
void sendTvRecordingInfoList(in List<TvRecordingInfo> recordingInfoList);
void notifyError(in String errMsg, in Bundle params);
+ void notifyTimeShiftPlaybackParams(in PlaybackParams params);
+ void notifyTimeShiftStatusChanged(in String inputId, int status);
+ void notifyTimeShiftStartPositionChanged(in String inputId, long timeMs);
+ void notifyTimeShiftCurrentPositionChanged(in String inputId, long timeMs);
void release();
void notifyTuned(in Uri channelUri);
void notifyTrackSelected(int type, in String trackId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index c5dbd19..d84affd 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -40,7 +40,9 @@
void onTeletextAppStateChanged(int state);
void onAdBuffer(in AdBuffer buffer);
void onCommandRequest(in String cmdType, in Bundle parameters);
+ void onTimeShiftCommandRequest(in String cmdType, in Bundle parameters);
void onSetVideoBounds(in Rect rect);
+ void onRequestCurrentVideoBounds();
void onRequestCurrentChannelUri();
void onRequestCurrentChannelLcn();
void onRequestStreamVolume();
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index a55e1ac..8a23e65 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -20,6 +20,7 @@
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Rect;
+import android.media.PlaybackParams;
import android.media.tv.AdBuffer;
import android.media.tv.AdResponse;
import android.media.tv.BroadcastInfoResponse;
@@ -89,6 +90,11 @@
private static final int DO_NOTIFY_TV_MESSAGE = 33;
private static final int DO_SEND_RECORDING_INFO = 34;
private static final int DO_SEND_RECORDING_INFO_LIST = 35;
+ private static final int DO_NOTIFY_TIME_SHIFT_PLAYBACK_PARAMS = 36;
+ private static final int DO_NOTIFY_TIME_SHIFT_STATUS_CHANGED = 37;
+ private static final int DO_NOTIFY_TIME_SHIFT_START_POSITION_CHANGED = 38;
+ private static final int DO_NOTIFY_TIME_SHIFT_CURRENT_POSITION_CHANGED = 39;
+ private static final int DO_SEND_CURRENT_VIDEO_BOUNDS = 40;
private final HandlerCaller mCaller;
private Session mSessionImpl;
@@ -152,6 +158,10 @@
mSessionImpl.setTeletextAppEnabled((Boolean) msg.obj);
break;
}
+ case DO_SEND_CURRENT_VIDEO_BOUNDS: {
+ mSessionImpl.sendCurrentVideoBounds((Rect) msg.obj);
+ break;
+ }
case DO_SEND_CURRENT_CHANNEL_URI: {
mSessionImpl.sendCurrentChannelUri((Uri) msg.obj);
break;
@@ -277,6 +287,30 @@
mSessionImpl.notifyAdBufferConsumed((AdBuffer) msg.obj);
break;
}
+ case DO_NOTIFY_TIME_SHIFT_PLAYBACK_PARAMS: {
+ mSessionImpl.notifyTimeShiftPlaybackParams((PlaybackParams) msg.obj);
+ break;
+ }
+ case DO_NOTIFY_TIME_SHIFT_STATUS_CHANGED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mSessionImpl.notifyTimeShiftStatusChanged((String) args.arg1, (Integer) args.arg2);
+ args.recycle();
+ break;
+ }
+ case DO_NOTIFY_TIME_SHIFT_START_POSITION_CHANGED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mSessionImpl.notifyTimeShiftStartPositionChanged(
+ (String) args.arg1, (Long) args.arg2);
+ args.recycle();
+ break;
+ }
+ case DO_NOTIFY_TIME_SHIFT_CURRENT_POSITION_CHANGED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mSessionImpl.notifyTimeShiftCurrentPositionChanged(
+ (String) args.arg1, (Long) args.arg2);
+ args.recycle();
+ break;
+ }
default: {
Log.w(TAG, "Unhandled message code: " + msg.what);
break;
@@ -326,6 +360,12 @@
}
@Override
+ public void sendCurrentVideoBounds(@Nullable Rect bounds) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageO(DO_SEND_CURRENT_VIDEO_BOUNDS, bounds));
+ }
+
+ @Override
public void sendCurrentChannelUri(@Nullable Uri channelUri) {
mCaller.executeOrSendMessage(
mCaller.obtainMessageO(DO_SEND_CURRENT_CHANNEL_URI, channelUri));
@@ -380,6 +420,30 @@
}
@Override
+ public void notifyTimeShiftPlaybackParams(@NonNull PlaybackParams params) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageO(DO_NOTIFY_TIME_SHIFT_PLAYBACK_PARAMS, params));
+ }
+
+ @Override
+ public void notifyTimeShiftStatusChanged(@NonNull String inputId, int status) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageOO(DO_NOTIFY_TIME_SHIFT_STATUS_CHANGED, inputId, status));
+ }
+
+ @Override
+ public void notifyTimeShiftStartPositionChanged(@NonNull String inputId, long timeMs) {
+ mCaller.executeOrSendMessage(mCaller.obtainMessageOO(
+ DO_NOTIFY_TIME_SHIFT_START_POSITION_CHANGED, inputId, timeMs));
+ }
+
+ @Override
+ public void notifyTimeShiftCurrentPositionChanged(@NonNull String inputId, long timeMs) {
+ mCaller.executeOrSendMessage(mCaller.obtainMessageOO(
+ DO_NOTIFY_TIME_SHIFT_CURRENT_POSITION_CHANGED, inputId, timeMs));
+ }
+
+ @Override
public void release() {
mSessionImpl.scheduleMediaViewCleanup();
mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE));
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index f4847f7..fd3c29b 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -23,6 +23,7 @@
import android.annotation.SystemService;
import android.content.Context;
import android.graphics.Rect;
+import android.media.PlaybackParams;
import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.AdResponse;
@@ -405,6 +406,21 @@
}
@Override
+ public void onTimeShiftCommandRequest(
+ @TvInteractiveAppService.TimeShiftCommandType String cmdType,
+ Bundle parameters,
+ int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postTimeShiftCommandRequest(cmdType, parameters);
+ }
+ }
+
+ @Override
public void onSetVideoBounds(Rect rect, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -429,6 +445,18 @@
}
@Override
+ public void onRequestCurrentVideoBounds(int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestCurrentVideoBounds();
+ }
+ }
+
+ @Override
public void onRequestCurrentChannelUri(int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -849,6 +877,24 @@
}
/**
+ * Returns a list of available app link information.
+ *
+ * <P>A package must declare its app link info in its manifest using meta-data tag, so the info
+ * can be detected by the system.
+ *
+ * @return List of {@link AppLinkInfo} for each package that deslares its app link information.
+ * @hide
+ */
+ @NonNull
+ public List<AppLinkInfo> getAppLinkInfoList() {
+ try {
+ return mService.getAppLinkInfoList(mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Registers an Android application link info record which can be used to launch the specific
* Android application by TV interactive App RTE.
*
@@ -1050,6 +1096,18 @@
}
}
+ void sendCurrentVideoBounds(@NonNull Rect bounds) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.sendCurrentVideoBounds(mToken, bounds, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
void sendCurrentChannelUri(@Nullable Uri channelUri) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
@@ -1182,6 +1240,55 @@
}
}
+ void notifyTimeShiftPlaybackParams(@NonNull PlaybackParams params) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.notifyTimeShiftPlaybackParams(mToken, params, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ void notifyTimeShiftStatusChanged(
+ @NonNull String inputId, @TvInputManager.TimeShiftStatus int status) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.notifyTimeShiftStatusChanged(mToken, inputId, status, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ void notifyTimeShiftStartPositionChanged(@NonNull String inputId, long timeMs) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.notifyTimeShiftStartPositionChanged(mToken, inputId, timeMs, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ void notifyTimeShiftCurrentPositionChanged(@NonNull String inputId, long timeMs) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.notifyTimeShiftCurrentPositionChanged(mToken, inputId, timeMs, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/**
* Sets the {@link android.view.Surface} for this session.
*
@@ -1795,6 +1902,17 @@
});
}
+ void postTimeShiftCommandRequest(
+ final @TvInteractiveAppService.TimeShiftCommandType String cmdType,
+ final Bundle parameters) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onTimeShiftCommandRequest(mSession, cmdType, parameters);
+ }
+ });
+ }
+
void postSetVideoBounds(Rect rect) {
mHandler.post(new Runnable() {
@Override
@@ -1804,6 +1922,15 @@
});
}
+ void postRequestCurrentVideoBounds() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestCurrentVideoBounds(mSession);
+ }
+ });
+ }
+
void postRequestCurrentChannelUri() {
mHandler.post(new Runnable() {
@Override
@@ -2003,6 +2130,20 @@
}
/**
+ * This is called when {@link TvInteractiveAppService.Session#requestTimeShiftCommand} is
+ * called.
+ *
+ * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
+ * @param cmdType type of the time shift command.
+ * @param parameters parameters of the command.
+ */
+ public void onTimeShiftCommandRequest(
+ Session session,
+ @TvInteractiveAppService.TimeShiftCommandType String cmdType,
+ Bundle parameters) {
+ }
+
+ /**
* This is called when {@link TvInteractiveAppService.Session#SetVideoBounds} is called.
*
* @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
@@ -2011,6 +2152,15 @@
}
/**
+ * This is called when {@link TvInteractiveAppService.Session#RequestCurrentVideoBounds} is
+ * called.
+ *
+ * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
+ */
+ public void onRequestCurrentVideoBounds(Session session) {
+ }
+
+ /**
* This is called when {@link TvInteractiveAppService.Session#RequestCurrentChannelUri} is
* called.
*
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 3ca9f2f..be2c16c 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -17,6 +17,7 @@
package android.media.tv.interactive;
import android.annotation.CallSuper;
+import android.annotation.IntDef;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -30,6 +31,7 @@
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.Rect;
+import android.media.PlaybackParams;
import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.AdResponse;
@@ -138,6 +140,38 @@
* Playback command type: select the given track.
*/
public static final String PLAYBACK_COMMAND_TYPE_SELECT_TRACK = "select_track";
+
+
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "PLAYBACK_COMMAND_STOP_MODE_", value = {
+ PLAYBACK_COMMAND_STOP_MODE_BLANK,
+ PLAYBACK_COMMAND_STOP_MODE_FREEZE
+ })
+ public @interface PlaybackCommandStopMode {}
+
+ /**
+ * Playback command stop mode: show a blank screen.
+ * @hide
+ */
+ public static final int PLAYBACK_COMMAND_STOP_MODE_BLANK = 1;
+
+ /**
+ * Playback command stop mode: freeze the video.
+ * @hide
+ */
+ public static final int PLAYBACK_COMMAND_STOP_MODE_FREEZE = 2;
+
+ /**
+ * Playback command parameter: stop mode.
+ * <p>Type: int
+ *
+ * @see #PLAYBACK_COMMAND_TYPE_STOP
+ * @hide
+ */
+ public static final String COMMAND_PARAMETER_KEY_STOP_MODE = "command_stop_mode";
+
/**
* Playback command parameter: channel URI.
* <p>Type: android.net.Uri
@@ -182,6 +216,78 @@
public static final String COMMAND_PARAMETER_KEY_CHANGE_CHANNEL_QUIETLY =
"command_change_channel_quietly";
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(prefix = "TIME_SHIFT_COMMAND_TYPE_", value = {
+ TIME_SHIFT_COMMAND_TYPE_PLAY,
+ TIME_SHIFT_COMMAND_TYPE_PAUSE,
+ TIME_SHIFT_COMMAND_TYPE_RESUME,
+ TIME_SHIFT_COMMAND_TYPE_SEEK_TO,
+ TIME_SHIFT_COMMAND_TYPE_SET_PLAYBACK_PARAMS,
+ })
+ public @interface TimeShiftCommandType {}
+
+ /**
+ * Time shift command type: play.
+ *
+ * @see TvView#timeShiftPlay(String, Uri)
+ * @hide
+ */
+ public static final String TIME_SHIFT_COMMAND_TYPE_PLAY = "play";
+ /**
+ * Time shift command type: pause.
+ *
+ * @see TvView#timeShiftPause()
+ * @hide
+ */
+ public static final String TIME_SHIFT_COMMAND_TYPE_PAUSE = "pause";
+ /**
+ * Time shift command type: resume.
+ *
+ * @see TvView#timeShiftResume()
+ * @hide
+ */
+ public static final String TIME_SHIFT_COMMAND_TYPE_RESUME = "resume";
+ /**
+ * Time shift command type: seek to.
+ *
+ * @see TvView#timeShiftSeekTo(long)
+ * @hide
+ */
+ public static final String TIME_SHIFT_COMMAND_TYPE_SEEK_TO = "seek_to";
+ /**
+ * Time shift command type: set playback params.
+ *
+ * @see TvView#timeShiftSetPlaybackParams(PlaybackParams)
+ * @hide
+ */
+ public static final String TIME_SHIFT_COMMAND_TYPE_SET_PLAYBACK_PARAMS = "set_playback_params";
+
+ /**
+ * Time shift command parameter: program URI.
+ * <p>Type: android.net.Uri
+ *
+ * @see #TIME_SHIFT_COMMAND_TYPE_PLAY
+ * @hide
+ */
+ public static final String COMMAND_PARAMETER_KEY_PROGRAM_URI = "command_program_uri";
+ /**
+ * Time shift command parameter: time position for time shifting, in milliseconds.
+ * <p>Type: long
+ *
+ * @see #TIME_SHIFT_COMMAND_TYPE_SEEK_TO
+ * @hide
+ */
+ public static final String COMMAND_PARAMETER_KEY_TIME_POSITION = "command_time_position";
+ /**
+ * Time shift command parameter: playback params.
+ * <p>Type: android.media.PlaybackParams
+ *
+ * @see #TIME_SHIFT_COMMAND_TYPE_SET_PLAYBACK_PARAMS
+ * @hide
+ */
+ public static final String COMMAND_PARAMETER_KEY_PLAYBACK_PARAMS = "command_playback_params";
+
private final Handler mServiceHandler = new ServiceHandler();
private final RemoteCallbackList<ITvInteractiveAppServiceCallback> mCallbacks =
new RemoteCallbackList<>();
@@ -425,6 +531,13 @@
}
/**
+ * Receives current video bounds.
+ * @hide
+ */
+ public void onCurrentVideoBounds(@NonNull Rect bounds) {
+ }
+
+ /**
* Receives current channel URI.
*/
public void onCurrentChannelUri(@Nullable Uri channelUri) {
@@ -520,6 +633,44 @@
}
/**
+ * Called when the time shift {@link android.media.PlaybackParams} is set or changed.
+ *
+ * @see TvView#timeShiftSetPlaybackParams(PlaybackParams)
+ * @hide
+ */
+ public void onTimeShiftPlaybackParams(@NonNull PlaybackParams params) {
+ }
+
+ /**
+ * Called when time shift status is changed.
+ *
+ * @see TvView.TvInputCallback#onTimeShiftStatusChanged(String, int)
+ * @see android.media.tv.TvInputService.Session#notifyTimeShiftStatusChanged(int)
+ * @hide
+ */
+ public void onTimeShiftStatusChanged(
+ @NonNull String inputId, @TvInputManager.TimeShiftStatus int status) {
+ }
+
+ /**
+ * Called when time shift start position is changed.
+ *
+ * @see TvView.TimeShiftPositionCallback#onTimeShiftStartPositionChanged(String, long)
+ * @hide
+ */
+ public void onTimeShiftStartPositionChanged(@NonNull String inputId, long timeMs) {
+ }
+
+ /**
+ * Called when time shift current position is changed.
+ *
+ * @see TvView.TimeShiftPositionCallback#onTimeShiftCurrentPositionChanged(String, long)
+ * @hide
+ */
+ public void onTimeShiftCurrentPositionChanged(@NonNull String inputId, long timeMs) {
+ }
+
+ /**
* Called when the application sets the surface.
*
* <p>The TV Interactive App service should render interactive app UI onto the given
@@ -820,6 +971,35 @@
}
/**
+ * Sends a specific time shift command to be processed by the related TV input.
+ *
+ * @param cmdType type of the specific command
+ * @param parameters parameters of the specific command
+ * @hide
+ */
+ @CallSuper
+ public void sendTimeShiftCommandRequest(
+ @TimeShiftCommandType @NonNull String cmdType, @Nullable Bundle parameters) {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestTimeShiftCommand (cmdType=" + cmdType
+ + ", parameters=" + parameters.toString() + ")");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onTimeShiftCommandRequest(cmdType, parameters);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestTimeShiftCommand", e);
+ }
+ }
+ });
+ }
+
+ /**
* Sets broadcast video bounds.
*/
@CallSuper
@@ -843,6 +1023,30 @@
}
/**
+ * Requests the bounds of the current video.
+ * @hide
+ */
+ @CallSuper
+ public void requestCurrentVideoBounds() {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestCurrentVideoBounds");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestCurrentVideoBounds();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestCurrentVideoBounds", e);
+ }
+ }
+ });
+ }
+
+ /**
* Requests the URI of the current channel.
*/
@CallSuper
@@ -1169,6 +1373,10 @@
onSetTeletextAppEnabled(enable);
}
+ void sendCurrentVideoBounds(@NonNull Rect bounds) {
+ onCurrentVideoBounds(bounds);
+ }
+
void sendCurrentChannelUri(@Nullable Uri channelUri) {
onCurrentChannelUri(channelUri);
}
@@ -1330,6 +1538,34 @@
}
/**
+ * Calls {@link #onTimeShiftPlaybackParams(PlaybackParams)}.
+ */
+ void notifyTimeShiftPlaybackParams(PlaybackParams params) {
+ onTimeShiftPlaybackParams(params);
+ }
+
+ /**
+ * Calls {@link #onTimeShiftStatusChanged(String, int)}.
+ */
+ void notifyTimeShiftStatusChanged(String inputId, int status) {
+ onTimeShiftStatusChanged(inputId, status);
+ }
+
+ /**
+ * Calls {@link #onTimeShiftStartPositionChanged(String, long)}.
+ */
+ void notifyTimeShiftStartPositionChanged(String inputId, long timeMs) {
+ onTimeShiftStartPositionChanged(inputId, timeMs);
+ }
+
+ /**
+ * Calls {@link #onTimeShiftCurrentPositionChanged(String, long)}.
+ */
+ void notifyTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
+ onTimeShiftCurrentPositionChanged(inputId, timeMs);
+ }
+
+ /**
* Notifies when the session state is changed.
*
* @param state the current session state.
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppServiceInfo.java b/media/java/android/media/tv/interactive/TvInteractiveAppServiceInfo.java
index 3e08852..acc2444 100644
--- a/media/java/android/media/tv/interactive/TvInteractiveAppServiceInfo.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppServiceInfo.java
@@ -57,6 +57,8 @@
INTERACTIVE_APP_TYPE_HBBTV,
INTERACTIVE_APP_TYPE_ATSC,
INTERACTIVE_APP_TYPE_GINGA,
+ INTERACTIVE_APP_TYPE_TARGETED_AD,
+ INTERACTIVE_APP_TYPE_OTHER
})
public @interface InteractiveAppType {}
@@ -66,10 +68,21 @@
public static final int INTERACTIVE_APP_TYPE_ATSC = 0x2;
/** Ginga interactive app type */
public static final int INTERACTIVE_APP_TYPE_GINGA = 0x4;
+ /**
+ * Targeted Advertisement interactive app type
+ * @hide
+ */
+ public static final int INTERACTIVE_APP_TYPE_TARGETED_AD = 0x8;
+ /**
+ * Other interactive app type
+ * @hide
+ */
+ public static final int INTERACTIVE_APP_TYPE_OTHER = 0x80000000;
private final ResolveInfo mService;
private final String mId;
private int mTypes;
+ private final List<String> mExtraTypes = new ArrayList<>();
/**
* Constructs a TvInteractiveAppServiceInfo object.
@@ -98,18 +111,21 @@
mService = resolveInfo;
mId = id;
- mTypes = toTypesFlag(types);
+ toTypesFlag(types);
}
- private TvInteractiveAppServiceInfo(ResolveInfo service, String id, int types) {
+ private TvInteractiveAppServiceInfo(
+ ResolveInfo service, String id, int types, List<String> extraTypes) {
mService = service;
mId = id;
mTypes = types;
+ mExtraTypes.addAll(extraTypes);
}
private TvInteractiveAppServiceInfo(@NonNull Parcel in) {
mService = ResolveInfo.CREATOR.createFromParcel(in);
mId = in.readString();
mTypes = in.readInt();
+ in.readStringList(mExtraTypes);
}
public static final @NonNull Creator<TvInteractiveAppServiceInfo> CREATOR =
@@ -135,6 +151,7 @@
mService.writeToParcel(dest, flags);
dest.writeString(mId);
dest.writeInt(mTypes);
+ dest.writeStringList(mExtraTypes);
}
/**
@@ -171,6 +188,17 @@
return mTypes;
}
+ /**
+ * Gets extra supported interactive app types which are not listed.
+ *
+ * @see #getSupportedTypes()
+ * @hide
+ */
+ @NonNull
+ public List<String> getExtraSupportedTypes() {
+ return mExtraTypes;
+ }
+
private static String generateInteractiveAppServiceId(ComponentName name) {
return name.flattenToShortString();
}
@@ -219,23 +247,27 @@
}
}
- private static int toTypesFlag(List<String> types) {
- int flag = 0;
+ private void toTypesFlag(List<String> types) {
+ mTypes = 0;
+ mExtraTypes.clear();
for (String type : types) {
switch (type) {
case "hbbtv":
- flag |= INTERACTIVE_APP_TYPE_HBBTV;
+ mTypes |= INTERACTIVE_APP_TYPE_HBBTV;
break;
case "atsc":
- flag |= INTERACTIVE_APP_TYPE_ATSC;
+ mTypes |= INTERACTIVE_APP_TYPE_ATSC;
break;
case "ginga":
- flag |= INTERACTIVE_APP_TYPE_GINGA;
+ mTypes |= INTERACTIVE_APP_TYPE_GINGA;
break;
+ case "targeted_ad":
+ mTypes |= INTERACTIVE_APP_TYPE_TARGETED_AD;
default:
+ mTypes |= INTERACTIVE_APP_TYPE_OTHER;
+ mExtraTypes.add(type);
break;
}
}
- return flag;
}
}
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 6777d1a..af03bb8 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -25,6 +25,7 @@
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.media.PlaybackParams;
import android.media.tv.TvInputManager;
import android.media.tv.TvRecordingInfo;
import android.media.tv.TvTrackInfo;
@@ -513,6 +514,19 @@
}
/**
+ * Sends current video bounds to related TV interactive app.
+ * @hide
+ */
+ public void sendCurrentVideoBounds(@NonNull Rect bounds) {
+ if (DEBUG) {
+ Log.d(TAG, "sendCurrentVideoBounds");
+ }
+ if (mSession != null) {
+ mSession.sendCurrentVideoBounds(bounds);
+ }
+ }
+
+ /**
* Sends current channel URI to related TV interactive app.
*
* @param channelUri The current channel URI; {@code null} if there is no currently tuned
@@ -684,6 +698,75 @@
}
}
+ /**
+ * Notifies the corresponding {@link TvInteractiveAppService} when a time shift
+ * {@link android.media.PlaybackParams} is set or changed.
+ *
+ * @see TvView#timeShiftSetPlaybackParams(PlaybackParams)
+ * @hide
+ */
+ public void notifyTimeShiftPlaybackParams(@NonNull PlaybackParams params) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyTimeShiftPlaybackParams params=" + params);
+ }
+ if (mSession != null) {
+ mSession.notifyTimeShiftPlaybackParams(params);
+ }
+ }
+
+ /**
+ * Notifies the corresponding {@link TvInteractiveAppService} when time shift
+ * status is changed.
+ *
+ * @see TvView.TvInputCallback#onTimeShiftStatusChanged(String, int)
+ * @see android.media.tv.TvInputService.Session#notifyTimeShiftStatusChanged(int)
+ * @hide
+ */
+ public void notifyTimeShiftStatusChanged(
+ @NonNull String inputId, @TvInputManager.TimeShiftStatus int status) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "notifyTimeShiftStatusChanged inputId=" + inputId + "; status=" + status);
+ }
+ if (mSession != null) {
+ mSession.notifyTimeShiftStatusChanged(inputId, status);
+ }
+ }
+
+ /**
+ * Notifies the corresponding {@link TvInteractiveAppService} when time shift
+ * start position is changed.
+ *
+ * @see TvView.TimeShiftPositionCallback#onTimeShiftStartPositionChanged(String, long)
+ * @hide
+ */
+ public void notifyTimeShiftStartPositionChanged(@NonNull String inputId, long timeMs) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyTimeShiftStartPositionChanged inputId=" + inputId
+ + "; timeMs=" + timeMs);
+ }
+ if (mSession != null) {
+ mSession.notifyTimeShiftStartPositionChanged(inputId, timeMs);
+ }
+ }
+
+ /**
+ * Notifies the corresponding {@link TvInteractiveAppService} when time shift
+ * current position is changed.
+ *
+ * @see TvView.TimeShiftPositionCallback#onTimeShiftCurrentPositionChanged(String, long)
+ * @hide
+ */
+ public void notifyTimeShiftCurrentPositionChanged(@NonNull String inputId, long timeMs) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyTimeShiftCurrentPositionChanged inputId=" + inputId
+ + "; timeMs=" + timeMs);
+ }
+ if (mSession != null) {
+ mSession.notifyTimeShiftCurrentPositionChanged(inputId, timeMs);
+ }
+ }
+
private void resetInternal() {
mSessionCallback = null;
if (mSession != null) {
@@ -808,6 +891,21 @@
}
/**
+ * This is called when a time shift command is requested to be processed by the related TV
+ * input.
+ *
+ * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+ * @param cmdType type of the command
+ * @param parameters parameters of the command
+ * @hide
+ */
+ public void onTimeShiftCommandRequest(
+ @NonNull String iAppServiceId,
+ @NonNull @TvInteractiveAppService.TimeShiftCommandType String cmdType,
+ @NonNull Bundle parameters) {
+ }
+
+ /**
* This is called when the state of corresponding interactive app is changed.
*
* @param iAppServiceId The ID of the TV interactive app service bound to this view.
@@ -859,6 +957,16 @@
}
/**
+ * This is called when {@link TvInteractiveAppService.Session#requestCurrentVideoBounds()}
+ * is called.
+ *
+ * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+ * @hide
+ */
+ public void onRequestCurrentVideoBounds(@NonNull String iAppServiceId) {
+ }
+
+ /**
* This is called when {@link TvInteractiveAppService.Session#requestCurrentChannelUri()} is
* called.
*
@@ -1068,6 +1176,33 @@
}
@Override
+ public void onTimeShiftCommandRequest(
+ Session session,
+ @TvInteractiveAppService.TimeShiftCommandType String cmdType,
+ Bundle parameters) {
+ if (DEBUG) {
+ Log.d(TAG, "onTimeShiftCommandRequest (cmdType=" + cmdType + ", parameters="
+ + parameters.toString() + ")");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onTimeShiftCommandRequest - session not created");
+ return;
+ }
+ synchronized (mCallbackLock) {
+ if (mCallbackExecutor != null) {
+ mCallbackExecutor.execute(() -> {
+ synchronized (mCallbackLock) {
+ if (mCallback != null) {
+ mCallback.onTimeShiftCommandRequest(
+ mIAppServiceId, cmdType, parameters);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ @Override
public void onSessionStateChanged(
Session session,
@TvInteractiveAppManager.InteractiveAppState int state,
@@ -1153,6 +1288,28 @@
}
@Override
+ public void onRequestCurrentVideoBounds(Session session) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestCurrentVideoBounds");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestCurrentVideoBounds - session not created");
+ return;
+ }
+ synchronized (mCallbackLock) {
+ if (mCallbackExecutor != null) {
+ mCallbackExecutor.execute(() -> {
+ synchronized (mCallbackLock) {
+ if (mCallback != null) {
+ mCallback.onRequestCurrentVideoBounds(mIAppServiceId);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ @Override
public void onRequestCurrentChannelUri(Session session) {
if (DEBUG) {
Log.d(TAG, "onRequestCurrentChannelUri");
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index d8705a7..5b0c2a2 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -2432,13 +2432,12 @@
throwExceptionAsNecessary(env, BAD_VALUE);
return;
}
- NativeCryptoInfo cryptoInfo = [env, cryptoInfoObj, size]{
- if (cryptoInfoObj == nullptr) {
- return NativeCryptoInfo{size};
- } else {
- return NativeCryptoInfo{env, cryptoInfoObj};
- }
- }();
+ auto cryptoInfo =
+ cryptoInfoObj ? NativeCryptoInfo{size} : NativeCryptoInfo{env, cryptoInfoObj};
+ if (env->ExceptionCheck()) {
+ // Creation of cryptoInfo failed. Let the exception bubble up.
+ return;
+ }
err = codec->queueEncryptedLinearBlock(
index,
memory,
diff --git a/media/jni/android_media_MediaPlayer.cpp b/media/jni/android_media_MediaPlayer.cpp
index da920bb..9552200 100644
--- a/media/jni/android_media_MediaPlayer.cpp
+++ b/media/jni/android_media_MediaPlayer.cpp
@@ -956,14 +956,16 @@
static void
android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this,
- jobject jAttributionSource)
+ jobject jAttributionSource,
+ jint jAudioSessionId)
{
ALOGV("native_setup");
Parcel* parcel = parcelForJavaObject(env, jAttributionSource);
android::content::AttributionSourceState attributionSource;
attributionSource.readFromParcel(parcel);
- sp<MediaPlayer> mp = sp<MediaPlayer>::make(attributionSource);
+ sp<MediaPlayer> mp = sp<MediaPlayer>::make(
+ attributionSource, static_cast<audio_session_t>(jAudioSessionId));
if (mp == NULL) {
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
@@ -1419,7 +1421,9 @@
{"native_setMetadataFilter", "(Landroid/os/Parcel;)I", (void *)android_media_MediaPlayer_setMetadataFilter},
{"native_getMetadata", "(ZZLandroid/os/Parcel;)Z", (void *)android_media_MediaPlayer_getMetadata},
{"native_init", "()V", (void *)android_media_MediaPlayer_native_init},
- {"native_setup", "(Ljava/lang/Object;Landroid/os/Parcel;)V",(void *)android_media_MediaPlayer_native_setup},
+ {"native_setup",
+ "(Ljava/lang/Object;Landroid/os/Parcel;I)V",
+ (void *)android_media_MediaPlayer_native_setup},
{"native_finalize", "()V", (void *)android_media_MediaPlayer_native_finalize},
{"getAudioSessionId", "()I", (void *)android_media_MediaPlayer_get_audio_session_id},
{"native_setAudioSessionId", "(I)V", (void *)android_media_MediaPlayer_set_audio_session_id},
diff --git a/media/lib/remotedisplay/java/com/android/media/remotedisplay/RemoteDisplayProvider.java b/media/lib/remotedisplay/java/com/android/media/remotedisplay/RemoteDisplayProvider.java
index 2cba03b..8752e3d 100644
--- a/media/lib/remotedisplay/java/com/android/media/remotedisplay/RemoteDisplayProvider.java
+++ b/media/lib/remotedisplay/java/com/android/media/remotedisplay/RemoteDisplayProvider.java
@@ -312,7 +312,7 @@
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
mSettingsPendingIntent = PendingIntent.getActivity(
- mContext, 0, settingsIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED, null);
+ mContext, 0, settingsIntent, PendingIntent.FLAG_IMMUTABLE, null);
}
return mSettingsPendingIntent;
}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerUnitTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerUnitTest.java
index f480566..f812d5f 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerUnitTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerUnitTest.java
@@ -23,6 +23,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -56,6 +57,7 @@
MediaPlayer mediaPlayer = new MediaPlayer(virtualDeviceContext);
assertNotEquals(vdmPlaybackSessionId, mediaPlayer.getAudioSessionId());
+ assertTrue(mediaPlayer.getAudioSessionId() > 0);
}
@Test
diff --git a/native/android/input.cpp b/native/android/input.cpp
index 5e5ebed..f1c3088 100644
--- a/native/android/input.cpp
+++ b/native/android/input.cpp
@@ -299,6 +299,8 @@
return AMOTION_EVENT_CLASSIFICATION_TWO_FINGER_SWIPE;
case android::MotionClassification::MULTI_FINGER_SWIPE:
return AMOTION_EVENT_CLASSIFICATION_MULTI_FINGER_SWIPE;
+ case android::MotionClassification::PINCH:
+ return AMOTION_EVENT_CLASSIFICATION_PINCH;
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 1e5bd7b..7d43364 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -152,22 +152,6 @@
return CreateFlowUtils.toRequestDisplayInfo(requestInfo, context)
}
- companion object {
- // TODO: find a way to resolve this static field leak problem
- lateinit var repo: CredentialManagerRepo
-
- fun setup(
- context: Context,
- intent: Intent,
- ) {
- repo = CredentialManagerRepo(context, intent)
- }
-
- fun getInstance(): CredentialManagerRepo {
- return repo
- }
- }
-
// TODO: below are prototype functionalities. To be removed for productionization.
private fun testCreateCredentialEnabledProviderList(): List<CreateCredentialProviderData> {
return listOf(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index 686415f..0620f9a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -45,19 +45,19 @@
class CredentialSelectorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- CredentialManagerRepo.setup(this, intent)
+ val credManRepo = CredentialManagerRepo(this, intent)
UserConfigRepo.setup(this)
- val requestInfo = CredentialManagerRepo.getInstance().requestInfo
+ val requestInfo = credManRepo.requestInfo
setContent {
CredentialSelectorTheme {
- CredentialManagerBottomSheet(DialogType.toDialogType(requestInfo.type))
+ CredentialManagerBottomSheet(DialogType.toDialogType(requestInfo.type), credManRepo)
}
}
}
@ExperimentalMaterialApi
@Composable
- fun CredentialManagerBottomSheet(dialogType: DialogType) {
+ fun CredentialManagerBottomSheet(dialogType: DialogType, credManRepo: CredentialManagerRepo) {
val providerActivityResult = remember { mutableStateOf<ProviderActivityResult?>(null) }
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
@@ -66,7 +66,9 @@
}
when (dialogType) {
DialogType.CREATE_PASSKEY -> {
- val viewModel: CreateCredentialViewModel = viewModel()
+ val viewModel: CreateCredentialViewModel = viewModel{
+ CreateCredentialViewModel(credManRepo)
+ }
lifecycleScope.launch {
viewModel.observeDialogResult().collect{ dialogResult ->
onCancel(dialogResult)
@@ -79,7 +81,9 @@
CreateCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
}
DialogType.GET_CREDENTIALS -> {
- val viewModel: GetCredentialViewModel = viewModel()
+ val viewModel: GetCredentialViewModel = viewModel{
+ GetCredentialViewModel(credManRepo)
+ }
lifecycleScope.launch {
viewModel.observeDialogResult().collect{ dialogResult ->
onCancel(dialogResult)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
index 3e1137d..ac84503 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
@@ -52,10 +52,9 @@
)
class CreateCredentialViewModel(
- credManRepo: CredentialManagerRepo = CredentialManagerRepo.getInstance(),
- userConfigRepo: UserConfigRepo = UserConfigRepo.getInstance()
+ private val credManRepo: CredentialManagerRepo,
+ userConfigRepo: UserConfigRepo = UserConfigRepo.getInstance(),
) : ViewModel() {
-
var providerEnableListUiState = credManRepo.getCreateProviderEnableListInitialUiState()
var providerDisableListUiState = credManRepo.getCreateProviderDisableListInitialUiState()
@@ -142,12 +141,12 @@
}
fun onDisabledProvidersSelected() {
- CredentialManagerRepo.getInstance().onCancel()
+ credManRepo.onCancel()
dialogResult.tryEmit(DialogResult(ResultState.LAUNCH_SETTING_CANCELED))
}
fun onCancel() {
- CredentialManagerRepo.getInstance().onCancel()
+ credManRepo.onCancel()
dialogResult.tryEmit(DialogResult(ResultState.NORMAL_CANCELED))
}
@@ -189,7 +188,7 @@
hidden = true,
)
} else {
- CredentialManagerRepo.getInstance().onOptionSelected(
+ credManRepo.onOptionSelected(
providerId,
entryKey,
entrySubkey
@@ -243,7 +242,7 @@
"$providerId, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " +
"resultCode=$resultCode, resultData=$resultData}"
)
- CredentialManagerRepo.getInstance().onOptionSelected(
+ credManRepo.onOptionSelected(
providerId, entry.entryKey, entry.entrySubkey, resultCode, resultData,
)
} else {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
index af59a0a..6f0f76b 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
@@ -46,9 +46,7 @@
val isNoAccount: Boolean = false,
)
-class GetCredentialViewModel(
- credManRepo: CredentialManagerRepo = CredentialManagerRepo.getInstance()
-) : ViewModel() {
+class GetCredentialViewModel(private val credManRepo: CredentialManagerRepo) : ViewModel() {
var uiState by mutableStateOf(credManRepo.getCredentialInitialUiState())
private set
@@ -70,9 +68,7 @@
hidden = true,
)
} else {
- CredentialManagerRepo.getInstance().onOptionSelected(
- entry.providerId, entry.entryKey, entry.entrySubkey,
- )
+ credManRepo.onOptionSelected(entry.providerId, entry.entryKey, entry.entrySubkey)
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
}
}
@@ -110,7 +106,7 @@
"${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " +
"resultCode=$resultCode, resultData=$resultData}"
)
- CredentialManagerRepo.getInstance().onOptionSelected(
+ credManRepo.onOptionSelected(
entry.providerId, entry.entryKey, entry.entrySubkey,
resultCode, resultData,
)
@@ -144,7 +140,7 @@
}
fun onCancel() {
- CredentialManagerRepo.getInstance().onCancel()
+ credManRepo.onCancel()
dialogResult.tryEmit(DialogResult(ResultState.NORMAL_CANCELED))
}
}
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index 468a976..1592094 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -59,6 +59,18 @@
private Uri mImageUri;
private Drawable mImageDrawable;
private View mMiddleGroundView;
+ private OnBindListener mOnBindListener;
+
+ /**
+ * Interface to listen in on when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
+ */
+ public interface OnBindListener {
+ /**
+ * Called when when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
+ * @param animationView the animation view for this preference.
+ */
+ void onBind(LottieAnimationView animationView);
+ }
private final Animatable2.AnimationCallback mAnimationCallback =
new Animatable2.AnimationCallback() {
@@ -133,6 +145,17 @@
if (IS_ENABLED_LOTTIE_ADAPTIVE_COLOR) {
ColorUtils.applyDynamicColors(getContext(), illustrationView);
}
+
+ if (mOnBindListener != null) {
+ mOnBindListener.onBind(illustrationView);
+ }
+ }
+
+ /**
+ * Sets a listener to be notified when the views are binded.
+ */
+ public void setOnBindListener(OnBindListener listener) {
+ mOnBindListener = listener;
}
/**
diff --git a/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml b/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml
index 2efa107..d1dceb3 100644
--- a/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml
+++ b/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml
@@ -15,4 +15,8 @@
limitations under the License.
-->
-<manifest package="com.android.settingslib.spaprivileged" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.spaprivileged">
+<uses-permission android:name="android.permission.MANAGE_USERS" />
+</manifest>
+
diff --git a/packages/SettingsLib/SpaPrivileged/res/values/strings.xml b/packages/SettingsLib/SpaPrivileged/res/values/strings.xml
index 25dbe00..e1e7649 100644
--- a/packages/SettingsLib/SpaPrivileged/res/values/strings.xml
+++ b/packages/SettingsLib/SpaPrivileged/res/values/strings.xml
@@ -27,4 +27,6 @@
<string name="app_permission_summary_not_allowed">Not allowed</string>
<!-- Manage applications, version string displayed in app snippet -->
<string name="version_text">version <xliff:g id="version_num">%1$s</xliff:g></string>
+ <!-- Label of an app on App Info page of Cloned Apps menu [CHAR LIMIT=40] -->
+ <string name="cloned_app_info_label"><xliff:g id="package_label">%1$s</xliff:g> clone</string>
</resources>
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
index 90710db..18b20733 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
@@ -19,9 +19,11 @@
import android.content.Context
import android.content.pm.ApplicationInfo
import android.graphics.drawable.Drawable
+import android.os.UserManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.android.settingslib.Utils
import com.android.settingslib.spa.framework.compose.rememberContext
@@ -36,12 +38,24 @@
fun loadLabel(app: ApplicationInfo): String
@Composable
- fun produceLabel(app: ApplicationInfo) =
- produceState(initialValue = stringResource(R.string.summary_placeholder), app) {
+ fun produceLabel(app: ApplicationInfo, isClonedAppPage: Boolean = false): State<String> {
+ val context = LocalContext.current
+ return produceState(initialValue = stringResource(R.string.summary_placeholder), app) {
withContext(Dispatchers.IO) {
- value = loadLabel(app)
+ if (isClonedAppPage || isCloneApp(context, app)) {
+ value = context.getString(R.string.cloned_app_info_label, loadLabel(app))
+ } else {
+ value = loadLabel(app)
+ }
}
}
+ }
+
+ private fun isCloneApp(context: Context, app: ApplicationInfo): Boolean {
+ val userManager = context.getSystemService(UserManager::class.java)!!
+ val userInfo = userManager.getUserInfo(app.userId)
+ return userInfo != null && userInfo.isCloneProfile
+ }
@Composable
fun produceIcon(app: ApplicationInfo): State<Drawable?>
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
index 16ca70f..602df54 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
@@ -43,7 +43,7 @@
class AppInfoProvider(private val packageInfo: PackageInfo) {
@Composable
- fun AppInfo(displayVersion: Boolean = false) {
+ fun AppInfo(displayVersion: Boolean = false, isClonedAppPage: Boolean = false) {
Column(
modifier = Modifier
.fillMaxWidth()
@@ -57,7 +57,7 @@
Box(modifier = Modifier.padding(SettingsDimension.itemPaddingAround)) {
AppIcon(app = app, size = SettingsDimension.appIconInfoSize)
}
- AppLabel(app)
+ AppLabel(app, isClonedAppPage)
InstallType(app)
if (displayVersion) AppVersion()
}
@@ -99,7 +99,7 @@
}
@Composable
-internal fun AppLabel(app: ApplicationInfo) {
+internal fun AppLabel(app: ApplicationInfo, isClonedAppPage: Boolean = false) {
val appRepository = rememberAppRepository()
- SettingsTitle(title = appRepository.produceLabel(app), useMediumWeight = true)
+ SettingsTitle(title = appRepository.produceLabel(app, isClonedAppPage), useMediumWeight = true)
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
index 29549d9..103512d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
@@ -61,6 +61,8 @@
private PreferenceViewHolder mViewHolder;
private FrameLayout mMiddleGroundLayout;
private final Context mContext = ApplicationProvider.getApplicationContext();
+ private IllustrationPreference.OnBindListener mOnBindListener;
+ private LottieAnimationView mOnBindListenerAnimationView;
@Before
public void setUp() {
@@ -82,6 +84,12 @@
final AttributeSet attributeSet = Robolectric.buildAttributeSet().build();
mPreference = new IllustrationPreference(mContext, attributeSet);
+ mOnBindListener = new IllustrationPreference.OnBindListener() {
+ @Override
+ public void onBind(LottieAnimationView animationView) {
+ mOnBindListenerAnimationView = animationView;
+ }
+ };
}
@Test
@@ -186,4 +194,25 @@
assertThat(mBackgroundView.getMaxHeight()).isEqualTo(restrictedHeight);
assertThat(mAnimationView.getMaxHeight()).isEqualTo(restrictedHeight);
}
+
+ @Test
+ public void setOnBindListener_isNotified() {
+ mOnBindListenerAnimationView = null;
+ mPreference.setOnBindListener(mOnBindListener);
+
+ mPreference.onBindViewHolder(mViewHolder);
+
+ assertThat(mOnBindListenerAnimationView).isNotNull();
+ assertThat(mOnBindListenerAnimationView).isEqualTo(mAnimationView);
+ }
+
+ @Test
+ public void setOnBindListener_notNotified() {
+ mOnBindListenerAnimationView = null;
+ mPreference.setOnBindListener(null);
+
+ mPreference.onBindViewHolder(mViewHolder);
+
+ assertThat(mOnBindListenerAnimationView).isNull();
+ }
}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index ecb88f6..4e620cd1 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -296,7 +296,7 @@
<queries>
<intent>
- <action android:name="android.intent.action.NOTES" />
+ <action android:name="android.intent.action.CREATE_NOTE" />
</intent>
</queries>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
index a494f5e..0b1a3e2 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
+++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
@@ -20,6 +20,17 @@
android_app {
name: "AccessibilityMenu",
+
+ static_libs: [
+ "androidx.coordinatorlayout_coordinatorlayout",
+ "androidx.core_core",
+ "androidx.viewpager_viewpager",
+ ],
+
+ uses_libs: [
+ "org.apache.http.legacy",
+ ],
+
srcs: [
"src/**/*.java",
],
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/color/footer_icon_tint_color.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/color/footer_icon_tint_color.xml
new file mode 100644
index 0000000..c89e4c3
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/color/footer_icon_tint_color.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:color="@color/footer_icon_disabled_color" /> <!-- disabled -->
+ <item android:color="@color/footer_icon_enabled_color" /> <!-- default -->
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/a11ymenu_intro.png b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/a11ymenu_intro.png
new file mode 100644
index 0000000..6149ee4
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/a11ymenu_intro.png
Binary files differ
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/footer_button_background_left.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/footer_button_background_left.xml
new file mode 100644
index 0000000..5ff245d
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/footer_button_background_left.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item>
+ <ripple
+ android:color="@color/ripple_material_color">
+ <item android:id="@android:id/mask">
+ <color android:color="@color/overlay_bg_color"/>
+ </item>
+ </ripple>
+ </item>
+
+</layer-list>
\ No newline at end of file
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/footer_button_background_right.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/footer_button_background_right.xml
new file mode 100644
index 0000000..5ff245d
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/footer_button_background_right.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item>
+ <ripple
+ android:color="@color/ripple_material_color">
+ <item android:id="@android:id/mask">
+ <color android:color="@color/overlay_bg_color"/>
+ </item>
+ </ripple>
+ </item>
+
+</layer-list>
\ No newline at end of file
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_a11y_menu_round.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_a11y_menu_round.xml
new file mode 100644
index 0000000..a2eaf95
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_a11y_menu_round.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ 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.
+ -->
+
+<vector android:height="108dp" android:viewportHeight="24"
+ android:viewportWidth="24" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#ffffff" android:fillType="evenOdd" android:pathData="M7.875,10.625C7.1188,10.625 6.5,11.2437 6.5,12C6.5,12.7562 7.1188,13.375 7.875,13.375C8.6313,13.375 9.25,12.7562 9.25,12C9.25,11.2437 8.6313,10.625 7.875,10.625ZM16.125,10.625C15.3687,10.625 14.75,11.2437 14.75,12C14.75,12.7562 15.3687,13.375 16.125,13.375C16.8813,13.375 17.5,12.7562 17.5,12C17.5,11.2437 16.8813,10.625 16.125,10.625ZM10.625,12C10.625,11.2437 11.2438,10.625 12,10.625C12.7563,10.625 13.375,11.2437 13.375,12C13.375,12.7562 12.7563,13.375 12,13.375C11.2438,13.375 10.625,12.7562 10.625,12Z"/>
+</vector>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_add_32dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_add_32dp.xml
new file mode 100644
index 0000000..7e1262c
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_add_32dp.xml
@@ -0,0 +1,5 @@
+<vector android:height="32dp" android:tint="#FFFFFF"
+ android:viewportHeight="24.0" android:viewportWidth="24.0"
+ android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+</vector>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_arrow_back_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_arrow_back_24dp.xml
new file mode 100644
index 0000000..f6af270
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_arrow_back_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/footer_arrow_length"
+ android:height="@dimen/footer_arrow_length"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/footer_icon_color"
+ android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
+</vector>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_arrow_forward_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_arrow_forward_24dp.xml
new file mode 100644
index 0000000..2f7b632
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_arrow_forward_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/footer_arrow_length"
+ android:height="@dimen/footer_arrow_length"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/footer_icon_color"
+ android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
+</vector>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_menu.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_menu.xml
new file mode 100644
index 0000000..79e0e08d
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_menu.xml
@@ -0,0 +1,33 @@
+<vector android:height="48dp" android:viewportHeight="192.0"
+ android:viewportWidth="192.0" android:width="48dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#34a853" android:pathData="M37.14,173.74l-28.53,-90a14.53,14.53 0,0 1,5 -15.63L87.15,11a14.21,14.21 0,0 1,17.61 0.12l73.81,58.94a14.53,14.53 0,0 1,4.8 15.57l-28.48,88.18A14.32,14.32 0,0 1,141.22 184H50.84A14.33,14.33 0,0 1,37.14 173.74Z"/>
+ <path android:pathData="M137.61,94.07l-17,17 -17,-17 -17,17L70.3,94.72l-17,17L125.66,184h15.56a14.32,14.32 0,0 0,13.67 -10.19l15.25,-47.21Z">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="27152.64"
+ android:endY="32745.600000000002"
+ android:startX="20910.72"
+ android:startY="21934.079999999998" android:type="linear">
+ <item android:color="#33263238" android:offset="0.0"/>
+ <item android:color="#11205432" android:offset="0.47"/>
+ <item android:color="#051E6130" android:offset="1.0"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.2" android:fillColor="#263238" android:pathData="M50.14,100.11a12,12 0,1 1,11.39 15.77,11.72 11.72,0 0,1 -5,-1.1l-3.41,-3.4ZM129.4,91.88a12,12 0,1 1,-12 12A12,12 0,0 1,129.4 91.88ZM95.4,91.88a12,12 0,1 1,-12 12A12,12 0,0 1,95.42 91.88Z"/>
+ <path android:fillColor="#fff"
+ android:pathData="M61.53,90.88a12,12 0,1 1,-12 12A12,12 0,0 1,61.53 90.88ZM129.41,90.88a12,12 0,1 1,-12 12A12,12 0,0 1,129.41 90.88ZM95.41,90.88a12,12 0,1 1,-12 12A12,12 0,0 1,95.42 90.88Z"
+ android:strokeAlpha="0" android:strokeColor="#000"
+ android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0.5"/>
+ <path android:fillAlpha="0.2" android:fillColor="#263238" android:pathData="M184,80.91a14.33,14.33 0,0 1,-0.63 4.7l-28.48,88.18A14.33,14.33 0,0 1,141.21 184H50.84a14.33,14.33 0,0 1,-13.7 -10.26l-28.53,-90A14.49,14.49 0,0 1,8 79.11a14.3,14.3 0,0 0,0.61 3.64l28.53,90A14.33,14.33 0,0 0,50.84 183h90.37a14.33,14.33 0,0 0,13.67 -10.19l28.48,-88.18A14.79,14.79 0,0 0,184 80.91Z"/>
+ <path android:fillAlpha="0.2" android:fillColor="#fff" android:pathData="M184,81.89A14.46,14.46 0,0 0,178.57 71L104.76,12.1A14.21,14.21 0,0 0,87.15 12L13.58,69.12A14.5,14.5 0,0 0,8 80.09a14.5,14.5 0,0 1,5.57 -12L87.15,11a14.21,14.21 0,0 1,17.61 0.12L178.57,70A14.48,14.48 0,0 1,184 81.89Z"/>
+ <path android:pathData="M37.14,173.74l-28.53,-90a14.53,14.53 0,0 1,5 -15.63L87.15,11a14.21,14.21 0,0 1,17.61 0.12l73.81,58.94a14.53,14.53 0,0 1,4.8 15.57l-28.48,88.18A14.32,14.32 0,0 1,141.22 184H50.84A14.33,14.33 0,0 1,37.14 173.74Z"/>
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/colorAccessibilityMenuIcon" />
+ <foreground>
+ <inset
+ android:drawable="@drawable/ic_a11y_menu_round"
+ android:inset="21.88%" />
+ </foreground>
+ </adaptive-icon>
+</vector>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_assistant_32dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_assistant_32dp.xml
new file mode 100644
index 0000000..ebeebf8
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_assistant_32dp.xml
@@ -0,0 +1,8 @@
+<vector android:height="32dp"
+ android:viewportHeight="192.0" android:viewportWidth="192.0"
+ android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#34A853" android:pathData="M172,60m-12,0a12,12 0,1 1,24 0a12,12 0,1 1,-24 0"/>
+ <path android:fillColor="#EA4335" android:pathData="M136,88m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
+ <path android:fillColor="#FBBC05" android:pathData="M136,148m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"/>
+ <path android:fillColor="#4285F4" android:pathData="M56,64m-48,0a48,48 0,1 1,96 0a48,48 0,1 1,-96 0"/>
+</vector>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_0deg.9.png b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_0deg.9.png
new file mode 100644
index 0000000..b0d1696
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_0deg.9.png
Binary files differ
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_270deg.9.png b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_270deg.9.png
new file mode 100644
index 0000000..b777ffe
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_270deg.9.png
Binary files differ
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_90deg.9.png b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_90deg.9.png
new file mode 100644
index 0000000..998bd90
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_90deg.9.png
Binary files differ
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/view_background.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/view_background.xml
new file mode 100644
index 0000000..c1f76f37
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/view_background.xml
@@ -0,0 +1,5 @@
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/overlay_bg_color" />
+</shape>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/footerlayout_switch_page.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/footerlayout_switch_page.xml
new file mode 100644
index 0000000..658c03b
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/footerlayout_switch_page.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/footerlayout"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/grid_item_btn_view_height"
+ android:layout_alignParentBottom="true"
+ android:layout_gravity="bottom"
+ android:layoutDirection="ltr"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <View
+ android:id="@+id/top_listDivider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?android:attr/listDivider"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/menu_prev_button"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:background="@drawable/footer_button_background_left"
+ android:contentDescription="@string/previous_button_content_description"
+ android:scaleType="centerInside"
+ android:src="@drawable/ic_arrow_back_24dp"
+ android:tint="@color/footer_icon_tint_color"/>
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:background="?android:attr/listDivider"/>
+
+ <ImageButton
+ android:id="@+id/menu_next_button"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:background="@drawable/footer_button_background_right"
+ android:contentDescription="@string/next_button_content_description"
+ android:scaleType="centerInside"
+ android:src="@drawable/ic_arrow_forward_24dp"
+ android:tint="@color/footer_icon_tint_color"/>
+
+ </LinearLayout>
+
+ <View
+ android:id="@+id/bottom_listDivider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?android:attr/listDivider"/>
+
+</LinearLayout>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml
new file mode 100644
index 0000000..39e5a8c
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="@dimen/grid_item_padding"
+ android:paddingBottom="@dimen/grid_item_padding"
+ android:gravity="center">
+
+ <ImageButton
+ android:id="@+id/shortcutIconBtn"
+ android:layout_width="@dimen/image_button_width"
+ android:layout_height="@dimen/image_button_height"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:scaleType="fitCenter"></ImageButton>
+
+<TextView
+ android:id="@+id/shortcutLabel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/grid_item_text_view_margin_top"
+ android:layout_below="@+id/shortcutIconBtn"
+ android:layout_centerHorizontal="true"
+ android:ellipsize="end"
+ android:gravity="center_horizontal"
+ android:importantForAccessibility="no"
+ android:lines="2"
+ android:textSize="@dimen/label_text_size"
+ android:textAlignment="center"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.Widget.Button"/>
+
+</RelativeLayout>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_view.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_view.xml
new file mode 100644
index 0000000..c198443
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_view.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/gridview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:horizontalSpacing="@dimen/a11ymenu_grid_layout_margin"
+ android:listSelector="@android:color/transparent"
+ android:numColumns="3"
+ android:overScrollMode="never"
+ android:stretchMode="columnWidth">
+</GridView>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/paged_menu.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/paged_menu.xml
new file mode 100644
index 0000000..28a633e
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/paged_menu.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/row_width"
+ android:layout_height="match_parent"
+ android:id="@+id/coordinatorLayout"
+ android:background="@drawable/view_background"
+ >
+ <LinearLayout
+ android:layout_width="@dimen/row_width"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <androidx.viewpager.widget.ViewPager
+ android:id="@+id/view_pager"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/table_margin_top"
+ android:paddingBottom="@dimen/a11ymenu_layout_margin"
+ android:paddingLeft="@dimen/a11ymenu_layout_margin"
+ android:paddingRight="@dimen/a11ymenu_layout_margin"
+ android:layout_gravity="center"
+ android:gravity="center"
+ />
+
+ <include layout="@layout/footerlayout_switch_page"/>
+ </LinearLayout>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values-land/dimens.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values-land/dimens.xml
new file mode 100644
index 0000000..69f0934
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values-land/dimens.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="table_margin_top">0dp</dimen>
+ <dimen name="row_width">388dp</dimen>
+ <dimen name="image_button_height">45dp</dimen>
+ <dimen name="image_button_width">45dp</dimen>
+ <dimen name="image_button_marginBottom">1dp</dimen>
+
+ <!-- dimens for gridview layout. -->
+ <dimen name="grid_item_padding">4dp</dimen>
+
+</resources>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/colors.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/colors.xml
new file mode 100644
index 0000000..33c0cca
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/colors.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<resources>
+ <color name="power_color">#dadce0</color>
+ <color name="quick_settings_color">#78d9ec</color>
+ <color name="a11y_settings_color">#d9affe</color>
+ <color name="recent_apps_color">#f0a5dd</color>
+ <color name="lockscreen_color">#85e4a0</color>
+ <color name="volume_color">#7ae3d4</color>
+ <color name="notifications_color">#f496ac</color>
+ <color name="screenshot_color">#adcbff</color>
+ <color name="assistant_color">#F1F3F4</color>
+ <color name="brightness_color">#fdd663</color>
+
+ <color name="ripple_material_color">#10FFFFFF</color>
+
+ <color name="overlay_bg_color">#313235</color>
+ <color name="footer_icon_color">#E8EAED</color>
+ <color name="footer_icon_enabled_color">#E8EAED</color>
+ <color name="footer_icon_disabled_color">#5F6368</color>
+ <color name="colorControlNormal">#202124</color>
+
+</resources>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml
new file mode 100644
index 0000000..81b3152
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!--Adds the theme to support SnackBar component and user configurable theme. -->
+ <style name="ServiceTheme" parent="android:Theme.DeviceDefault.DayNight">
+ <item name="android:colorControlNormal">@color/colorControlNormal</item>
+ </style>
+
+</resources>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/bool.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/bool.xml
new file mode 100644
index 0000000..2f9d6b5
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/bool.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="isAtLeastP">true</bool>
+
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/colors.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/colors.xml
new file mode 100644
index 0000000..36d1fc1
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/colors.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<resources>
+ <color name="power_color">#757575</color>
+ <color name="quick_settings_color">#2196F3</color>
+ <color name="a11y_settings_color">#5806C9</color>
+ <color name="recent_apps_color">#AD2EC6</color>
+ <color name="lockscreen_color">#0F9D58</color>
+ <color name="volume_color">#01A2A0</color>
+ <color name="notifications_color">#F15B8D</color>
+ <color name="screenshot_color">#26459C</color>
+ <color name="assistant_color">#F1F3F4</color>
+ <color name="brightness_color">#E59810</color>
+ <color name="colorAccent">#1a73e8</color>
+
+ <color name="ripple_material_color">#1f000000</color>
+
+ <color name="overlay_bg_color">@android:color/white</color>
+ <color name="footer_icon_color">@android:color/black</color>
+ <color name="footer_icon_enabled_color">@android:color/black</color>
+ <color name="footer_icon_disabled_color">#ddd</color>
+ <color name="colorControlNormal">@android:color/white</color>
+
+ <color name="colorAccessibilityMenuIcon">#3AA757</color>
+</resources>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/dimens.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/dimens.xml
new file mode 100644
index 0000000..7ed1897
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/dimens.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- the curve radius for the background of the complete layout -->
+ <dimen name="table_margin_top">22dp</dimen>
+ <dimen name="row_width">@dimen/custom_match_parent</dimen>
+ <dimen name="image_button_height">60dp</dimen>
+ <dimen name="image_button_width">60dp</dimen>
+ <dimen name="image_button_marginBottom">2dp</dimen>
+ <dimen name="a11ymenu_layout_margin">4dp</dimen>
+ <dimen name="custom_match_parent">-1px</dimen>
+
+ <!-- dimens for gridview layout. -->
+ <dimen name="grid_item_text_view_margin_top">2dp</dimen>
+ <dimen name="grid_item_padding">10dp</dimen>
+ <dimen name="grid_item_btn_view_height">48dp</dimen>
+ <dimen name="a11ymenu_grid_layout_margin">8dp</dimen>
+
+ <!-- dimens for a11y menu footer layout. -->
+ <dimen name="footer_arrow_length">24dp</dimen>
+
+ <!-- text size for shortcut label when large button settings in on. -->
+ <dimen name="large_label_text_size">18sp</dimen>
+ <dimen name="label_text_size">14sp</dimen>
+
+</resources>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/donottranslate.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/donottranslate.xml
new file mode 100644
index 0000000..0c25ec4
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/donottranslate.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- user customized shortcuts preference -->
+ <string name="pref_user_shortcuts">accessibility_menu_user_shortcuts</string>
+ <!-- key for user customized shortcuts -->
+ <string name="pref_user_shortcuts_key">pref_user_shortcuts_key</string>
+ <!-- value for empty shortcut -->
+ <string name="pref_user_shortcuts_value_empty">[]</string>
+ <!-- empty string for shortcut label -->
+ <string name="empty_content"></string>
+
+ <string name="pref_large_buttons">pref_large_buttons</string>
+
+ <!-- key for Help&feedback settings [CHAR_LIMIT=NONE] -->
+ <string name="pref_help">pref_help</string>
+
+</resources>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/strings.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/strings.xml
new file mode 100644
index 0000000..30fd017
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/strings.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- String defining the service name -->
+ <string name="accessibility_menu_service_name">Accessibility Menu</string>
+ <!-- Accessibility Menu detail intro. [CHAR_LIMIT=NONE] -->
+ <string name="accessibility_menu_intro">
+ The Accessibility Menu provides a large on-screen menu to control your device. You can lock your device, control volume and brightness, take screenshots, and more.
+ </string>
+ <!-- String defining the label for the assistant button -->
+ <string name="assistant_label">Assistant</string>
+ <!-- String defining utterance for the assistant button for screen readers -->
+ <string name="assistant_utterance">Google Assistant</string>
+ <!-- String defining the label for the accessibility settings button -->
+ <string name="a11y_settings_label">Accessibility Settings</string>
+ <!-- String defining the label for the volume button -->
+ <string name="volume_label">Volume</string>
+ <!-- String defining utterance for the volume button for screen readers -->
+ <string name="volume_utterance">Volume controls</string>
+ <!-- String defining the label for the power button -->
+ <string name="power_label">Power</string>
+ <!-- String defining utterance for the power button for screen readers -->
+ <string name="power_utterance">Power options</string>
+ <!-- String defining the label for the recent apps button -->
+ <string name="recent_apps_label">Recent apps</string>
+ <!-- String defining the label for the lockscreen button -->
+ <string name="lockscreen_label">Lock screen</string>
+ <!-- String defining the label for the quick settings button -->
+ <string name="quick_settings_label">Quick Settings</string>
+ <!-- String defining the label for the notifications button -->
+ <string name="notifications_label">Notifications</string>
+ <!-- String defining the label for the screenshot button -->
+ <string name="screenshot_label">Screenshot</string>
+ <!-- String defining the utterance for the screenshot button for screen readers -->
+ <string name="screenshot_utterance">Take screenshot</string>
+ <!-- String defining the label for the volume up/down button -->
+ <string name="volume_up_label">Volume up</string>
+ <string name="volume_down_label">Volume down</string>
+ <!-- String defining the label for the brightness up/down button -->
+ <string name="brightness_up_label">Brightness up</string>
+ <string name="brightness_down_label">Brightness down</string>
+ <!-- String defining the content description for the footer previous/next button -->
+ <string name="previous_button_content_description">Go to previous screen</string>
+ <string name="next_button_content_description">Go to next screen</string>
+
+ <string name="accessibility_menu_description">
+ The Accessibility Menu provides a large on-screen menu to control your device. You can lock your device, control volume and brightness, take screenshots, and more.
+ </string>
+ <!-- Short summary of app that appears as subtext on the service preference in Settings -->
+ <string name="accessibility_menu_summary">Control device via large menu</string>
+
+ <!-- TODO(b/113371047): string need to be reviewed -->
+ <!-- String defining the settings name -->
+ <string name="accessibility_menu_settings_name">Accessibility Menu Settings</string>
+
+ <!-- String defining the title of Large button setting -->
+ <string name="accessibility_menu_large_buttons_title">Large buttons</string>
+ <!-- String defining the summary of Large button setting -->
+ <string name="accessibility_menu_large_buttons_summary">Increase size of Accessibility Menu Buttons</string>
+ <!-- String defining the title of the preference to show help and feedback menu [CHAR LIMIT=40] -->
+ <string name="pref_help_and_feedback_title">Help & feedback</string>
+ <!-- String defining the title of the preference to show help menu [CHAR LIMIT=40] -->
+ <string name="pref_help_title">Help</string>
+
+ <!-- The percentage of the brightness, and double "%" is required to represent the symbol "%" -->
+ <string name="brightness_percentage_label">Brightness <xliff:g id="percentage">%1$s</xliff:g> %%</string>
+ <!-- The percentage of the music volume, and double "%" is required to represent the symbol "%" -->
+ <string name="music_volume_percentage_label">Music volume <xliff:g id="percentage">%1$s</xliff:g> %%</string>
+
+ <!-- The label of a settings item that displays legal information about the licenses used in this app. [CHAR LIMIT=NONE] -->
+ <string name="pref_item_licenses">Open Source Licenses</string>
+
+</resources>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml
new file mode 100644
index 0000000..a2cf267
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!--The theme is for preference CollapsingToolbarBaseActivity settings-->
+ <style name="AccessibilityMenuSettings" parent="android:Theme.DeviceDefault.Light" />
+
+ <!--Adds the theme to support SnackBar component and user configurable theme. -->
+ <style name="ServiceTheme" parent="android:Theme.DeviceDefault.Light">
+ <item name="android:colorControlNormal">@color/colorControlNormal</item>
+ </style>
+
+ <!--The basic theme for service and test case only-->
+ <style name="A11yMenuBaseTheme" parent="android:Theme.DeviceDefault.Light">
+ <item name="android:windowActionBar">false</item>
+ </style>
+</resources>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml
index 96882d33..3dbbb1a 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml
@@ -13,4 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"/>
\ No newline at end of file
+<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accessibilityFeedbackType="feedbackGeneric"
+ android:accessibilityFlags="flagRequestAccessibilityButton|flagRequestFilterKeyEvents"
+ android:canRequestFilterKeyEvents="true"
+ android:summary="@string/accessibility_menu_summary"
+ android:intro="@string/accessibility_menu_intro"
+ android:animatedImageDrawable="@drawable/a11ymenu_intro"
+ android:isAccessibilityTool="true"
+/>
\ No newline at end of file
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
index 8b75900..5c4fdcc 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
@@ -17,16 +17,39 @@
package com.android.systemui.accessibility.accessibilitymenu;
import android.accessibilityservice.AccessibilityService;
+import android.view.MotionEvent;
+import android.view.View;
import android.view.accessibility.AccessibilityEvent;
+import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuOverlayLayout;
+
/** @hide */
-public class AccessibilityMenuService extends AccessibilityService {
+public class AccessibilityMenuService extends AccessibilityService implements View.OnTouchListener {
+ private static final String TAG = "A11yMenuService";
+
+ private A11yMenuOverlayLayout mA11yMenuLayout;
@Override
- public void onAccessibilityEvent(AccessibilityEvent event) {
+ public void onCreate() {
+ super.onCreate();
}
@Override
+ protected void onServiceConnected() {
+ mA11yMenuLayout = new A11yMenuOverlayLayout(this);
+ super.onServiceConnected();
+ mA11yMenuLayout.toggleVisibility();
+ }
+
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {}
+
+ @Override
public void onInterrupt() {
}
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ return false;
+ }
}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java
new file mode 100644
index 0000000..fa42e61
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 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 com.android.systemui.accessibility.accessibilitymenu.model;
+
+import com.android.systemui.accessibility.accessibilitymenu.R;
+
+/** Provides a data structure for a11y menu shortcuts. */
+public class A11yMenuShortcut {
+
+ public enum ShortcutId {
+ UNSPECIFIED_ID_VALUE,
+ ID_ASSISTANT_VALUE,
+ ID_A11YSETTING_VALUE,
+ ID_POWER_VALUE,
+ ID_VOLUME_DOWN_VALUE,
+ ID_VOLUME_UP_VALUE,
+ ID_RECENT_VALUE,
+ ID_BRIGHTNESS_DOWN_VALUE,
+ ID_BRIGHTNESS_UP_VALUE,
+ ID_LOCKSCREEN_VALUE,
+ ID_QUICKSETTING_VALUE,
+ ID_NOTIFICATION_VALUE,
+ ID_SCREENSHOT_VALUE
+ }
+
+ private static final String TAG = "A11yMenuShortcut";
+
+ /** Shortcut id used to identify. */
+ private int mShortcutId = ShortcutId.UNSPECIFIED_ID_VALUE.ordinal();
+
+ // Resource IDs of shortcut button and label.
+ public int imageSrc;
+ public int imageColor;
+ public int imgContentDescription;
+ public int labelText;
+
+ public A11yMenuShortcut(int id) {
+ setId(id);
+ }
+
+ /**
+ * Sets Id to shortcut, checks the value first and updates shortcut resources. It will set id to
+ *
+ * @param id id set to shortcut
+ */
+ public void setId(int id) {
+ mShortcutId = id;
+
+ // TODO(jonesriley) load the proper resources based on id
+ imageSrc = R.drawable.ic_logo_assistant_32dp;
+ imageColor = android.R.color.darker_gray;
+ imgContentDescription = R.string.empty_content;
+ labelText = R.string.empty_content;
+ }
+
+ public int getId() {
+ return mShortcutId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof A11yMenuShortcut)) {
+ return false;
+ }
+
+ A11yMenuShortcut targetObject = (A11yMenuShortcut) o;
+
+ return mShortcutId == targetObject.mShortcutId;
+ }
+
+ @Override
+ public int hashCode() {
+ return mShortcutId;
+ }
+}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/utils/ShortcutDrawableUtils.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/utils/ShortcutDrawableUtils.java
new file mode 100644
index 0000000..28ba4b5
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/utils/ShortcutDrawableUtils.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 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 com.android.systemui.accessibility.accessibilitymenu.utils;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.RippleDrawable;
+
+import com.android.systemui.accessibility.accessibilitymenu.R;
+
+/** Creates background drawable for a11y menu shortcut. */
+public class ShortcutDrawableUtils {
+
+ /**
+ * To make the circular background of shortcut icons have higher resolution. The higher value of
+ * LENGTH is, the higher resolution of the circular background are.
+ */
+ private static final int LENGTH = 480;
+
+ private static final int RADIUS = LENGTH / 2;
+ private static final int COORDINATE = LENGTH / 2;
+ private static final int RIPPLE_COLOR_ID = R.color.ripple_material_color;
+
+ private final Context mContext;
+ private final ColorStateList mRippleColorStateList;
+
+ // Placeholder of drawable to prevent NullPointerException
+ private final ColorDrawable mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT);
+
+ public ShortcutDrawableUtils(Context context) {
+ this.mContext = context;
+
+ int rippleColor = context.getColor(RIPPLE_COLOR_ID);
+ mRippleColorStateList = ColorStateList.valueOf(rippleColor);
+ }
+
+ /**
+ * Creates a circular drawable in specific color for shortcut.
+ *
+ * @param colorResId color resource ID
+ * @return drawable circular drawable
+ */
+ public Drawable createCircularDrawable(int colorResId) {
+ Bitmap output = Bitmap.createBitmap(LENGTH, LENGTH, Config.ARGB_8888);
+ Canvas canvas = new Canvas(output);
+ int color = mContext.getColor(colorResId);
+ Paint paint = new Paint();
+ paint.setColor(color);
+ paint.setStrokeCap(Paint.Cap.ROUND);
+ paint.setStyle(Style.FILL);
+ canvas.drawCircle(COORDINATE, COORDINATE, RADIUS, paint);
+
+ BitmapDrawable drawable = new BitmapDrawable(mContext.getResources(), output);
+ return drawable;
+ }
+
+ /**
+ * Creates an adaptive icon drawable in specific color for shortcut.
+ *
+ * @param colorResId color resource ID
+ * @return drawable for adaptive icon
+ */
+ public Drawable createAdaptiveIconDrawable(int colorResId) {
+ Drawable circleLayer = createCircularDrawable(colorResId);
+ RippleDrawable rippleLayer = new RippleDrawable(mRippleColorStateList, null, null);
+
+ AdaptiveIconDrawable adaptiveIconDrawable =
+ new AdaptiveIconDrawable(circleLayer, mTransparentDrawable);
+
+ Drawable[] layers = {adaptiveIconDrawable, rippleLayer};
+ LayerDrawable drawable = new LayerDrawable(layers);
+ return drawable;
+ }
+}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java
new file mode 100644
index 0000000..e3401a9
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java
@@ -0,0 +1,145 @@
+/*
+ * 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.accessibility.accessibilitymenu.view;
+
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+import android.view.TouchDelegate;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
+import com.android.systemui.accessibility.accessibilitymenu.R;
+import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
+import com.android.systemui.accessibility.accessibilitymenu.utils.ShortcutDrawableUtils;
+
+import java.util.List;
+
+/** GridView Adapter for a11y menu overlay. */
+public class A11yMenuAdapter extends BaseAdapter {
+
+ // The large scale of shortcut icon and label.
+ private static final float LARGE_BUTTON_SCALE = 1.5f;
+ private final int mLargeTextSize;
+
+ private final AccessibilityMenuService mService;
+ private final LayoutInflater mInflater;
+ private final List<A11yMenuShortcut> mShortcutDataList;
+ private final ShortcutDrawableUtils mShortcutDrawableUtils;
+
+ public A11yMenuAdapter(
+ AccessibilityMenuService service, List<A11yMenuShortcut> shortcutDataList) {
+ this.mService = service;
+ this.mShortcutDataList = shortcutDataList;
+ mInflater = LayoutInflater.from(service);
+
+ mShortcutDrawableUtils = new ShortcutDrawableUtils(service);
+
+ mLargeTextSize =
+ service.getResources().getDimensionPixelOffset(R.dimen.large_label_text_size);
+ }
+
+ @Override
+ public int getCount() {
+ return mShortcutDataList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mShortcutDataList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mShortcutDataList.get(position).getId();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ convertView = mInflater.inflate(R.layout.grid_item, null);
+
+ A11yMenuShortcut shortcutItem = (A11yMenuShortcut) getItem(position);
+ // Sets shortcut icon and label resource.
+ configureShortcutView(convertView, shortcutItem);
+
+ expandIconTouchArea(convertView);
+ setActionForMenuShortcut(convertView);
+ return convertView;
+ }
+
+ /**
+ * Expand shortcut icon touch area to the border of grid item.
+ * The height is from the top of icon to the bottom of label.
+ * The width is from the left border of grid item to the right border of grid item.
+ */
+ private void expandIconTouchArea(View convertView) {
+ ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn);
+ TextView shortcutLabel = convertView.findViewById(R.id.shortcutLabel);
+
+ shortcutIconButton.post(
+ () -> {
+ Rect iconHitRect = new Rect();
+ shortcutIconButton.getHitRect(iconHitRect);
+ Rect labelHitRect = new Rect();
+ shortcutLabel.getHitRect(labelHitRect);
+
+ final int widthAdjustment = iconHitRect.left;
+ iconHitRect.left = 0;
+ iconHitRect.right += widthAdjustment;
+ iconHitRect.top = 0;
+ iconHitRect.bottom = labelHitRect.bottom;
+ ((View) shortcutIconButton.getParent())
+ .setTouchDelegate(new TouchDelegate(iconHitRect, shortcutIconButton));
+ });
+ }
+
+ private void setActionForMenuShortcut(View convertView) {
+ ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn);
+
+ shortcutIconButton.setOnClickListener(
+ (View v) -> {
+ // Handles shortcut click event by AccessibilityMenuService.
+ // service.handleClick(v);
+ });
+ }
+
+ private void configureShortcutView(View convertView, A11yMenuShortcut shortcutItem) {
+ ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn);
+ TextView shortcutLabel = convertView.findViewById(R.id.shortcutLabel);
+
+ // TODO: Enlarge shortcut icon & label when large button setting is on.
+
+ if (shortcutItem.getId() == A11yMenuShortcut.ShortcutId.UNSPECIFIED_ID_VALUE.ordinal()) {
+ // Sets empty shortcut icon and label when the shortcut is ADD_ITEM.
+ shortcutIconButton.setImageResource(android.R.color.transparent);
+ shortcutIconButton.setBackground(null);
+ } else {
+ // Sets shortcut ID as tagId, to handle menu item click in AccessibilityMenuService.
+ shortcutIconButton.setTag(shortcutItem.getId());
+ shortcutIconButton.setContentDescription(
+ mService.getString(shortcutItem.imgContentDescription));
+ shortcutLabel.setText(shortcutItem.labelText);
+ shortcutIconButton.setImageResource(shortcutItem.imageSrc);
+
+ shortcutIconButton.setBackground(
+ mShortcutDrawableUtils.createAdaptiveIconDrawable(shortcutItem.imageColor));
+ }
+ }
+}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuFooter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuFooter.java
new file mode 100644
index 0000000..20c63df
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuFooter.java
@@ -0,0 +1,125 @@
+/*
+ * 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.accessibility.accessibilitymenu.view;
+
+import android.graphics.Rect;
+import android.view.TouchDelegate;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.ImageButton;
+
+import androidx.annotation.Nullable;
+
+import com.android.systemui.accessibility.accessibilitymenu.R;
+
+/**
+ * This class is for Accessibility menu footer layout. Handles switching between a11y menu pages.
+ */
+public class A11yMenuFooter {
+
+ /** Provides an interface for footer of a11yMenu. */
+ public interface A11yMenuFooterCallBack {
+
+ /** Calls back when user clicks the left button. */
+ void onLeftButtonClicked();
+
+ /** Calls back when user clicks the right button. */
+ void onRightButtonClicked();
+ }
+
+ private final FooterButtonClickListener mFooterButtonClickListener;
+
+ private ImageButton mPreviousPageBtn;
+ private ImageButton mNextPageBtn;
+ private View mTopListDivider;
+ private View mBottomListDivider;
+ private final A11yMenuFooterCallBack mCallBack;
+
+ public A11yMenuFooter(ViewGroup menuLayout, A11yMenuFooterCallBack callBack) {
+ this.mCallBack = callBack;
+ mFooterButtonClickListener = new FooterButtonClickListener();
+ configureFooterLayout(menuLayout);
+ }
+
+ public @Nullable ImageButton getPreviousPageBtn() {
+ return mPreviousPageBtn;
+ }
+
+ public @Nullable ImageButton getNextPageBtn() {
+ return mNextPageBtn;
+ }
+
+ private void configureFooterLayout(ViewGroup menuLayout) {
+ ViewGroup footerContainer = menuLayout.findViewById(R.id.footerlayout);
+ footerContainer.setVisibility(View.VISIBLE);
+
+ mPreviousPageBtn = menuLayout.findViewById(R.id.menu_prev_button);
+ mNextPageBtn = menuLayout.findViewById(R.id.menu_next_button);
+ mTopListDivider = menuLayout.findViewById(R.id.top_listDivider);
+ mBottomListDivider = menuLayout.findViewById(R.id.bottom_listDivider);
+
+ // Registers listeners for footer buttons.
+ setListener(mPreviousPageBtn);
+ setListener(mNextPageBtn);
+
+ menuLayout
+ .getViewTreeObserver()
+ .addOnGlobalLayoutListener(
+ new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ menuLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ expandBtnTouchArea(mPreviousPageBtn, menuLayout);
+ expandBtnTouchArea(mNextPageBtn, (View) mNextPageBtn.getParent());
+ }
+ });
+ }
+
+ private void expandBtnTouchArea(ImageButton btn, View btnParent) {
+ Rect btnRect = new Rect();
+ btn.getHitRect(btnRect);
+ btnRect.top -= getHitRectHeight(mTopListDivider);
+ btnRect.bottom += getHitRectHeight(mBottomListDivider);
+ btnParent.setTouchDelegate(new TouchDelegate(btnRect, btn));
+ }
+
+ private static int getHitRectHeight(View listDivider) {
+ Rect hitRect = new Rect();
+ listDivider.getHitRect(hitRect);
+ return hitRect.height();
+ }
+
+ private void setListener(@Nullable View view) {
+ if (view != null) {
+ view.setOnClickListener(mFooterButtonClickListener);
+ }
+ }
+
+ /** Handles click event for footer buttons. */
+ private class FooterButtonClickListener implements OnClickListener {
+ @Override
+ public void onClick(View view) {
+ if (view.getId() == R.id.menu_prev_button) {
+ mCallBack.onLeftButtonClicked();
+ } else if (view.getId() == R.id.menu_next_button) {
+ mCallBack.onRightButtonClicked();
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java
new file mode 100644
index 0000000..740bc8a
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.accessibilitymenu.view;
+
+import static java.lang.Math.max;
+
+import android.content.res.Configuration;
+import android.graphics.Insets;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+import android.widget.FrameLayout;
+import android.widget.Toast;
+
+import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
+import com.android.systemui.accessibility.accessibilitymenu.R;
+import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides functionality for Accessibility menu layout in a11y menu overlay. There are functions to
+ * configure or update Accessibility menu layout when orientation and display size changed, and
+ * functions to toggle menu visibility when button clicked or screen off.
+ */
+public class A11yMenuOverlayLayout {
+
+ /** Predefined default shortcuts when large button setting is off. */
+ private static final int[] SHORTCUT_LIST_DEFAULT = {
+ A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(),
+ A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(),
+ A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(),
+ A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(),
+ A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(),
+ A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(),
+ A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(),
+ A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(),
+ A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(),
+ A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(),
+ A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(),
+ A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal()
+ };
+
+ private final AccessibilityMenuService mService;
+ private final WindowManager mWindowManager;
+ private ViewGroup mLayout;
+ private WindowManager.LayoutParams mLayoutParameter;
+ private A11yMenuViewPager mA11yMenuViewPager;
+
+ public A11yMenuOverlayLayout(AccessibilityMenuService service) {
+ mService = service;
+ mWindowManager = mService.getSystemService(WindowManager.class);
+ configureLayout();
+ }
+
+ /** Creates Accessibility menu layout and configure layout parameters. */
+ public View configureLayout() {
+ return configureLayout(A11yMenuViewPager.DEFAULT_PAGE_INDEX);
+ }
+
+ // TODO(b/78292783): Find a better way to inflate layout in the test.
+ /**
+ * Creates Accessibility menu layout, configure layout parameters and apply index to ViewPager.
+ *
+ * @param pageIndex the index of the ViewPager to show.
+ */
+ public View configureLayout(int pageIndex) {
+
+ int lastVisibilityState = View.GONE;
+ if (mLayout != null) {
+ lastVisibilityState = mLayout.getVisibility();
+ mWindowManager.removeView(mLayout);
+ mLayout = null;
+ }
+
+ if (mLayoutParameter == null) {
+ initLayoutParams();
+ }
+
+ mLayout = new FrameLayout(mService);
+ updateLayoutPosition();
+ inflateLayoutAndSetOnTouchListener(mLayout);
+ mA11yMenuViewPager = new A11yMenuViewPager(mService);
+ mA11yMenuViewPager.configureViewPagerAndFooter(mLayout, createShortcutList(), pageIndex);
+ mWindowManager.addView(mLayout, mLayoutParameter);
+ mLayout.setVisibility(lastVisibilityState);
+
+ return mLayout;
+ }
+
+ /** Updates view layout with new layout parameters only. */
+ public void updateViewLayout() {
+ if (mLayout == null || mLayoutParameter == null) {
+ return;
+ }
+ updateLayoutPosition();
+ mWindowManager.updateViewLayout(mLayout, mLayoutParameter);
+ }
+
+ private void initLayoutParams() {
+ mLayoutParameter = new WindowManager.LayoutParams();
+ mLayoutParameter.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
+ mLayoutParameter.format = PixelFormat.TRANSLUCENT;
+ mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+ mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+ mLayoutParameter.setTitle(mService.getString(R.string.accessibility_menu_service_name));
+ }
+
+ private void inflateLayoutAndSetOnTouchListener(ViewGroup view) {
+ LayoutInflater inflater = LayoutInflater.from(mService);
+ inflater.inflate(R.layout.paged_menu, view);
+ view.setOnTouchListener(mService);
+ }
+
+ /**
+ * Loads shortcut data from default shortcut ID array.
+ *
+ * @return A list of default shortcuts
+ */
+ private List<A11yMenuShortcut> createShortcutList() {
+ List<A11yMenuShortcut> shortcutList = new ArrayList<>();
+ for (int shortcutId : SHORTCUT_LIST_DEFAULT) {
+ shortcutList.add(new A11yMenuShortcut(shortcutId));
+ }
+ return shortcutList;
+ }
+
+ /** Updates a11y menu layout position by configuring layout params. */
+ private void updateLayoutPosition() {
+ Display display = mLayout.getDisplay();
+ final int orientation = mService.getResources().getConfiguration().orientation;
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ switch (display.getRotation()) {
+ case Surface.ROTATION_90:
+ case Surface.ROTATION_180:
+ mLayoutParameter.gravity =
+ Gravity.END | Gravity.BOTTOM
+ | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL;
+ mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT;
+ mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+ mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+ mLayout.setBackgroundResource(R.drawable.shadow_90deg);
+ break;
+ case Surface.ROTATION_0:
+ case Surface.ROTATION_270:
+ mLayoutParameter.gravity =
+ Gravity.START | Gravity.BOTTOM
+ | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL;
+ mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT;
+ mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+ mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+ mLayout.setBackgroundResource(R.drawable.shadow_270deg);
+ break;
+ default:
+ break;
+ }
+ } else {
+ mLayoutParameter.gravity = Gravity.BOTTOM;
+ mLayoutParameter.width = WindowManager.LayoutParams.MATCH_PARENT;
+ mLayoutParameter.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ mLayout.setBackgroundResource(R.drawable.shadow_0deg);
+ }
+
+ // Adjusts the y position of a11y menu layout to make the layout not to overlap bottom
+ // navigation bar window.
+ updateLayoutByWindowInsetsIfNeeded();
+ mLayout.setOnApplyWindowInsetsListener(
+ (view, insets) -> {
+ if (updateLayoutByWindowInsetsIfNeeded()) {
+ mWindowManager.updateViewLayout(mLayout, mLayoutParameter);
+ }
+ return view.onApplyWindowInsets(insets);
+ });
+ }
+
+ /**
+ * Returns {@code true} if the a11y menu layout params
+ * should be updated by {@link WindowManager} immediately due to window insets change.
+ * This method adjusts the layout position and size to
+ * make a11y menu not to overlap navigation bar window.
+ */
+ private boolean updateLayoutByWindowInsetsIfNeeded() {
+ boolean shouldUpdateLayout = false;
+ WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
+ Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility(
+ WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
+ int xOffset = max(windowInsets.left, windowInsets.right);
+ int yOffset = windowInsets.bottom;
+ Rect windowBound = windowMetrics.getBounds();
+ if (mLayoutParameter.x != xOffset || mLayoutParameter.y != yOffset) {
+ mLayoutParameter.x = xOffset;
+ mLayoutParameter.y = yOffset;
+ shouldUpdateLayout = true;
+ }
+ // for gestural navigation mode and the landscape mode,
+ // the layout height should be decreased by system bar
+ // and display cutout inset to fit the new
+ // frame size that doesn't overlap the navigation bar window.
+ int orientation = mService.getResources().getConfiguration().orientation;
+ if (mLayout.getHeight() != mLayoutParameter.height
+ && orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ mLayoutParameter.height = windowBound.height() - yOffset;
+ shouldUpdateLayout = true;
+ }
+ return shouldUpdateLayout;
+ }
+
+ /**
+ * Gets the current page index when device configuration changed. {@link
+ * AccessibilityMenuService#onConfigurationChanged(Configuration)}
+ *
+ * @return the current index of the ViewPager.
+ */
+ public int getPageIndex() {
+ if (mA11yMenuViewPager != null) {
+ return mA11yMenuViewPager.mViewPager.getCurrentItem();
+ }
+ return A11yMenuViewPager.DEFAULT_PAGE_INDEX;
+ }
+
+ /**
+ * Hides a11y menu layout. And return if layout visibility has been changed.
+ *
+ * @return {@code true} layout visibility is toggled off; {@code false} is unchanged
+ */
+ public boolean hideMenu() {
+ if (mLayout.getVisibility() == View.VISIBLE) {
+ mLayout.setVisibility(View.GONE);
+ return true;
+ }
+ return false;
+ }
+
+ /** Toggles a11y menu layout visibility. */
+ public void toggleVisibility() {
+ mLayout.setVisibility((mLayout.getVisibility() == View.VISIBLE) ? View.GONE : View.VISIBLE);
+ }
+
+ /** Shows hint text on Toast. */
+ public void showToast(String text) {
+ final View viewPos = mLayout.findViewById(R.id.coordinatorLayout);
+ Toast.makeText(viewPos.getContext(), text, Toast.LENGTH_SHORT).show();
+ }
+}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java
new file mode 100644
index 0000000..c510b87
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java
@@ -0,0 +1,356 @@
+/*
+ * 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.accessibility.accessibilitymenu.view;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Insets;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+import android.widget.GridView;
+
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
+import com.android.systemui.accessibility.accessibilitymenu.R;
+import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
+import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuFooter.A11yMenuFooterCallBack;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class handles UI for viewPager and footer.
+ * It displays grid pages containing all shortcuts in viewPager,
+ * and handles the click events from footer to switch between pages.
+ */
+public class A11yMenuViewPager {
+
+ /** The default index of the ViewPager. */
+ public static final int DEFAULT_PAGE_INDEX = 0;
+
+ /**
+ * The class holds the static parameters for grid view when large button settings is on/off.
+ */
+ public static final class GridViewParams {
+ /** Total shortcuts count in the grid view when large button settings is off. */
+ public static final int GRID_ITEM_COUNT = 9;
+
+ /** The number of columns in the grid view when large button settings is off. */
+ public static final int GRID_COLUMN_COUNT = 3;
+
+ /** Total shortcuts count in the grid view when large button settings is on. */
+ public static final int LARGE_GRID_ITEM_COUNT = 4;
+
+ /** The number of columns in the grid view when large button settings is on. */
+ public static final int LARGE_GRID_COLUMN_COUNT = 2;
+
+ /** Temporary measure to test both item types. */
+ private static final boolean USE_LARGE_ITEMS = true;
+
+ /**
+ * Returns the number of items in the grid view.
+ *
+ * @param context The parent context
+ * @return Grid item count
+ */
+ public static int getGridItemCount(Context context) {
+ return USE_LARGE_ITEMS
+ ? LARGE_GRID_ITEM_COUNT
+ : GRID_ITEM_COUNT;
+ }
+
+ /**
+ * Returns the number of columns in the grid view.
+ *
+ * @param context The parent context
+ * @return Grid column count
+ */
+ public static int getGridColumnCount(Context context) {
+ return USE_LARGE_ITEMS
+ ? LARGE_GRID_COLUMN_COUNT
+ : GRID_COLUMN_COUNT;
+ }
+
+ /**
+ * Returns the number of rows in the grid view.
+ *
+ * @param context The parent context
+ * @return Grid row count
+ */
+ public static int getGridRowCount(Context context) {
+ return USE_LARGE_ITEMS
+ ? (LARGE_GRID_ITEM_COUNT / LARGE_GRID_COLUMN_COUNT)
+ : (GRID_ITEM_COUNT / GRID_COLUMN_COUNT);
+ }
+
+ /**
+ * Separates a provided list of accessibility shortcuts into multiple sub-lists.
+ * Does not modify the original list.
+ *
+ * @param pageItemCount The maximum size of an individual sub-list.
+ * @param shortcutList The list of shortcuts to be separated into sub-lists.
+ * @return A list of shortcut sub-lists.
+ */
+ public static List<List<A11yMenuShortcut>> generateShortcutSubLists(
+ int pageItemCount, List<A11yMenuShortcut> shortcutList) {
+ int start = 0;
+ int end;
+ int shortcutListSize = shortcutList.size();
+ List<List<A11yMenuShortcut>> subLists = new ArrayList<>();
+ while (start < shortcutListSize) {
+ end = Math.min(start + pageItemCount, shortcutListSize);
+ subLists.add(shortcutList.subList(start, end));
+ start = end;
+ }
+ return subLists;
+ }
+
+ private GridViewParams() {}
+ }
+
+ private final AccessibilityMenuService mService;
+
+ /**
+ * The pager widget, which handles animation and allows swiping horizontally to access previous
+ * and next gridView pages.
+ */
+ protected ViewPager mViewPager;
+
+ private ViewPagerAdapter<GridView> mViewPagerAdapter;
+ private final List<GridView> mGridPageList = new ArrayList<>();
+
+ /** The footer, which provides buttons to switch between pages */
+ protected A11yMenuFooter mA11yMenuFooter;
+
+ /** The shortcut list intended to show in grid pages of viewPager */
+ private List<A11yMenuShortcut> mA11yMenuShortcutList;
+
+ /** The container layout for a11y menu. */
+ private ViewGroup mA11yMenuLayout;
+
+ public A11yMenuViewPager(AccessibilityMenuService service) {
+ this.mService = service;
+ }
+
+ /**
+ * Configures UI for view pager and footer.
+ *
+ * @param a11yMenuLayout the container layout for a11y menu
+ * @param shortcutDataList the data list need to show in view pager
+ * @param pageIndex the index of ViewPager to show
+ */
+ public void configureViewPagerAndFooter(
+ ViewGroup a11yMenuLayout, List<A11yMenuShortcut> shortcutDataList, int pageIndex) {
+ this.mA11yMenuLayout = a11yMenuLayout;
+ mA11yMenuShortcutList = shortcutDataList;
+ initViewPager();
+ initChildPage();
+ mA11yMenuFooter = new A11yMenuFooter(a11yMenuLayout, mFooterCallbacks);
+ updateFooterState();
+ registerOnGlobalLayoutListener();
+ goToPage(pageIndex);
+ }
+
+ /** Initializes viewPager and its adapter. */
+ private void initViewPager() {
+ mViewPager = mA11yMenuLayout.findViewById(R.id.view_pager);
+ mViewPagerAdapter = new ViewPagerAdapter<>();
+ mViewPager.setAdapter(mViewPagerAdapter);
+ mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER);
+ mViewPager.addOnPageChangeListener(
+ new ViewPager.OnPageChangeListener() {
+ @Override
+ public void onPageScrollStateChanged(int state) {}
+
+ @Override
+ public void onPageScrolled(
+ int position, float positionOffset, int positionOffsetPixels) {}
+
+ @Override
+ public void onPageSelected(int position) {
+ updateFooterState();
+ }
+ });
+ }
+
+ /** Creates child pages of viewPager by the length of shortcuts and initializes them. */
+ private void initChildPage() {
+ if (mA11yMenuShortcutList == null || mA11yMenuShortcutList.isEmpty()) {
+ return;
+ }
+
+ if (!mGridPageList.isEmpty()) {
+ mGridPageList.clear();
+ }
+
+ // Generate pages by calculating # of items per grid.
+ for (List<A11yMenuShortcut> page : GridViewParams.generateShortcutSubLists(
+ GridViewParams.getGridItemCount(mService), mA11yMenuShortcutList)
+ ) {
+ addGridPage(page);
+ }
+
+ mViewPagerAdapter.set(mGridPageList);
+ }
+
+ private void addGridPage(List<A11yMenuShortcut> shortcutDataListInPage) {
+ LayoutInflater inflater = LayoutInflater.from(mService);
+ View view = inflater.inflate(R.layout.grid_view, null);
+ GridView gridView = view.findViewById(R.id.gridview);
+ A11yMenuAdapter adapter = new A11yMenuAdapter(mService, shortcutDataListInPage);
+ gridView.setNumColumns(GridViewParams.getGridColumnCount(mService));
+ gridView.setAdapter(adapter);
+ mGridPageList.add(gridView);
+ }
+
+ /** Updates footer's state by index of current page in view pager. */
+ private void updateFooterState() {
+ int currentPage = mViewPager.getCurrentItem();
+ int lastPage = mViewPager.getAdapter().getCount() - 1;
+ mA11yMenuFooter.getPreviousPageBtn().setEnabled(currentPage > 0);
+ mA11yMenuFooter.getNextPageBtn().setEnabled(currentPage < lastPage);
+ }
+
+ private void goToPage(int pageIndex) {
+ if (mViewPager == null) {
+ return;
+ }
+ if ((pageIndex >= 0) && (pageIndex < mViewPager.getAdapter().getCount())) {
+ mViewPager.setCurrentItem(pageIndex);
+ }
+ }
+
+ /** Registers OnGlobalLayoutListener to adjust menu UI by running callback at first time. */
+ private void registerOnGlobalLayoutListener() {
+ mA11yMenuLayout
+ .getViewTreeObserver()
+ .addOnGlobalLayoutListener(
+ new OnGlobalLayoutListener() {
+
+ boolean mIsFirstTime = true;
+
+ @Override
+ public void onGlobalLayout() {
+ if (!mIsFirstTime) {
+ return;
+ }
+
+ if (mGridPageList.isEmpty()) {
+ return;
+ }
+
+ GridView firstGridView = mGridPageList.get(0);
+ if (firstGridView == null
+ || firstGridView.getChildAt(0) == null) {
+ return;
+ }
+
+ mIsFirstTime = false;
+
+ int gridItemHeight = firstGridView.getChildAt(0)
+ .getMeasuredHeight();
+ adjustMenuUISize(gridItemHeight);
+ }
+ });
+ }
+
+ /**
+ * Adjusts menu UI to fit both landscape and portrait mode.
+ *
+ * <ol>
+ * <li>Adjust view pager's height.
+ * <li>Adjust vertical interval between grid items.
+ * <li>Adjust padding in view pager.
+ * </ol>
+ */
+ private void adjustMenuUISize(int gridItemHeight) {
+ final int rowsInGridView = GridViewParams.getGridRowCount(mService);
+ final int defaultMargin =
+ (int) mService.getResources().getDimension(R.dimen.a11ymenu_layout_margin);
+ final int topMargin = (int) mService.getResources().getDimension(R.dimen.table_margin_top);
+ final int displayMode = mService.getResources().getConfiguration().orientation;
+ int viewPagerHeight = mViewPager.getMeasuredHeight();
+
+ if (displayMode == Configuration.ORIENTATION_PORTRAIT) {
+ // In portrait mode, we only need to adjust view pager's height to match its
+ // child's height.
+ viewPagerHeight = gridItemHeight * rowsInGridView + defaultMargin + topMargin;
+ } else if (displayMode == Configuration.ORIENTATION_LANDSCAPE) {
+ // In landscape mode, we need to adjust view pager's height to match screen height
+ // and adjust its child too,
+ // because a11y menu layout height is limited by the screen height.
+ DisplayMetrics displayMetrics = mService.getResources().getDisplayMetrics();
+ float densityScale = (float) displayMetrics.densityDpi
+ / DisplayMetrics.DENSITY_DEVICE_STABLE;
+ View footerLayout = mA11yMenuLayout.findViewById(R.id.footerlayout);
+ // Keeps footer window height unchanged no matter the density is changed.
+ footerLayout.getLayoutParams().height =
+ (int) (footerLayout.getLayoutParams().height / densityScale);
+ // Adjust the view pager height for system bar and display cutout insets.
+ WindowManager windowManager = mService.getSystemService(WindowManager.class);
+ WindowMetrics windowMetric = windowManager.getCurrentWindowMetrics();
+ Insets windowInsets = windowMetric.getWindowInsets().getInsetsIgnoringVisibility(
+ WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
+ viewPagerHeight =
+ windowMetric.getBounds().height()
+ - footerLayout.getLayoutParams().height
+ - windowInsets.bottom;
+ // Sets vertical interval between grid items.
+ int interval =
+ (viewPagerHeight - topMargin - defaultMargin
+ - (rowsInGridView * gridItemHeight))
+ / (rowsInGridView + 1);
+ for (GridView gridView : mGridPageList) {
+ gridView.setVerticalSpacing(interval);
+ }
+
+ // Sets padding to view pager.
+ final int finalMarginTop = interval + topMargin;
+ mViewPager.setPadding(defaultMargin, finalMarginTop, defaultMargin, defaultMargin);
+ }
+ final ViewGroup.LayoutParams layoutParams = mViewPager.getLayoutParams();
+ layoutParams.height = viewPagerHeight;
+ mViewPager.setLayoutParams(layoutParams);
+ }
+
+ /** Callback object to handle click events from A11yMenuFooter */
+ protected A11yMenuFooterCallBack mFooterCallbacks =
+ new A11yMenuFooterCallBack() {
+ @Override
+ public void onLeftButtonClicked() {
+ // Moves to previous page.
+ int targetPage = mViewPager.getCurrentItem() - 1;
+ goToPage(targetPage);
+ updateFooterState();
+ }
+
+ @Override
+ public void onRightButtonClicked() {
+ // Moves to next page.
+ int targetPage = mViewPager.getCurrentItem() + 1;
+ goToPage(targetPage);
+ updateFooterState();
+ }
+ };
+}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/ViewPagerAdapter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/ViewPagerAdapter.java
new file mode 100644
index 0000000..5670d72
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/ViewPagerAdapter.java
@@ -0,0 +1,70 @@
+/*
+ * 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.accessibility.accessibilitymenu.view;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.viewpager.widget.PagerAdapter;
+
+import java.util.List;
+
+/** The pager adapter, which provides the pages to the view pager widget. */
+class ViewPagerAdapter<T extends View> extends PagerAdapter {
+
+ /** The widget list in each page of view pager. */
+ private List<T> mWidgetList;
+
+ ViewPagerAdapter() {}
+
+ public void set(List<T> tList) {
+ mWidgetList = tList;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ if (mWidgetList == null) {
+ return 0;
+ }
+ return mWidgetList.size();
+ }
+
+ @Override
+ public int getItemPosition(Object object) {
+ return POSITION_NONE;
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return view == object;
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ if (mWidgetList == null) {
+ return null;
+ }
+ container.addView(mWidgetList.get(position));
+ return mWidgetList.get(position);
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ container.removeView((View) object);
+ }
+}
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index af6e646..6d5eb6a 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1302,6 +1302,9 @@
<!-- LOCKSCREEN -> DREAMING transition: Amount to shift lockscreen content on entering -->
<dimen name="lockscreen_to_dreaming_transition_lockscreen_translation_y">-40dp</dimen>
+ <!-- GONE -> DREAMING transition: Amount to shift lockscreen content on entering -->
+ <dimen name="gone_to_dreaming_transition_lockscreen_translation_y">-40dp</dimen>
+
<!-- LOCKSCREEN -> OCCLUDED transition: Amount to shift lockscreen content on entering -->
<dimen name="lockscreen_to_occluded_transition_lockscreen_translation_y">-40dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e4f339a..2745202 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2463,10 +2463,10 @@
<!-- Text to ask the user to move their device closer to a different device (deviceName) in order to play media on the different device. [CHAR LIMIT=75] -->
<string name="media_move_closer_to_start_cast">Move closer to play on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
<!-- Text to ask the user to move their device closer to a different device (deviceName) in order to transfer media from the different device and back onto the current device. [CHAR LIMIT=75] -->
- <string name="media_move_closer_to_end_cast">Move closer to <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g> to play here</string>
+ <string name="media_move_closer_to_end_cast">To play here, move closer to <xliff:g id="deviceName" example="tablet">%1$s</xliff:g></string>
<!-- Text informing the user that their media is now playing on a different device (deviceName). [CHAR LIMIT=50] -->
<string name="media_transfer_playing_different_device">Playing on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
- <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIsMIT=50] -->
+ <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIMIT=50] -->
<string name="media_transfer_failed">Something went wrong. Try again.</string>
<!-- Text to indicate that a media transfer is currently in-progress, aka loading. [CHAR LIMIT=NONE] -->
<string name="media_transfer_loading">Loading</string>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index a71fb56..fa484c7 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -37,7 +37,7 @@
/**
* Sent when overview is to be shown.
*/
- void onOverviewShown(boolean triggeredFromAltTab) = 7;
+ void onOverviewShown(boolean triggeredFromAltTab, boolean forward) = 7;
/**
* Sent when overview is to be hidden.
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
index eed5531..9b2a224 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
@@ -51,13 +51,22 @@
fun bindAndLoadSuggested(component: ComponentName, callback: LoadCallback)
/**
- * Request to bind to the given service.
+ * Request to bind to the given service. This should only be used for services using the full
+ * [ControlsProviderService] API, where SystemUI renders the devices' UI.
*
* @param component The [ComponentName] of the service to bind
*/
fun bindService(component: ComponentName)
/**
+ * Bind to a service that provides a Device Controls panel (embedded activity). This will allow
+ * the app to remain "warm", and reduce latency.
+ *
+ * @param component The [ComponentName] of the [ControlsProviderService] to bind.
+ */
+ fun bindServiceForPanel(component: ComponentName)
+
+ /**
* Send a subscribe message to retrieve status of a set of controls.
*
* @param structureInfo structure containing the controls to update
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
index 2f0fd99..3d6d335 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
@@ -170,6 +170,10 @@
retrieveLifecycleManager(component).bindService()
}
+ override fun bindServiceForPanel(component: ComponentName) {
+ retrieveLifecycleManager(component).bindServiceForPanel()
+ }
+
override fun changeUser(newUser: UserHandle) {
if (newUser == currentUser) return
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
index 2f49c3f..f29f6d0 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
@@ -189,6 +189,14 @@
fun getPreferredSelection(): SelectedItem
/**
+ * Bind to a service that provides a Device Controls panel (embedded activity). This will allow
+ * the app to remain "warm", and reduce latency.
+ *
+ * @param component The [ComponentName] of the [ControlsProviderService] to bind.
+ */
+ fun bindComponentForPanel(componentName: ComponentName)
+
+ /**
* Interface for structure to pass data to [ControlsFavoritingActivity].
*/
interface LoadData {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index 7b1c623..49771dd 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -479,6 +479,10 @@
bindingController.unsubscribe()
}
+ override fun bindComponentForPanel(componentName: ComponentName) {
+ bindingController.bindServiceForPanel(componentName)
+ }
+
override fun addFavorite(
componentName: ComponentName,
structureName: CharSequence,
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
index 5b38e5b..72c3a94 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
@@ -78,6 +78,10 @@
private const val DEBUG = true
private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or
Context.BIND_NOT_PERCEPTIBLE
+ // Use BIND_NOT_PERCEPTIBLE so it will be at lower priority from SystemUI.
+ // However, don't use WAIVE_PRIORITY, as by itself, it will kill the app
+ // once the Task is finished in the device controls panel.
+ private val BIND_FLAGS_PANEL = Context.BIND_AUTO_CREATE or Context.BIND_NOT_PERCEPTIBLE
}
private val intent = Intent().apply {
@@ -87,18 +91,19 @@
})
}
- private fun bindService(bind: Boolean) {
+ private fun bindService(bind: Boolean, forPanel: Boolean = false) {
executor.execute {
requiresBound = bind
if (bind) {
- if (bindTryCount != MAX_BIND_RETRIES) {
+ if (bindTryCount != MAX_BIND_RETRIES && wrapper == null) {
if (DEBUG) {
Log.d(TAG, "Binding service $intent")
}
bindTryCount++
try {
+ val flags = if (forPanel) BIND_FLAGS_PANEL else BIND_FLAGS
val bound = context
- .bindServiceAsUser(intent, serviceConnection, BIND_FLAGS, user)
+ .bindServiceAsUser(intent, serviceConnection, flags, user)
if (!bound) {
context.unbindService(serviceConnection)
}
@@ -279,6 +284,10 @@
bindService(true)
}
+ fun bindServiceForPanel() {
+ bindService(bind = true, forPanel = true)
+ }
+
/**
* Request unbind from the service.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 1e3e5cd..6289788 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -232,6 +232,8 @@
ControlKey(selected.structure.componentName, it.ci.controlId)
}
controlsController.get().subscribeToFavorites(selected.structure)
+ } else {
+ controlsController.get().bindComponentForPanel(selected.componentName)
}
listingCallback = createCallback(::showControlsView)
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index d040f8f..c880c59 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -200,7 +200,7 @@
/** A different path for unocclusion transitions back to keyguard */
// TODO(b/262859270): Tracking Bug
@JvmField
- val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = false)
+ val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = true)
// flag for controlling auto pin confirmation and material u shapes in bouncer
@JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index d14b66a..0c4bca6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -209,7 +209,7 @@
return
}
- if (state == TransitionState.FINISHED) {
+ if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) {
updateTransitionId = null
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 3b09ae7..7134ec0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -21,7 +21,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -56,7 +56,7 @@
scope.launch {
// Using isDreamingWithOverlay provides an optimized path to LOCKSCREEN state, which
// otherwise would have gone through OCCLUDED first
- keyguardInteractor.isDreamingWithOverlay
+ keyguardInteractor.isAbleToDream
.sample(
combine(
keyguardInteractor.dozeTransitionModel,
@@ -65,8 +65,7 @@
),
::toTriple
)
- .collect { triple ->
- val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple
+ .collect { (isDreaming, dozeTransitionModel, lastStartedTransition) ->
if (
!isDreaming &&
isDozeOff(dozeTransitionModel.to) &&
@@ -96,8 +95,7 @@
),
::toTriple
)
- .collect { triple ->
- val (isDreaming, isOccluded, lastStartedTransition) = triple
+ .collect { (isDreaming, isOccluded, lastStartedTransition) ->
if (
isOccluded &&
!isDreaming &&
@@ -123,24 +121,18 @@
private fun listenForDreamingToGone() {
scope.launch {
- keyguardInteractor.biometricUnlockState
- .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
- .collect { pair ->
- val (biometricUnlockState, keyguardState) = pair
- if (
- keyguardState == KeyguardState.DREAMING &&
- isWakeAndUnlock(biometricUnlockState)
- ) {
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- name,
- KeyguardState.DREAMING,
- KeyguardState.GONE,
- getAnimator(),
- )
+ keyguardInteractor.biometricUnlockState.collect { biometricUnlockState ->
+ if (biometricUnlockState == BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.DREAMING,
+ KeyguardState.GONE,
+ getAnimator(),
)
- }
+ )
}
+ }
}
}
@@ -151,8 +143,7 @@
keyguardTransitionInteractor.finishedKeyguardState,
::Pair
)
- .collect { pair ->
- val (dozeTransitionModel, keyguardState) = pair
+ .collect { (dozeTransitionModel, keyguardState) ->
if (
dozeTransitionModel.to == DozeStateModel.DOZE &&
keyguardState == KeyguardState.DREAMING
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 64028ce..5674e2a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -48,8 +48,6 @@
private val keyguardTransitionRepository: KeyguardTransitionRepository,
) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) {
- private var transitionId: UUID? = null
-
override fun start() {
listenForLockscreenToGone()
listenForLockscreenToOccluded()
@@ -104,6 +102,7 @@
/* Starts transitions when manually dragging up the bouncer from the lockscreen. */
private fun listenForLockscreenToBouncerDragging() {
+ var transitionId: UUID? = null
scope.launch {
shadeRepository.shadeModel
.sample(
@@ -114,25 +113,43 @@
),
::toTriple
)
- .collect { triple ->
- val (shadeModel, keyguardState, statusBarState) = triple
-
+ .collect { (shadeModel, keyguardState, statusBarState) ->
val id = transitionId
if (id != null) {
// An existing `id` means a transition is started, and calls to
- // `updateTransition` will control it until FINISHED
- keyguardTransitionRepository.updateTransition(
- id,
- 1f - shadeModel.expansionAmount,
- if (
- shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
- ) {
- transitionId = null
+ // `updateTransition` will control it until FINISHED or CANCELED
+ var nextState =
+ if (shadeModel.expansionAmount == 0f) {
TransitionState.FINISHED
+ } else if (shadeModel.expansionAmount == 1f) {
+ TransitionState.CANCELED
} else {
TransitionState.RUNNING
}
+ keyguardTransitionRepository.updateTransition(
+ id,
+ 1f - shadeModel.expansionAmount,
+ nextState,
)
+
+ if (
+ nextState == TransitionState.CANCELED ||
+ nextState == TransitionState.FINISHED
+ ) {
+ transitionId = null
+ }
+
+ // If canceled, just put the state back
+ if (nextState == TransitionState.CANCELED) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ ownerName = name,
+ from = KeyguardState.BOUNCER,
+ to = KeyguardState.LOCKSCREEN,
+ animator = getAnimator(0.milliseconds)
+ )
+ )
+ }
} else {
// TODO (b/251849525): Remove statusbarstate check when that state is
// integrated into KeyguardTransitionRepository
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 490d22e..4cf56fe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -32,12 +32,15 @@
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.CommandQueue.Callbacks
-import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.merge
/**
@@ -89,15 +92,23 @@
/**
* Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means
* that doze mode is not running and DREAMING is ok to commence.
+ *
+ * Allow a brief moment to prevent rapidly oscillating between true/false signals.
*/
val isAbleToDream: Flow<Boolean> =
merge(isDreaming, isDreamingWithOverlay)
- .sample(
+ .combine(
dozeTransitionModel,
{ isDreaming, dozeTransitionModel ->
isDreaming && isDozeOff(dozeTransitionModel.to)
}
)
+ .flatMapLatest { isAbleToDream ->
+ flow {
+ delay(50)
+ emit(isAbleToDream)
+ }
+ }
.distinctUntilChanged()
/** Whether the keyguard is showing or not. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 9cdbcda..ad6dbea 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -22,13 +22,17 @@
import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER
import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.shared.model.TransitionStep
import javax.inject.Inject
+import kotlin.math.max
+import kotlin.math.min
import kotlin.time.Duration
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
@@ -53,9 +57,16 @@
val dreamingToLockscreenTransition: Flow<TransitionStep> =
repository.transition(DREAMING, LOCKSCREEN)
+ /** GONE->DREAMING transition information. */
+ val goneToDreamingTransition: Flow<TransitionStep> = repository.transition(GONE, DREAMING)
+
/** LOCKSCREEN->AOD transition information. */
val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
+ /** LOCKSCREEN->BOUNCER transition information. */
+ val lockscreenToBouncerTransition: Flow<TransitionStep> =
+ repository.transition(LOCKSCREEN, BOUNCER)
+
/** LOCKSCREEN->DREAMING transition information. */
val lockscreenToDreamingTransition: Flow<TransitionStep> =
repository.transition(LOCKSCREEN, DREAMING)
@@ -106,13 +117,23 @@
): Flow<Float> {
val start = (params.startTime / totalDuration).toFloat()
val chunks = (totalDuration / params.duration).toFloat()
+ var isRunning = false
return flow
- // When starting, emit a value of 0f to give animations a chance to set initial state
.map { step ->
+ val value = (step.value - start) * chunks
if (step.transitionState == STARTED) {
- 0f
+ // When starting, make sure to always emit. If a transition is started from the
+ // middle, it is possible this animation is being skipped but we need to inform
+ // the ViewModels of the last update
+ isRunning = true
+ max(0f, min(1f, value))
+ } else if (isRunning && value >= 1f) {
+ // Always send a final value of 1. Because of rounding, [value] may never be
+ // exactly 1.
+ isRunning = false
+ 1f
} else {
- (step.value - start) * chunks
+ value
}
}
.filter { value -> value >= 0f && value <= 1f }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index e164f5d..6627865 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -22,10 +22,14 @@
import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
/**
* Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -49,9 +53,15 @@
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
- }
+ return merge(
+ flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
+ -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
+ },
+ // On end, reset the translation to 0
+ interactor.dreamingToLockscreenTransition
+ .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
+ .map { 0f }
+ )
}
/** Lockscreen views alpha */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
new file mode 100644
index 0000000..5a47960
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+
+/** Breaks down GONE->DREAMING transition into discrete steps for corresponding views to consume. */
+@SysUISingleton
+class GoneToDreamingTransitionViewModel
+@Inject
+constructor(
+ private val interactor: KeyguardTransitionInteractor,
+) {
+
+ /** Lockscreen views y-translation */
+ fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+ return merge(
+ flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
+ (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx)
+ },
+ // On end, reset the translation to 0
+ interactor.goneToDreamingTransition
+ .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
+ .map { 0f }
+ )
+ }
+
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it }
+
+ private fun flowForAnimation(params: AnimationParams): Flow<Float> {
+ return interactor.transitionStepAnimation(
+ interactor.goneToDreamingTransition,
+ params,
+ totalDuration = TO_DREAMING_DURATION
+ )
+ }
+
+ companion object {
+ val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds)
+ val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
index d48f87d..e05adbd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
@@ -21,7 +21,8 @@
import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
@@ -48,7 +49,7 @@
},
// On end, reset the translation to 0
interactor.lockscreenToDreamingTransition
- .filter { step -> step.transitionState == TransitionState.FINISHED }
+ .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
.map { 0f }
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
index 899148b..8f1c904 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
@@ -130,7 +130,12 @@
private var splitShadeContainer: ViewGroup? = null
/** Track the media player setting status on lock screen. */
- private var allowMediaPlayerOnLockScreen: Boolean = true
+ private var allowMediaPlayerOnLockScreen: Boolean =
+ secureSettings.getBoolForUser(
+ Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+ true,
+ UserHandle.USER_CURRENT
+ )
private val lockScreenMediaPlayerUri =
secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 8356440..08d1857 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -104,4 +104,9 @@
PackageManager.DONT_KILL_APP,
)
}
+
+ companion object {
+ // TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead.
+ const val NOTE_TASK_KEY_EVENT = 311
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index d14b7a7..d5f4a5a 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -16,7 +16,6 @@
package com.android.systemui.notetask
-import android.view.KeyEvent
import androidx.annotation.VisibleForTesting
import com.android.systemui.statusbar.CommandQueue
import com.android.wm.shell.bubbles.Bubbles
@@ -37,7 +36,7 @@
val callbacks =
object : CommandQueue.Callbacks {
override fun handleSystemKey(keyCode: Int) {
- if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) {
+ if (keyCode == NoteTaskController.NOTE_TASK_KEY_EVENT) {
noteTaskController.showNoteTask()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
index 98d6991..26e3f49 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
@@ -21,12 +21,12 @@
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ResolveInfoFlags
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
import javax.inject.Inject
/**
- * Class responsible to query all apps and find one that can handle the [NOTES_ACTION]. If found, an
- * [Intent] ready for be launched will be returned. Otherwise, returns null.
+ * Class responsible to query all apps and find one that can handle the [ACTION_CREATE_NOTE]. If
+ * found, an [Intent] ready for be launched will be returned. Otherwise, returns null.
*
* TODO(b/248274123): should be revisited once the notes role is implemented.
*/
@@ -37,15 +37,16 @@
) {
fun resolveIntent(): Intent? {
- val intent = Intent(NOTES_ACTION)
+ val intent = Intent(ACTION_CREATE_NOTE)
val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
val infoList = packageManager.queryIntentActivities(intent, flags)
for (info in infoList) {
- val packageName = info.serviceInfo.applicationInfo.packageName ?: continue
+ val packageName = info.activityInfo.applicationInfo.packageName ?: continue
val activityName = resolveActivityNameForNotesAction(packageName) ?: continue
- return Intent(NOTES_ACTION)
+ return Intent(ACTION_CREATE_NOTE)
+ .setPackage(packageName)
.setComponent(ComponentName(packageName, activityName))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
@@ -54,7 +55,7 @@
}
private fun resolveActivityNameForNotesAction(packageName: String): String? {
- val intent = Intent(NOTES_ACTION).setPackage(packageName)
+ val intent = Intent(ACTION_CREATE_NOTE).setPackage(packageName)
val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
val resolveInfo = packageManager.resolveActivity(intent, flags)
@@ -69,8 +70,8 @@
}
companion object {
- // TODO(b/254606432): Use Intent.ACTION_NOTES and Intent.ACTION_NOTES_LOCKED instead.
- const val NOTES_ACTION = "android.intent.action.NOTES"
+ // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead.
+ const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
index 47fe676..f203e7a 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
@@ -45,8 +45,8 @@
fun newIntent(context: Context): Intent {
return Intent(context, LaunchNoteTaskActivity::class.java).apply {
// Intent's action must be set in shortcuts, or an exception will be thrown.
- // TODO(b/254606432): Use Intent.ACTION_NOTES instead.
- action = NoteTaskIntentResolver.NOTES_ACTION
+ // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead.
+ action = NoteTaskIntentResolver.ACTION_CREATE_NOTE
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index 30f8124..1921586 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -219,9 +219,9 @@
// Small button with the number only.
foregroundServicesWithTextView.isVisible = false
- foregroundServicesWithNumberView.visibility = View.VISIBLE
+ foregroundServicesWithNumberView.isVisible = true
foregroundServicesWithNumberView.setOnClickListener {
- foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView))
+ foregroundServices.onClick(Expandable.fromView(foregroundServicesWithNumberView))
}
foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString()
foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java
index 5ea1c0b..c335a6d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java
@@ -59,11 +59,11 @@
}
@Override
- public void showRecentApps(boolean triggeredFromAltTab) {
+ public void showRecentApps(boolean triggeredFromAltTab, boolean forward) {
IOverviewProxy overviewProxy = mOverviewProxyService.getProxy();
if (overviewProxy != null) {
try {
- overviewProxy.onOverviewShown(triggeredFromAltTab);
+ overviewProxy.onOverviewShown(triggeredFromAltTab, forward);
} catch (RemoteException e) {
Log.e(TAG, "Failed to send overview show event to launcher.", e);
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
index b041f95..95d6c18 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
@@ -65,14 +65,14 @@
}
@Override
- public void showRecentApps(boolean triggeredFromAltTab) {
+ public void showRecentApps(boolean triggeredFromAltTab, boolean forward) {
// Ensure the device has been provisioned before allowing the user to interact with
// recents
if (!isUserSetup()) {
return;
}
- mImpl.showRecentApps(triggeredFromAltTab);
+ mImpl.showRecentApps(triggeredFromAltTab, forward);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImplementation.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImplementation.java
index 8848dbb..010ceda 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImplementation.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImplementation.java
@@ -31,7 +31,7 @@
default void preloadRecentApps() {}
default void cancelPreloadRecentApps() {}
- default void showRecentApps(boolean triggeredFromAltTab) {}
+ default void showRecentApps(boolean triggeredFromAltTab, boolean forward) {}
default void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {}
default void toggleRecentApps() {}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 93e8151..964d0b2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -144,6 +144,7 @@
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
@@ -692,6 +693,7 @@
private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
private LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel;
+ private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel;
private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@@ -700,6 +702,7 @@
private int mDreamingToLockscreenTransitionTranslationY;
private int mOccludedToLockscreenTransitionTranslationY;
private int mLockscreenToDreamingTransitionTranslationY;
+ private int mGoneToDreamingTransitionTranslationY;
private int mLockscreenToOccludedTransitionTranslationY;
private boolean mUnocclusionTransitionFlagEnabled = false;
@@ -735,6 +738,12 @@
step.getTransitionState() == TransitionState.RUNNING;
};
+ private final Consumer<TransitionStep> mGoneToDreamingTransition =
+ (TransitionStep step) -> {
+ mIsOcclusionTransitionRunning =
+ step.getTransitionState() == TransitionState.RUNNING;
+ };
+
private final Consumer<TransitionStep> mLockscreenToOccludedTransition =
(TransitionStep step) -> {
mIsOcclusionTransitionRunning =
@@ -813,6 +822,7 @@
DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel,
OccludedToLockscreenTransitionViewModel occludedToLockscreenTransitionViewModel,
LockscreenToDreamingTransitionViewModel lockscreenToDreamingTransitionViewModel,
+ GoneToDreamingTransitionViewModel goneToDreamingTransitionViewModel,
LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel,
@Main CoroutineDispatcher mainDispatcher,
KeyguardTransitionInteractor keyguardTransitionInteractor,
@@ -834,6 +844,7 @@
mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel;
mOccludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel;
mLockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel;
+ mGoneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel;
mLockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel;
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@@ -1172,6 +1183,17 @@
setTransitionY(mNotificationStackScrollLayoutController),
mMainDispatcher);
+ // Gone->Dreaming
+ collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(),
+ mGoneToDreamingTransition, mMainDispatcher);
+ collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(),
+ setTransitionAlpha(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ collectFlow(mView, mGoneToDreamingTransitionViewModel.lockscreenTranslationY(
+ mGoneToDreamingTransitionTranslationY),
+ setTransitionY(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+
// Lockscreen->Occluded
collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToOccludedTransition(),
mLockscreenToOccludedTransition, mMainDispatcher);
@@ -1223,6 +1245,8 @@
R.dimen.occluded_to_lockscreen_transition_lockscreen_translation_y);
mLockscreenToDreamingTransitionTranslationY = mResources.getDimensionPixelSize(
R.dimen.lockscreen_to_dreaming_transition_lockscreen_translation_y);
+ mGoneToDreamingTransitionTranslationY = mResources.getDimensionPixelSize(
+ R.dimen.gone_to_dreaming_transition_lockscreen_translation_y);
mLockscreenToOccludedTransitionTranslationY = mResources.getDimensionPixelSize(
R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y);
}
@@ -2473,7 +2497,7 @@
mInitialTouchY = event.getY();
mInitialTouchX = event.getX();
}
- if (!isFullyCollapsed()) {
+ if (!isFullyCollapsed() && !isShadeOrQsHeightAnimationRunning()) {
handleQsDown(event);
}
// defer touches on QQS to shade while shade is collapsing. Added margin for error
@@ -5263,6 +5287,11 @@
}
}
+ /** Returns whether a shade or QS expansion animation is running */
+ private boolean isShadeOrQsHeightAnimationRunning() {
+ return mHeightAnimator != null && !mHintAnimationRunning && !mIsSpringBackAnimation;
+ }
+
/**
* Phase 2: Bounce down.
*/
@@ -6280,8 +6309,7 @@
mCollapsedAndHeadsUpOnDown =
isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp();
addMovement(event);
- boolean regularHeightAnimationRunning = mHeightAnimator != null
- && !mHintAnimationRunning && !mIsSpringBackAnimation;
+ boolean regularHeightAnimationRunning = isShadeOrQsHeightAnimationRunning();
if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) {
mTouchSlopExceeded = regularHeightAnimationRunning
|| mTouchSlopExceededBeforeDown;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 8314ec7..26f8b62 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -321,9 +321,12 @@
&& !state.mKeyguardFadingAway && !state.mKeyguardGoingAway;
if (onKeyguard
&& mAuthController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser())) {
+ // both max and min display refresh rate must be set to take effect:
mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardPreferredRefreshRate;
+ mLpChanged.preferredMinDisplayRefreshRate = mKeyguardPreferredRefreshRate;
} else {
mLpChanged.preferredMaxDisplayRefreshRate = 0;
+ mLpChanged.preferredMinDisplayRefreshRate = 0;
}
Trace.setCounter("display_set_preferred_refresh_rate",
(long) mKeyguardPreferredRefreshRate);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index bad942f..04adaae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -224,7 +224,7 @@
*/
default void setImeWindowStatus(int displayId, IBinder token, int vis,
@BackDispositionMode int backDisposition, boolean showImeSwitcher) { }
- default void showRecentApps(boolean triggeredFromAltTab) { }
+ default void showRecentApps(boolean triggeredFromAltTab, boolean forward) { }
default void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { }
default void toggleRecentApps() { }
default void toggleSplitScreen() { }
@@ -686,11 +686,11 @@
}
}
- public void showRecentApps(boolean triggeredFromAltTab) {
+ public void showRecentApps(boolean triggeredFromAltTab, boolean forward) {
synchronized (mLock) {
mHandler.removeMessages(MSG_SHOW_RECENT_APPS);
- mHandler.obtainMessage(MSG_SHOW_RECENT_APPS, triggeredFromAltTab ? 1 : 0, 0,
- null).sendToTarget();
+ mHandler.obtainMessage(MSG_SHOW_RECENT_APPS, triggeredFromAltTab ? 1 : 0,
+ forward ? 1 : 0, null).sendToTarget();
}
}
@@ -1384,7 +1384,7 @@
break;
case MSG_SHOW_RECENT_APPS:
for (int i = 0; i < mCallbacks.size(); i++) {
- mCallbacks.get(i).showRecentApps(msg.arg1 != 0);
+ mCallbacks.get(i).showRecentApps(msg.arg1 != 0, msg.arg2 != 0);
}
break;
case MSG_HIDE_RECENT_APPS:
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
index 1e7fc93..197cf56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
@@ -54,7 +54,7 @@
* their respective views based on the progress of the animator. Interpolation differences TBD
*/
@SysUISingleton
-class SystemStatusAnimationScheduler @Inject constructor(
+open class SystemStatusAnimationScheduler @Inject constructor(
private val coordinator: SystemEventCoordinator,
private val chipAnimationController: SystemEventChipAnimationController,
private val statusBarWindowController: StatusBarWindowController,
@@ -66,7 +66,7 @@
companion object {
private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator"
}
- private fun isImmersiveIndicatorEnabled(): Boolean {
+ public fun isImmersiveIndicatorEnabled(): Boolean {
return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
PROPERTY_ENABLE_IMMERSIVE_INDICATOR, true)
}
@@ -76,18 +76,22 @@
/** True if the persistent privacy dot should be active */
var hasPersistentDot = false
- private set
+ protected set
private var scheduledEvent: StatusEvent? = null
private var cancelExecutionRunnable: Runnable? = null
private val listeners = mutableSetOf<SystemStatusAnimationCallback>()
+ fun getListeners(): MutableSet<SystemStatusAnimationCallback> {
+ return listeners
+ }
+
init {
coordinator.attachScheduler(this)
dumpManager.registerDumpable(TAG, this)
}
- fun onStatusEvent(event: StatusEvent) {
+ open fun onStatusEvent(event: StatusEvent) {
// Ignore any updates until the system is up and running
if (isTooEarly() || !isImmersiveIndicatorEnabled()) {
return
@@ -139,7 +143,7 @@
}
}
- private fun isTooEarly(): Boolean {
+ public fun isTooEarly(): Boolean {
return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
index 5960387..5562e73 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
@@ -17,6 +17,8 @@
package com.android.systemui.statusbar.pipeline.mobile.data.model
import android.telephony.Annotation.NetworkType
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
/**
@@ -38,4 +40,12 @@
data class OverrideNetworkType(
override val lookupKey: String,
) : ResolvedNetworkType
+
+ /** Represents the carrier merged network. See [CarrierMergedConnectionRepository]. */
+ object CarrierMergedNetworkType : ResolvedNetworkType {
+ // Effectively unused since [iconGroupOverride] is used instead.
+ override val lookupKey: String = "cwf"
+
+ val iconGroupOverride: SignalIcon.MobileIconGroup = TelephonyIcons.CARRIER_MERGED_WIFI
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index d04996b..6187f64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -22,7 +22,6 @@
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/**
@@ -50,7 +49,7 @@
* A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single
* listener + model.
*/
- val connectionInfo: Flow<MobileConnectionModel>
+ val connectionInfo: StateFlow<MobileConnectionModel>
/** The total number of levels. Used with [SignalDrawable]. */
val numberOfLevels: StateFlow<Int>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
index 8ac1237..22aca0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -39,7 +39,11 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.CarrierMergedConnectionRepository.Companion.createCarrierMergedConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.MOBILE_CONNECTION_BUFFER_SIZE
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -60,15 +64,19 @@
class DemoMobileConnectionsRepository
@Inject
constructor(
- private val dataSource: DemoModeMobileConnectionDataSource,
+ private val mobileDataSource: DemoModeMobileConnectionDataSource,
+ private val wifiDataSource: DemoModeWifiDataSource,
@Application private val scope: CoroutineScope,
context: Context,
private val logFactory: TableLogBufferFactory,
) : MobileConnectionsRepository {
- private var demoCommandJob: Job? = null
+ private var mobileDemoCommandJob: Job? = null
+ private var wifiDemoCommandJob: Job? = null
- private var connectionRepoCache = mutableMapOf<Int, DemoMobileConnectionRepository>()
+ private var carrierMergedSubId: Int? = null
+
+ private var connectionRepoCache = mutableMapOf<Int, CacheContainer>()
private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionModel>()
val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
@@ -144,52 +152,83 @@
override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel())
override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository {
- return connectionRepoCache[subId]
- ?: createDemoMobileConnectionRepo(subId).also { connectionRepoCache[subId] = it }
+ val current = connectionRepoCache[subId]?.repo
+ if (current != null) {
+ return current
+ }
+
+ val new = createDemoMobileConnectionRepo(subId)
+ connectionRepoCache[subId] = new
+ return new.repo
}
- private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository {
- val tableLogBuffer = logFactory.getOrCreate("DemoMobileConnectionLog [$subId]", 100)
+ private fun createDemoMobileConnectionRepo(subId: Int): CacheContainer {
+ val tableLogBuffer =
+ logFactory.getOrCreate(
+ "DemoMobileConnectionLog [$subId]",
+ MOBILE_CONNECTION_BUFFER_SIZE,
+ )
- return DemoMobileConnectionRepository(
- subId,
- tableLogBuffer,
- )
+ val repo =
+ DemoMobileConnectionRepository(
+ subId,
+ tableLogBuffer,
+ )
+ return CacheContainer(repo, lastMobileState = null)
}
override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
fun startProcessingCommands() {
- demoCommandJob =
+ mobileDemoCommandJob =
scope.launch {
- dataSource.mobileEvents.filterNotNull().collect { event -> processEvent(event) }
+ mobileDataSource.mobileEvents.filterNotNull().collect { event ->
+ processMobileEvent(event)
+ }
+ }
+ wifiDemoCommandJob =
+ scope.launch {
+ wifiDataSource.wifiEvents.filterNotNull().collect { event ->
+ processWifiEvent(event)
+ }
}
}
fun stopProcessingCommands() {
- demoCommandJob?.cancel()
+ mobileDemoCommandJob?.cancel()
+ wifiDemoCommandJob?.cancel()
_subscriptions.value = listOf()
connectionRepoCache.clear()
subscriptionInfoCache.clear()
}
- private fun processEvent(event: FakeNetworkEventModel) {
+ private fun processMobileEvent(event: FakeNetworkEventModel) {
when (event) {
is Mobile -> {
processEnabledMobileState(event)
}
is MobileDisabled -> {
- processDisabledMobileState(event)
+ maybeRemoveSubscription(event.subId)
}
}
}
+ private fun processWifiEvent(event: FakeWifiEventModel) {
+ when (event) {
+ is FakeWifiEventModel.WifiDisabled -> disableCarrierMerged()
+ is FakeWifiEventModel.Wifi -> disableCarrierMerged()
+ is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event)
+ }
+ }
+
private fun processEnabledMobileState(state: Mobile) {
// get or create the connection repo, and set its values
val subId = state.subId ?: DEFAULT_SUB_ID
maybeCreateSubscription(subId)
val connection = getRepoForSubId(subId)
+ connectionRepoCache[subId]?.lastMobileState = state
+
// This is always true here, because we split out disabled states at the data-source level
connection.dataEnabled.value = true
connection.networkName.value = NetworkNameModel.Derived(state.name)
@@ -198,14 +237,36 @@
connection.connectionInfo.value = state.toMobileConnectionModel()
}
- private fun processDisabledMobileState(state: MobileDisabled) {
+ private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) {
+ // The new carrier merged connection is for a different sub ID, so disable carrier merged
+ // for the current (now old) sub
+ if (carrierMergedSubId != event.subscriptionId) {
+ disableCarrierMerged()
+ }
+
+ // get or create the connection repo, and set its values
+ val subId = event.subscriptionId
+ maybeCreateSubscription(subId)
+ carrierMergedSubId = subId
+
+ val connection = getRepoForSubId(subId)
+ // This is always true here, because we split out disabled states at the data-source level
+ connection.dataEnabled.value = true
+ connection.networkName.value = NetworkNameModel.Derived(CARRIER_MERGED_NAME)
+ connection.numberOfLevels.value = event.numberOfLevels
+ connection.cdmaRoaming.value = false
+ connection.connectionInfo.value = event.toMobileConnectionModel()
+ Log.e("CCS", "output connection info = ${connection.connectionInfo.value}")
+ }
+
+ private fun maybeRemoveSubscription(subId: Int?) {
if (_subscriptions.value.isEmpty()) {
// Nothing to do here
return
}
- val subId =
- state.subId
+ val finalSubId =
+ subId
?: run {
// For sake of usability, we can allow for no subId arg if there is only one
// subscription
@@ -223,7 +284,21 @@
_subscriptions.value[0].subscriptionId
}
- removeSubscription(subId)
+ removeSubscription(finalSubId)
+ }
+
+ private fun disableCarrierMerged() {
+ val currentCarrierMergedSubId = carrierMergedSubId ?: return
+
+ // If this sub ID was previously not carrier merged, we should reset it to its previous
+ // connection.
+ val lastMobileState = connectionRepoCache[carrierMergedSubId]?.lastMobileState
+ if (lastMobileState != null) {
+ processEnabledMobileState(lastMobileState)
+ } else {
+ // Otherwise, just remove the subscription entirely
+ removeSubscription(currentCarrierMergedSubId)
+ }
}
private fun removeSubscription(subId: Int) {
@@ -251,6 +326,10 @@
)
}
+ private fun FakeWifiEventModel.CarrierMerged.toMobileConnectionModel(): MobileConnectionModel {
+ return createCarrierMergedConnectionModel(this.level)
+ }
+
private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType {
val key = mobileMappingsReverseLookup.value[this] ?: "dis"
return DefaultNetworkType(key)
@@ -260,9 +339,17 @@
private const val TAG = "DemoMobileConnectionsRepo"
private const val DEFAULT_SUB_ID = 1
+
+ private const val CARRIER_MERGED_NAME = "Carrier Merged Network"
}
}
+class CacheContainer(
+ var repo: DemoMobileConnectionRepository,
+ /** The last received [Mobile] event. Used when switching from carrier merged back to mobile. */
+ var lastMobileState: Mobile?,
+)
+
class DemoMobileConnectionRepository(
override val subId: Int,
override val tableLogBuffer: TableLogBuffer,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
new file mode 100644
index 0000000..c783b12
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 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 com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+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.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A repository implementation for a carrier merged (aka VCN) network. A carrier merged network is
+ * delivered to SysUI as a wifi network (see [WifiNetworkModel.CarrierMerged], but is visually
+ * displayed as a mobile network triangle.
+ *
+ * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
+ *
+ * See [MobileConnectionRepositoryImpl] for a repository implementation of a typical mobile
+ * connection.
+ */
+class CarrierMergedConnectionRepository(
+ override val subId: Int,
+ override val tableLogBuffer: TableLogBuffer,
+ defaultNetworkName: NetworkNameModel,
+ @Application private val scope: CoroutineScope,
+ val wifiRepository: WifiRepository,
+) : MobileConnectionRepository {
+
+ /**
+ * Outputs the carrier merged network to use, or null if we don't have a valid carrier merged
+ * network.
+ */
+ private val network: Flow<WifiNetworkModel.CarrierMerged?> =
+ combine(
+ wifiRepository.isWifiEnabled,
+ wifiRepository.isWifiDefault,
+ wifiRepository.wifiNetwork,
+ ) { isEnabled, isDefault, network ->
+ when {
+ !isEnabled -> null
+ !isDefault -> null
+ network !is WifiNetworkModel.CarrierMerged -> null
+ network.subscriptionId != subId -> {
+ Log.w(
+ TAG,
+ "Connection repo subId=$subId " +
+ "does not equal wifi repo subId=${network.subscriptionId}; " +
+ "not showing carrier merged"
+ )
+ null
+ }
+ else -> network
+ }
+ }
+
+ override val connectionInfo: StateFlow<MobileConnectionModel> =
+ network
+ .map { it.toMobileConnectionModel() }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectionModel())
+
+ // TODO(b/238425913): Add logging to this class.
+ // TODO(b/238425913): Make sure SignalStrength.getEmptyState is used when appropriate.
+
+ // Carrier merged is never roaming.
+ override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
+
+ // TODO(b/238425913): Fetch the carrier merged network name.
+ override val networkName: StateFlow<NetworkNameModel> =
+ flowOf(defaultNetworkName)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
+
+ override val numberOfLevels: StateFlow<Int> =
+ wifiRepository.wifiNetwork
+ .map {
+ if (it is WifiNetworkModel.CarrierMerged) {
+ it.numberOfLevels
+ } else {
+ DEFAULT_NUM_LEVELS
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS)
+
+ override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled
+
+ private fun WifiNetworkModel.CarrierMerged?.toMobileConnectionModel(): MobileConnectionModel {
+ if (this == null) {
+ return MobileConnectionModel()
+ }
+
+ return createCarrierMergedConnectionModel(level)
+ }
+
+ companion object {
+ /**
+ * Creates an instance of [MobileConnectionModel] that represents a carrier merged network
+ * with the given [level].
+ */
+ fun createCarrierMergedConnectionModel(level: Int): MobileConnectionModel {
+ return MobileConnectionModel(
+ primaryLevel = level,
+ cdmaLevel = level,
+ // A [WifiNetworkModel.CarrierMerged] instance is always connected.
+ // (A [WifiNetworkModel.Inactive] represents a disconnected network.)
+ dataConnectionState = DataConnectionState.Connected,
+ // TODO(b/238425913): This should come from [WifiRepository.wifiActivity].
+ dataActivityDirection =
+ DataActivityModel(
+ hasActivityIn = false,
+ hasActivityOut = false,
+ ),
+ resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType,
+ // Carrier merged is never roaming
+ isRoaming = false,
+
+ // TODO(b/238425913): Verify that these fields never change for carrier merged.
+ isEmergencyOnly = false,
+ operatorAlphaShort = null,
+ isInService = true,
+ isGsm = false,
+ carrierNetworkChangeActive = false,
+ )
+ }
+ }
+
+ @SysUISingleton
+ class Factory
+ @Inject
+ constructor(
+ @Application private val scope: CoroutineScope,
+ private val wifiRepository: WifiRepository,
+ ) {
+ fun build(
+ subId: Int,
+ mobileLogger: TableLogBuffer,
+ defaultNetworkName: NetworkNameModel,
+ ): MobileConnectionRepository {
+ return CarrierMergedConnectionRepository(
+ subId,
+ mobileLogger,
+ defaultNetworkName,
+ scope,
+ wifiRepository,
+ )
+ }
+ }
+}
+
+private const val TAG = "CarrierMergedConnectionRepository"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
new file mode 100644
index 0000000..0f30ae2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 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 com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A repository that fully implements a mobile connection.
+ *
+ * This connection could either be a typical mobile connection (see [MobileConnectionRepositoryImpl]
+ * or a carrier merged connection (see [CarrierMergedConnectionRepository]). This repository
+ * switches between the two types of connections based on whether the connection is currently
+ * carrier merged (see [setIsCarrierMerged]).
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class FullMobileConnectionRepository(
+ override val subId: Int,
+ startingIsCarrierMerged: Boolean,
+ override val tableLogBuffer: TableLogBuffer,
+ private val defaultNetworkName: NetworkNameModel,
+ private val networkNameSeparator: String,
+ private val globalMobileDataSettingChangedEvent: Flow<Unit>,
+ @Application scope: CoroutineScope,
+ private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory,
+ private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory,
+) : MobileConnectionRepository {
+ /**
+ * Sets whether this connection is a typical mobile connection or a carrier merged connection.
+ */
+ fun setIsCarrierMerged(isCarrierMerged: Boolean) {
+ _isCarrierMerged.value = isCarrierMerged
+ }
+
+ /**
+ * Returns true if this repo is currently for a carrier merged connection and false otherwise.
+ */
+ @VisibleForTesting fun getIsCarrierMerged() = _isCarrierMerged.value
+
+ private val _isCarrierMerged = MutableStateFlow(startingIsCarrierMerged)
+ private val isCarrierMerged: StateFlow<Boolean> =
+ _isCarrierMerged
+ .logDiffsForTable(
+ tableLogBuffer,
+ columnPrefix = "",
+ columnName = "isCarrierMerged",
+ initialValue = startingIsCarrierMerged,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), startingIsCarrierMerged)
+
+ private val mobileRepo: MobileConnectionRepository by lazy {
+ mobileRepoFactory.build(
+ subId,
+ tableLogBuffer,
+ defaultNetworkName,
+ networkNameSeparator,
+ globalMobileDataSettingChangedEvent,
+ )
+ }
+
+ private val carrierMergedRepo: MobileConnectionRepository by lazy {
+ carrierMergedRepoFactory.build(subId, tableLogBuffer, defaultNetworkName)
+ }
+
+ @VisibleForTesting
+ internal val activeRepo: StateFlow<MobileConnectionRepository> = run {
+ val initial =
+ if (startingIsCarrierMerged) {
+ carrierMergedRepo
+ } else {
+ mobileRepo
+ }
+
+ this.isCarrierMerged
+ .mapLatest { isCarrierMerged ->
+ if (isCarrierMerged) {
+ carrierMergedRepo
+ } else {
+ mobileRepo
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+ }
+
+ override val cdmaRoaming =
+ activeRepo
+ .flatMapLatest { it.cdmaRoaming }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaRoaming.value)
+
+ override val connectionInfo =
+ activeRepo
+ .flatMapLatest { it.connectionInfo }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.connectionInfo.value)
+
+ override val dataEnabled =
+ activeRepo
+ .flatMapLatest { it.dataEnabled }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.dataEnabled.value)
+
+ override val numberOfLevels =
+ activeRepo
+ .flatMapLatest { it.numberOfLevels }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.numberOfLevels.value)
+
+ override val networkName =
+ activeRepo
+ .flatMapLatest { it.networkName }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value)
+
+ class Factory
+ @Inject
+ constructor(
+ @Application private val scope: CoroutineScope,
+ private val logFactory: TableLogBufferFactory,
+ private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory,
+ private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory,
+ ) {
+ fun build(
+ subId: Int,
+ startingIsCarrierMerged: Boolean,
+ defaultNetworkName: NetworkNameModel,
+ networkNameSeparator: String,
+ globalMobileDataSettingChangedEvent: Flow<Unit>,
+ ): FullMobileConnectionRepository {
+ val mobileLogger =
+ logFactory.getOrCreate(tableBufferLogName(subId), MOBILE_CONNECTION_BUFFER_SIZE)
+
+ return FullMobileConnectionRepository(
+ subId,
+ startingIsCarrierMerged,
+ mobileLogger,
+ defaultNetworkName,
+ networkNameSeparator,
+ globalMobileDataSettingChangedEvent,
+ scope,
+ mobileRepoFactory,
+ carrierMergedRepoFactory,
+ )
+ }
+
+ companion object {
+ /** The buffer size to use for logging. */
+ const val MOBILE_CONNECTION_BUFFER_SIZE = 100
+
+ /** Returns a log buffer name for a mobile connection with the given [subId]. */
+ fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]"
+ }
+ }
+}
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 4e42f9b..3f2ce40 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
@@ -38,7 +38,6 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
@@ -70,6 +69,10 @@
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
+/**
+ * A repository implementation for a typical mobile connection (as opposed to a carrier merged
+ * connection -- see [CarrierMergedConnectionRepository]).
+ */
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileConnectionRepositoryImpl(
@@ -298,18 +301,16 @@
private val logger: ConnectivityPipelineLogger,
private val globalSettings: GlobalSettings,
private val mobileMappingsProxy: MobileMappingsProxy,
- private val logFactory: TableLogBufferFactory,
@Background private val bgDispatcher: CoroutineDispatcher,
@Application private val scope: CoroutineScope,
) {
fun build(
subId: Int,
+ mobileLogger: TableLogBuffer,
defaultNetworkName: NetworkNameModel,
networkNameSeparator: String,
globalMobileDataSettingChangedEvent: Flow<Unit>,
): MobileConnectionRepository {
- val mobileLogger = logFactory.getOrCreate(tableBufferLogName(subId), 100)
-
return MobileConnectionRepositoryImpl(
context,
subId,
@@ -327,8 +328,4 @@
)
}
}
-
- companion object {
- fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]"
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index c88c700..4472e09 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -46,11 +46,12 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -85,9 +86,14 @@
private val context: Context,
@Background private val bgDispatcher: CoroutineDispatcher,
@Application private val scope: CoroutineScope,
- private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
+ // Some "wifi networks" should be rendered as a mobile connection, which is why the wifi
+ // repository is an input to the mobile repository.
+ // See [CarrierMergedConnectionRepository] for details.
+ wifiRepository: WifiRepository,
+ private val fullMobileRepoFactory: FullMobileConnectionRepository.Factory,
) : MobileConnectionsRepository {
- private var subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+ private var subIdRepositoryCache: MutableMap<Int, FullMobileConnectionRepository> =
+ mutableMapOf()
private val defaultNetworkName =
NetworkNameModel.Default(
@@ -97,30 +103,43 @@
private val networkNameSeparator: String =
context.getString(R.string.status_bar_network_name_separator)
+ private val carrierMergedSubId: StateFlow<Int?> =
+ wifiRepository.wifiNetwork
+ .mapLatest {
+ if (it is WifiNetworkModel.CarrierMerged) {
+ it.subscriptionId
+ } else {
+ null
+ }
+ }
+ .distinctUntilChanged()
+ .stateIn(scope, started = SharingStarted.WhileSubscribed(), null)
+
+ private val mobileSubscriptionsChangeEvent: Flow<Unit> = conflatedCallbackFlow {
+ val callback =
+ object : SubscriptionManager.OnSubscriptionsChangedListener() {
+ override fun onSubscriptionsChanged() {
+ trySend(Unit)
+ }
+ }
+
+ subscriptionManager.addOnSubscriptionsChangedListener(
+ bgDispatcher.asExecutor(),
+ callback,
+ )
+
+ awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
+ }
+
/**
* State flow that emits the set of mobile data subscriptions, each represented by its own
- * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
- * info object, but for now we keep track of the infos themselves.
+ * [SubscriptionModel].
*/
override val subscriptions: StateFlow<List<SubscriptionModel>> =
- conflatedCallbackFlow {
- val callback =
- object : SubscriptionManager.OnSubscriptionsChangedListener() {
- override fun onSubscriptionsChanged() {
- trySend(Unit)
- }
- }
-
- subscriptionManager.addOnSubscriptionsChangedListener(
- bgDispatcher.asExecutor(),
- callback,
- )
-
- awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
- }
+ merge(mobileSubscriptionsChangeEvent, carrierMergedSubId)
.mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } }
.logInputChange(logger, "onSubscriptionsChanged")
- .onEach { infos -> dropUnusedReposFromCache(infos) }
+ .onEach { infos -> updateRepos(infos) }
.stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
/** StateFlow that keeps track of the current active mobile data subscription */
@@ -173,7 +192,7 @@
.distinctUntilChanged()
.logInputChange(logger, "defaultMobileIconGroup")
- override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+ override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository {
if (!isValidSubId(subId)) {
throw IllegalArgumentException(
"subscriptionId $subId is not in the list of valid subscriptions"
@@ -251,15 +270,27 @@
@VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
- private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
- return mobileConnectionRepositoryFactory.build(
+ private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository {
+ return fullMobileRepoFactory.build(
subId,
+ isCarrierMerged(subId),
defaultNetworkName,
networkNameSeparator,
globalMobileDataSettingChangedEvent,
)
}
+ private fun updateRepos(newInfos: List<SubscriptionModel>) {
+ dropUnusedReposFromCache(newInfos)
+ subIdRepositoryCache.forEach { (subId, repo) ->
+ repo.setIsCarrierMerged(isCarrierMerged(subId))
+ }
+ }
+
+ private fun isCarrierMerged(subId: Int): Boolean {
+ return subId == carrierMergedSubId.value
+ }
+
private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) {
// Remove any connection repository from the cache that isn't in the new set of IDs. They
// will get garbage collected once their subscribers go away
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 9427c6b..003df24 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -22,8 +22,8 @@
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
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.repository.MobileConnectionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -138,7 +138,11 @@
defaultMobileIconMapping,
defaultMobileIconGroup,
) { info, mapping, defaultGroup ->
- mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup
+ when (info.resolvedNetworkType) {
+ is ResolvedNetworkType.CarrierMergedNetworkType ->
+ info.resolvedNetworkType.iconGroupOverride
+ else -> mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup
+ }
}
.stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
index 4251d18..da2daf2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
@@ -16,13 +16,18 @@
package com.android.systemui.statusbar.pipeline.wifi.data.model
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import androidx.annotation.VisibleForTesting
import com.android.systemui.log.table.TableRowLogger
import com.android.systemui.log.table.Diffable
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
/** Provides information about the current wifi network. */
sealed class WifiNetworkModel : Diffable<WifiNetworkModel> {
+ // TODO(b/238425913): Have a better, more unified strategy for diff-logging instead of
+ // copy-pasting the column names for each sub-object.
+
/**
* A model representing that we couldn't fetch any wifi information.
*
@@ -41,8 +46,43 @@
override fun logFull(row: TableRowLogger) {
row.logChange(COL_NETWORK_TYPE, TYPE_UNAVAILABLE)
row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+ row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
row.logChange(COL_VALIDATED, false)
row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+ row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
+ row.logChange(COL_SSID, null)
+ row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+ row.logChange(COL_ONLINE_SIGN_UP, false)
+ row.logChange(COL_PASSPOINT_NAME, null)
+ }
+ }
+
+ /**
+ * A model representing that the wifi information we received was invalid in some way.
+ */
+ data class Invalid(
+ /** A description of why the wifi information was invalid. */
+ val invalidReason: String,
+ ) : WifiNetworkModel() {
+ override fun toString() = "WifiNetwork.Invalid[$invalidReason]"
+ override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+ if (prevVal !is Invalid) {
+ logFull(row)
+ return
+ }
+
+ if (invalidReason != prevVal.invalidReason) {
+ row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason")
+ }
+ }
+
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason")
+ row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+ row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
+ row.logChange(COL_VALIDATED, false)
+ row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+ row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
row.logChange(COL_SSID, null)
row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
row.logChange(COL_ONLINE_SIGN_UP, false)
@@ -59,18 +99,21 @@
return
}
- if (prevVal is CarrierMerged) {
- // The only difference between CarrierMerged and Inactive is the type
- row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE)
- return
- }
-
- // When changing from Active to Inactive, we need to log diffs to all the fields.
- logFullNonActiveNetwork(TYPE_INACTIVE, row)
+ // When changing to Inactive, we need to log diffs to all the fields.
+ logFull(row)
}
override fun logFull(row: TableRowLogger) {
- logFullNonActiveNetwork(TYPE_INACTIVE, row)
+ row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE)
+ row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+ row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
+ row.logChange(COL_VALIDATED, false)
+ row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+ row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
+ row.logChange(COL_SSID, null)
+ row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+ row.logChange(COL_ONLINE_SIGN_UP, false)
+ row.logChange(COL_PASSPOINT_NAME, null)
}
}
@@ -80,22 +123,75 @@
*
* See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
*/
- object CarrierMerged : WifiNetworkModel() {
- override fun toString() = "WifiNetwork.CarrierMerged"
+ data class CarrierMerged(
+ /**
+ * The [android.net.Network.netId] we received from
+ * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network.
+ *
+ * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId].
+ */
+ val networkId: Int,
+
+ /**
+ * The subscription ID that this connection represents.
+ *
+ * Comes from [android.net.wifi.WifiInfo.getSubscriptionId].
+ *
+ * Per that method, this value must not be [INVALID_SUBSCRIPTION_ID] (if it was invalid,
+ * then this is *not* a carrier merged network).
+ */
+ val subscriptionId: Int,
+
+ /**
+ * The signal level, guaranteed to be 0 <= level <= numberOfLevels.
+ */
+ val level: Int,
+
+ /**
+ * The maximum possible level.
+ */
+ val numberOfLevels: Int = DEFAULT_NUM_LEVELS,
+ ) : WifiNetworkModel() {
+ init {
+ require(level in MIN_VALID_LEVEL..numberOfLevels) {
+ "0 <= wifi level <= $numberOfLevels required; level was $level"
+ }
+ require(subscriptionId != INVALID_SUBSCRIPTION_ID) {
+ "subscription ID cannot be invalid"
+ }
+ }
override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
- if (prevVal is CarrierMerged) {
+ if (prevVal !is CarrierMerged) {
+ logFull(row)
return
}
- if (prevVal is Inactive) {
- // The only difference between CarrierMerged and Inactive is the type.
- row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)
- return
+ if (prevVal.networkId != networkId) {
+ row.logChange(COL_NETWORK_ID, networkId)
}
+ if (prevVal.subscriptionId != subscriptionId) {
+ row.logChange(COL_SUB_ID, subscriptionId)
+ }
+ if (prevVal.level != level) {
+ row.logChange(COL_LEVEL, level)
+ }
+ if (prevVal.numberOfLevels != numberOfLevels) {
+ row.logChange(COL_NUM_LEVELS, numberOfLevels)
+ }
+ }
- // When changing from Active to CarrierMerged, we need to log diffs to all the fields.
- logFullNonActiveNetwork(TYPE_CARRIER_MERGED, row)
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)
+ row.logChange(COL_NETWORK_ID, networkId)
+ row.logChange(COL_SUB_ID, subscriptionId)
+ row.logChange(COL_VALIDATED, true)
+ row.logChange(COL_LEVEL, level)
+ row.logChange(COL_NUM_LEVELS, numberOfLevels)
+ row.logChange(COL_SSID, null)
+ row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+ row.logChange(COL_ONLINE_SIGN_UP, false)
+ row.logChange(COL_PASSPOINT_NAME, null)
}
}
@@ -137,38 +233,50 @@
override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
if (prevVal !is Active) {
- row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE)
+ logFull(row)
+ return
}
- if (prevVal !is Active || prevVal.networkId != networkId) {
+ if (prevVal.networkId != networkId) {
row.logChange(COL_NETWORK_ID, networkId)
}
- if (prevVal !is Active || prevVal.isValidated != isValidated) {
+ if (prevVal.isValidated != isValidated) {
row.logChange(COL_VALIDATED, isValidated)
}
- if (prevVal !is Active || prevVal.level != level) {
+ if (prevVal.level != level) {
row.logChange(COL_LEVEL, level)
}
- if (prevVal !is Active || prevVal.ssid != ssid) {
+ if (prevVal.ssid != ssid) {
row.logChange(COL_SSID, ssid)
}
// TODO(b/238425913): The passpoint-related values are frequently never used, so it
// would be great to not log them when they're not used.
- if (prevVal !is Active || prevVal.isPasspointAccessPoint != isPasspointAccessPoint) {
+ if (prevVal.isPasspointAccessPoint != isPasspointAccessPoint) {
row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
}
- if (prevVal !is Active ||
- prevVal.isOnlineSignUpForPasspointAccessPoint !=
+ if (prevVal.isOnlineSignUpForPasspointAccessPoint !=
isOnlineSignUpForPasspointAccessPoint) {
row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
}
- if (prevVal !is Active ||
- prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) {
+ if (prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) {
row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
}
}
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE)
+ row.logChange(COL_NETWORK_ID, networkId)
+ row.logChange(COL_SUB_ID, null)
+ row.logChange(COL_VALIDATED, isValidated)
+ row.logChange(COL_LEVEL, level)
+ row.logChange(COL_NUM_LEVELS, null)
+ row.logChange(COL_SSID, ssid)
+ row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
+ row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
+ row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
+ }
+
override fun toString(): String {
// Only include the passpoint-related values in the string if we have them. (Most
// networks won't have them so they'll be mostly clutter.)
@@ -189,21 +297,13 @@
companion object {
@VisibleForTesting
- internal const val MIN_VALID_LEVEL = 0
- @VisibleForTesting
internal const val MAX_VALID_LEVEL = 4
}
}
- internal fun logFullNonActiveNetwork(type: String, row: TableRowLogger) {
- row.logChange(COL_NETWORK_TYPE, type)
- row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
- row.logChange(COL_VALIDATED, false)
- row.logChange(COL_LEVEL, LEVEL_DEFAULT)
- row.logChange(COL_SSID, null)
- row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
- row.logChange(COL_ONLINE_SIGN_UP, false)
- row.logChange(COL_PASSPOINT_NAME, null)
+ companion object {
+ @VisibleForTesting
+ internal const val MIN_VALID_LEVEL = 0
}
}
@@ -214,12 +314,16 @@
const val COL_NETWORK_TYPE = "type"
const val COL_NETWORK_ID = "networkId"
+const val COL_SUB_ID = "subscriptionId"
const val COL_VALIDATED = "isValidated"
const val COL_LEVEL = "level"
+const val COL_NUM_LEVELS = "maxLevel"
const val COL_SSID = "ssid"
const val COL_PASSPOINT_ACCESS_POINT = "isPasspointAccessPoint"
const val COL_ONLINE_SIGN_UP = "isOnlineSignUpForPasspointAccessPoint"
const val COL_PASSPOINT_NAME = "passpointProviderFriendlyName"
val LEVEL_DEFAULT: String? = null
+val NUM_LEVELS_DEFAULT: String? = null
val NETWORK_ID_DEFAULT: String? = null
+val SUB_ID_DEFAULT: String? = null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
index c588945..caac8fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
@@ -22,6 +22,7 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -43,10 +44,10 @@
private fun Bundle.toWifiEvent(): FakeWifiEventModel? {
val wifi = getString("wifi") ?: return null
- return if (wifi == "show") {
- activeWifiEvent()
- } else {
- FakeWifiEventModel.WifiDisabled
+ return when (wifi) {
+ "show" -> activeWifiEvent()
+ "carriermerged" -> carrierMergedWifiEvent()
+ else -> FakeWifiEventModel.WifiDisabled
}
}
@@ -64,6 +65,14 @@
)
}
+ private fun Bundle.carrierMergedWifiEvent(): FakeWifiEventModel.CarrierMerged {
+ val subId = getString("slot")?.toInt() ?: DEFAULT_CARRIER_MERGED_SUB_ID
+ val level = getString("level")?.toInt() ?: 0
+ val numberOfLevels = getString("numlevels")?.toInt() ?: DEFAULT_NUM_LEVELS
+
+ return FakeWifiEventModel.CarrierMerged(subId, level, numberOfLevels)
+ }
+
private fun String.toActivity(): Int =
when (this) {
"inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT
@@ -71,4 +80,8 @@
"out" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT
else -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE
}
+
+ companion object {
+ const val DEFAULT_CARRIER_MERGED_SUB_ID = 10
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
index be3d7d4..e161b3e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
@@ -66,6 +66,7 @@
private fun processEvent(event: FakeWifiEventModel) =
when (event) {
is FakeWifiEventModel.Wifi -> processEnabledWifiState(event)
+ is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event)
is FakeWifiEventModel.WifiDisabled -> processDisabledWifiState()
}
@@ -85,6 +86,14 @@
_wifiNetwork.value = event.toWifiNetworkModel()
}
+ private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) {
+ _isWifiEnabled.value = true
+ _isWifiDefault.value = true
+ // TODO(b/238425913): Support activity in demo mode.
+ _wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+ _wifiNetwork.value = event.toCarrierMergedModel()
+ }
+
private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel =
WifiNetworkModel.Active(
networkId = DEMO_NET_ID,
@@ -99,6 +108,14 @@
passpointProviderFriendlyName = null,
)
+ private fun FakeWifiEventModel.CarrierMerged.toCarrierMergedModel(): WifiNetworkModel =
+ WifiNetworkModel.CarrierMerged(
+ networkId = DEMO_NET_ID,
+ subscriptionId = subscriptionId,
+ level = level,
+ numberOfLevels = numberOfLevels,
+ )
+
companion object {
private const val DEMO_NET_ID = 1234
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
index 2353fb8..518f8ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
@@ -29,5 +29,11 @@
val validated: Boolean?,
) : FakeWifiEventModel
+ data class CarrierMerged(
+ val subscriptionId: Int,
+ val level: Int,
+ val numberOfLevels: Int,
+ ) : FakeWifiEventModel
+
object WifiDisabled : FakeWifiEventModel
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index c47c20d..d26499c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -29,6 +29,7 @@
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.TrafficStateCallback
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import com.android.settingslib.Utils
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -269,7 +270,19 @@
wifiManager: WifiManager,
): WifiNetworkModel {
return if (wifiInfo.isCarrierMerged) {
- WifiNetworkModel.CarrierMerged
+ if (wifiInfo.subscriptionId == INVALID_SUBSCRIPTION_ID) {
+ WifiNetworkModel.Invalid(CARRIER_MERGED_INVALID_SUB_ID_REASON)
+ } else {
+ WifiNetworkModel.CarrierMerged(
+ networkId = network.getNetId(),
+ subscriptionId = wifiInfo.subscriptionId,
+ level = wifiManager.calculateSignalLevel(wifiInfo.rssi),
+ // The WiFi signal level returned by WifiManager#calculateSignalLevel start
+ // from 0, so WifiManager#getMaxSignalLevel + 1 represents the total level
+ // buckets count.
+ numberOfLevels = wifiManager.maxSignalLevel + 1,
+ )
+ }
} else {
WifiNetworkModel.Active(
network.getNetId(),
@@ -302,6 +315,9 @@
.build()
private const val WIFI_NETWORK_CALLBACK_NAME = "wifiNetworkModel"
+
+ private const val CARRIER_MERGED_INVALID_SUB_ID_REASON =
+ "Wifi network was carrier merged but had invalid sub ID"
}
@SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index 980560a..86dcd18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -66,6 +66,7 @@
override val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info ->
when (info) {
is WifiNetworkModel.Unavailable -> null
+ is WifiNetworkModel.Invalid -> null
is WifiNetworkModel.Inactive -> null
is WifiNetworkModel.CarrierMerged -> null
is WifiNetworkModel.Active -> when {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index 824b597..95431af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -83,6 +83,7 @@
private fun WifiNetworkModel.icon(): WifiIcon {
return when (this) {
is WifiNetworkModel.Unavailable -> WifiIcon.Hidden
+ is WifiNetworkModel.Invalid -> WifiIcon.Hidden
is WifiNetworkModel.CarrierMerged -> WifiIcon.Hidden
is WifiNetworkModel.Inactive -> WifiIcon.Visible(
res = WIFI_NO_NETWORK,
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
index 235495cf..b22af3b 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
@@ -102,8 +102,7 @@
registerBatteryListener(deviceId)
}
- // TODO(b/257936830): get address once input api available
- val btAddress: String? = null
+ val btAddress: String? = device.bluetoothAddress
inputDeviceAddressMap[deviceId] = btAddress
executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) }
@@ -120,8 +119,7 @@
val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
- // TODO(b/257936830): get address once input api available
- val currAddress: String? = null
+ val currAddress: String? = device.bluetoothAddress
val prevAddress: String? = inputDeviceAddressMap[deviceId]
inputDeviceAddressMap[deviceId] = currAddress
@@ -212,7 +210,6 @@
* physical stylus device has actually been used.
*/
private fun onStylusUsed() {
- if (true) return // TODO(b/261826950): remove on main
if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
if (inputManager.isStylusEverUsed(context)) return
@@ -250,8 +247,7 @@
for (deviceId: Int in inputManager.inputDeviceIds) {
val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue
if (device.supportsSource(InputDevice.SOURCE_STYLUS)) {
- // TODO(b/257936830): get address once input api available
- inputDeviceAddressMap[deviceId] = null
+ inputDeviceAddressMap[deviceId] = device.bluetoothAddress
if (!device.isExternal) { // TODO(b/263556967): add supportsUsi check once available
// For most devices, an active (non-bluetooth) stylus is represented by an
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt
index 14a9161..5a8850a 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt
@@ -52,8 +52,8 @@
eventTimeMillis: Long,
batteryState: BatteryState
) {
- if (batteryState.isPresent) {
- stylusUsiPowerUi.updateBatteryState(batteryState)
+ if (batteryState.isPresent && batteryState.capacity > 0f) {
+ stylusUsiPowerUi.updateBatteryState(deviceId, batteryState)
}
}
@@ -61,6 +61,7 @@
if (!featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)) return
if (!hostDeviceSupportsStylusInput()) return
+ stylusUsiPowerUi.init()
stylusManager.registerCallback(this)
stylusManager.startListener()
}
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
index e821657..8d5e01c 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
@@ -18,17 +18,21 @@
import android.Manifest
import android.app.PendingIntent
+import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.BatteryState
import android.hardware.input.InputManager
+import android.os.Bundle
import android.os.Handler
import android.os.UserHandle
+import android.util.Log
import android.view.InputDevice
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
+import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -53,6 +57,7 @@
// These values must only be accessed on the handler.
private var batteryCapacity = 1.0f
private var suppressed = false
+ private var inputDeviceId: Int? = null
fun init() {
val filter =
@@ -87,10 +92,12 @@
}
}
- fun updateBatteryState(batteryState: BatteryState) {
+ fun updateBatteryState(deviceId: Int, batteryState: BatteryState) {
handler.post updateBattery@{
- if (batteryState.capacity == batteryCapacity) return@updateBattery
+ if (batteryState.capacity == batteryCapacity || batteryState.capacity <= 0f)
+ return@updateBattery
+ inputDeviceId = deviceId
batteryCapacity = batteryState.capacity
refresh()
}
@@ -150,23 +157,41 @@
}
private fun getPendingBroadcast(action: String): PendingIntent? {
- return PendingIntent.getBroadcastAsUser(
+ return PendingIntent.getBroadcast(
context,
0,
- Intent(action),
+ Intent(action).setPackage(context.packageName),
PendingIntent.FLAG_IMMUTABLE,
- UserHandle.CURRENT
)
}
- private val receiver: BroadcastReceiver =
+ @VisibleForTesting
+ internal val receiver: BroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
ACTION_DISMISSED_LOW_BATTERY -> updateSuppression(true)
ACTION_CLICKED_LOW_BATTERY -> {
updateSuppression(true)
- // TODO(b/261584943): open USI device details page
+ if (inputDeviceId == null) return
+
+ val args = Bundle()
+ args.putInt(KEY_DEVICE_INPUT_ID, inputDeviceId!!)
+ try {
+ context.startActivity(
+ Intent(ACTION_STYLUS_USI_DETAILS)
+ .putExtra(KEY_SETTINGS_FRAGMENT_ARGS, args)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ } catch (e: ActivityNotFoundException) {
+ // In the rare scenario where the Settings app manifest doesn't contain
+ // the USI details activity, ignore the intent.
+ Log.e(
+ StylusUsiPowerUI::class.java.simpleName,
+ "Cannot open USI details page."
+ )
+ }
}
}
}
@@ -179,7 +204,11 @@
private val USI_NOTIFICATION_ID = R.string.stylus_battery_low_percentage
- private const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss"
- private const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click"
+ @VisibleForTesting const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss"
+ @VisibleForTesting const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click"
+ @VisibleForTesting
+ const val ACTION_STYLUS_USI_DETAILS = "com.android.settings.STYLUS_USI_DETAILS_SETTINGS"
+ @VisibleForTesting const val KEY_DEVICE_INPUT_ID = "device_input_id"
+ @VisibleForTesting const val KEY_SETTINGS_FRAGMENT_ARGS = ":settings:show_fragment_args"
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
index 0a81c38..ebbe096 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
@@ -269,6 +269,14 @@
}
@Test
+ fun testBindServiceForPanel() {
+ controller.bindServiceForPanel(TEST_COMPONENT_NAME_1)
+ executor.runAllReady()
+
+ verify(providers[0]).bindServiceForPanel()
+ }
+
+ @Test
fun testSubscribe() {
val controlInfo1 = ControlInfo("id_1", "", "", DeviceTypes.TYPE_UNKNOWN)
val controlInfo2 = ControlInfo("id_2", "", "", DeviceTypes.TYPE_UNKNOWN)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index 1b34706..25f471b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -919,6 +919,12 @@
.getFile(ControlsFavoritePersistenceWrapper.FILE_NAME, context.user.identifier)
assertThat(userStructure.file).isNotNull()
}
+
+ @Test
+ fun testBindForPanel() {
+ controller.bindComponentForPanel(TEST_COMPONENT)
+ verify(bindingController).bindServiceForPanel(TEST_COMPONENT)
+ }
}
private class DidRunRunnable() : Runnable {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
index af3f24a..da548f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
@@ -105,6 +105,22 @@
}
@Test
+ fun testBindForPanel() {
+ manager.bindServiceForPanel()
+ executor.runAllReady()
+ assertTrue(context.isBound(componentName))
+ }
+
+ @Test
+ fun testUnbindPanelIsUnbound() {
+ manager.bindServiceForPanel()
+ executor.runAllReady()
+ manager.unbindService()
+ executor.runAllReady()
+ assertFalse(context.isBound(componentName))
+ }
+
+ @Test
fun testNullBinding() {
val mockContext = mock(Context::class.java)
lateinit var serviceConnection: ServiceConnection
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index d172c9a..edc6882 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -229,6 +229,15 @@
}
@Test
+ fun testPanelBindsForPanel() {
+ val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+ setUpPanel(panel)
+
+ underTest.show(parent, {}, context)
+ verify(controlsController).bindComponentForPanel(panel.componentName)
+ }
+
+ @Test
fun testPanelCallsTaskViewFactoryCreate() {
mockLayoutInflater()
val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index f8f2a56..32cec09 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -168,6 +168,25 @@
assertThat(wtfHandler.failed).isTrue()
}
+ @Test
+ fun `Attempt to manually update transition after CANCELED state throws exception`() {
+ val uuid =
+ underTest.startTransition(
+ TransitionInfo(
+ ownerName = OWNER_NAME,
+ from = AOD,
+ to = LOCKSCREEN,
+ animator = null,
+ )
+ )
+
+ checkNotNull(uuid).let {
+ underTest.updateTransition(it, 0.2f, TransitionState.CANCELED)
+ underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
+ }
+ assertThat(wtfHandler.failed).isTrue()
+ }
+
private fun listWithStep(
step: BigDecimal,
start: BigDecimal = BigDecimal.ZERO,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index b3cee22..a1b6d47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -38,6 +38,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -177,7 +178,7 @@
keyguardRepository.setDreamingWithOverlay(false)
// AND occluded has stopped
keyguardRepository.setKeyguardOccluded(false)
- runCurrent()
+ advanceUntilIdle()
val info =
withArgCaptor<TransitionInfo> {
@@ -506,7 +507,7 @@
withArgCaptor<TransitionInfo> {
verify(mockTransitionRepository).startTransition(capture())
}
- // THEN a transition to DOZING should occur
+ // THEN a transition to AOD should occur
assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
assertThat(info.from).isEqualTo(KeyguardState.GONE)
assertThat(info.to).isEqualTo(KeyguardState.AOD)
@@ -515,6 +516,49 @@
coroutineContext.cancelChildren()
}
+ @Test
+ fun `GONE to DREAMING`() =
+ testScope.runTest {
+ // GIVEN a device that is not dreaming or dozing
+ keyguardRepository.setDreamingWithOverlay(false)
+ keyguardRepository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+ )
+ runCurrent()
+
+ // GIVEN a prior transition has run to GONE
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ reset(mockTransitionRepository)
+
+ // WHEN the device begins to dream
+ keyguardRepository.setDreamingWithOverlay(true)
+ advanceUntilIdle()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DREAMING should occur
+ assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.GONE)
+ assertThat(info.to).isEqualTo(KeyguardState.DREAMING)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
private fun startingToWake() =
WakefulnessModel(
WakefulnessState.STARTING_TO_WAKE,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
new file mode 100644
index 0000000..7fa204b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA
+import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class GoneToDreamingTransitionViewModelTest : SysuiTestCase() {
+ private lateinit var underTest: GoneToDreamingTransitionViewModel
+ private lateinit var repository: FakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ repository = FakeKeyguardTransitionRepository()
+ val interactor = KeyguardTransitionInteractor(repository)
+ underTest = GoneToDreamingTransitionViewModel(interactor)
+ }
+
+ @Test
+ fun lockscreenFadeOut() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+ // Should start running here...
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.1f))
+ repository.sendTransitionStep(step(0.2f))
+ // ...up to here
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(1f))
+
+ // Only three values should be present, since the dream overlay runs for a small
+ // fraction
+ // of the overall animation time
+ assertThat(values.size).isEqualTo(3)
+ assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
+ assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
+ assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA))
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenTranslationY() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+ // Should start running here...
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.5f))
+ // ...up to here
+ repository.sendTransitionStep(step(1f))
+ // And a final reset event on CANCEL
+ repository.sendTransitionStep(step(0.8f, TransitionState.CANCELED))
+
+ assertThat(values.size).isEqualTo(4)
+ assertThat(values[0])
+ .isEqualTo(
+ EMPHASIZED_ACCELERATE.getInterpolation(
+ animValue(0f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[1])
+ .isEqualTo(
+ EMPHASIZED_ACCELERATE.getInterpolation(
+ animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[2])
+ .isEqualTo(
+ EMPHASIZED_ACCELERATE.getInterpolation(
+ animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[3]).isEqualTo(0f)
+ job.cancel()
+ }
+
+ private fun animValue(stepValue: Float, params: AnimationParams): Float {
+ val totalDuration = TO_DREAMING_DURATION
+ val startValue = (params.startTime / totalDuration).toFloat()
+
+ val multiplier = (totalDuration / params.duration).toFloat()
+ return (stepValue - startValue) * multiplier
+ }
+
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.DREAMING,
+ value = value,
+ transitionState = state,
+ ownerName = "GoneToDreamingTransitionViewModelTest"
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index 7390591..539fc2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -67,8 +67,7 @@
repository.sendTransitionStep(step(1f))
// Only three values should be present, since the dream overlay runs for a small
- // fraction
- // of the overall animation time
+ // fraction of the overall animation time
assertThat(values.size).isEqualTo(3)
assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
@@ -92,8 +91,10 @@
repository.sendTransitionStep(step(0.5f))
// ...up to here
repository.sendTransitionStep(step(1f))
+ // And a final reset event on FINISHED
+ repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
- assertThat(values.size).isEqualTo(3)
+ assertThat(values.size).isEqualTo(4)
assertThat(values[0])
.isEqualTo(
EMPHASIZED_ACCELERATE.getInterpolation(
@@ -112,6 +113,8 @@
animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
) * pixels
)
+ assertThat(values[3]).isEqualTo(0f)
+
job.cancel()
}
@@ -123,12 +126,15 @@
return (stepValue - startValue) * multiplier
}
- private fun step(value: Float): TransitionStep {
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
return TransitionStep(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.DREAMING,
value = value,
- transitionState = TransitionState.RUNNING,
+ transitionState = state,
ownerName = "LockscreenToDreamingTransitionViewModelTest"
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index fc90c1a..8440455 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -24,7 +24,7 @@
import android.test.suitebuilder.annotation.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
@@ -50,7 +50,7 @@
@RunWith(AndroidJUnit4::class)
internal class NoteTaskControllerTest : SysuiTestCase() {
- private val notesIntent = Intent(NOTES_ACTION)
+ private val notesIntent = Intent(ACTION_CREATE_NOTE)
@Mock lateinit var context: Context
@Mock lateinit var packageManager: PackageManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 538131a..010ac5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -106,7 +106,9 @@
// region handleSystemKey
@Test
fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() {
- createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+ createNoteTaskInitializer()
+ .callbacks
+ .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
verify(noteTaskController).showNoteTask()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
index dd2cc2f..bbe60f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
@@ -23,11 +23,10 @@
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ResolveInfoFlags
import android.content.pm.ResolveInfo
-import android.content.pm.ServiceInfo
import android.test.suitebuilder.annotation.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -58,19 +57,13 @@
}
private fun createResolveInfo(
- packageName: String = "PackageName",
- activityInfo: ActivityInfo? = null,
+ activityInfo: ActivityInfo? = createActivityInfo(),
): ResolveInfo {
- return ResolveInfo().apply {
- serviceInfo =
- ServiceInfo().apply {
- applicationInfo = ApplicationInfo().apply { this.packageName = packageName }
- }
- this.activityInfo = activityInfo
- }
+ return ResolveInfo().apply { this.activityInfo = activityInfo }
}
private fun createActivityInfo(
+ packageName: String = "PackageName",
name: String? = "ActivityName",
exported: Boolean = true,
enabled: Boolean = true,
@@ -87,6 +80,7 @@
if (turnScreenOn) {
flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON
}
+ this.applicationInfo = ApplicationInfo().apply { this.packageName = packageName }
}
}
@@ -107,7 +101,8 @@
val actual = resolver.resolveIntent()
val expected =
- Intent(NOTES_ACTION)
+ Intent(ACTION_CREATE_NOTE)
+ .setPackage("PackageName")
.setComponent(ComponentName("PackageName", "ActivityName"))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// Compares the string representation of both intents, as they are different instances.
@@ -204,7 +199,9 @@
@Test
fun resolveIntent_packageNameIsBlank_shouldReturnNull() {
- givenQueryIntentActivities { listOf(createResolveInfo(packageName = "")) }
+ givenQueryIntentActivities {
+ listOf(createResolveInfo(createActivityInfo(packageName = "")))
+ }
val actual = resolver.resolveIntent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index a1e9a27..6dd2d61 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -107,6 +107,7 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
@@ -299,6 +300,7 @@
@Mock private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
@Mock private LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel;
@Mock private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel;
+ @Mock private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
@Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@Mock private CoroutineDispatcher mMainDispatcher;
@@ -522,6 +524,7 @@
mDreamingToLockscreenTransitionViewModel,
mOccludedToLockscreenTransitionViewModel,
mLockscreenToDreamingTransitionViewModel,
+ mGoneToDreamingTransitionViewModel,
mLockscreenToOccludedTransitionViewModel,
mMainDispatcher,
mKeyguardTransitionInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 08a9c96..526dc8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -46,11 +46,14 @@
import androidx.test.filters.SmallTest;
import com.android.internal.colorextraction.ColorExtractor;
+import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -68,6 +71,8 @@
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
+import java.util.List;
+
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
@@ -91,13 +96,21 @@
@Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
@Mock private ShadeWindowLogger mShadeWindowLogger;
@Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
+ @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener;
private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
-
+ private float mPreferredRefreshRate = -1;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ // Preferred refresh rate is equal to the first displayMode's refresh rate
+ mPreferredRefreshRate = mContext.getDisplay().getSupportedModes()[0].getRefreshRate();
+ overrideResource(
+ R.integer.config_keyguardRefreshRate,
+ (int) mPreferredRefreshRate
+ );
+
when(mDozeParameters.getAlwaysOn()).thenReturn(true);
when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
@@ -117,6 +130,7 @@
mNotificationShadeWindowController.attach();
verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any());
+ verify(mStatusBarStateController).addCallback(mStateListener.capture(), anyInt());
}
@Test
@@ -334,4 +348,59 @@
assertThat(mLayoutParameters.getValue().screenOrientation)
.isEqualTo(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
}
+
+ @Test
+ public void udfpsEnrolled_minAndMaxRefreshRateSetToPreferredRefreshRate() {
+ // GIVEN udfps is enrolled
+ when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true);
+
+ // WHEN keyguard is showing
+ setKeyguardShowing();
+
+ // THEN min and max refresh rate is set to the preferredRefreshRate
+ verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+ final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues();
+ final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1);
+ assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(mPreferredRefreshRate);
+ assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(mPreferredRefreshRate);
+ }
+
+ @Test
+ public void udfpsNotEnrolled_refreshRateUnset() {
+ // GIVEN udfps is NOT enrolled
+ when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false);
+
+ // WHEN keyguard is showing
+ setKeyguardShowing();
+
+ // THEN min and max refresh rate aren't set (set to 0)
+ verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+ final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues();
+ final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1);
+ assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(0);
+ assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(0);
+ }
+
+ @Test
+ public void keyguardNotShowing_refreshRateUnset() {
+ // GIVEN UDFPS is enrolled
+ when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true);
+
+ // WHEN keyguard is NOT showing
+ mNotificationShadeWindowController.setKeyguardShowing(false);
+
+ // THEN min and max refresh rate aren't set (set to 0)
+ verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+ final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues();
+ final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1);
+ assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(0);
+ assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(0);
+ }
+
+ private void setKeyguardShowing() {
+ mNotificationShadeWindowController.setKeyguardShowing(true);
+ mNotificationShadeWindowController.setKeyguardGoingAway(false);
+ mNotificationShadeWindowController.setKeyguardFadingAway(false);
+ mStateListener.getValue().onStateChanged(StatusBarState.KEYGUARD);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 0000c32..fc7cd89 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -209,9 +209,9 @@
@Test
public void testShowRecentApps() {
- mCommandQueue.showRecentApps(true);
+ mCommandQueue.showRecentApps(true, false);
waitForIdleSync();
- verify(mCallbacks).showRecentApps(eq(true));
+ verify(mCallbacks).showRecentApps(eq(true), eq(false));
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
index 5d377a8..0859d14 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
@@ -34,6 +34,8 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.kotlinArgumentCaptor
import com.android.systemui.util.mockito.mock
@@ -71,8 +73,10 @@
private lateinit var underTest: MobileRepositorySwitcher
private lateinit var realRepo: MobileConnectionsRepositoryImpl
private lateinit var demoRepo: DemoMobileConnectionsRepository
- private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+ private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource
+ private lateinit var wifiDataSource: DemoModeWifiDataSource
private lateinit var logFactory: TableLogBufferFactory
+ private lateinit var wifiRepository: FakeWifiRepository
@Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var subscriptionManager: SubscriptionManager
@@ -96,10 +100,15 @@
// Never start in demo mode
whenever(demoModeController.isInDemoMode).thenReturn(false)
- mockDataSource =
+ mobileDataSource =
mock<DemoModeMobileConnectionDataSource>().also {
whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow)
}
+ wifiDataSource =
+ mock<DemoModeWifiDataSource>().also {
+ whenever(it.wifiEvents).thenReturn(MutableStateFlow(null))
+ }
+ wifiRepository = FakeWifiRepository()
realRepo =
MobileConnectionsRepositoryImpl(
@@ -113,12 +122,14 @@
context,
IMMEDIATE,
scope,
+ wifiRepository,
mock(),
)
demoRepo =
DemoMobileConnectionsRepository(
- dataSource = mockDataSource,
+ mobileDataSource = mobileDataSource,
+ wifiDataSource = wifiDataSource,
scope = scope,
context = context,
logFactory = logFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
index 2102085..6989b514 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
@@ -29,6 +29,8 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
@@ -63,10 +65,12 @@
private val testScope = TestScope(testDispatcher)
private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+ private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null)
private lateinit var connectionsRepo: DemoMobileConnectionsRepository
private lateinit var underTest: DemoMobileConnectionRepository
private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+ private lateinit var mockWifiDataSource: DemoModeWifiDataSource
@Before
fun setUp() {
@@ -75,10 +79,15 @@
mock<DemoModeMobileConnectionDataSource>().also {
whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
}
+ mockWifiDataSource =
+ mock<DemoModeWifiDataSource>().also {
+ whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow)
+ }
connectionsRepo =
DemoMobileConnectionsRepository(
- dataSource = mockDataSource,
+ mobileDataSource = mockDataSource,
+ wifiDataSource = mockWifiDataSource,
scope = testScope.backgroundScope,
context = context,
logFactory = logFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
index cdbe75e..9d16b7fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
@@ -32,6 +32,8 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
@@ -57,21 +59,28 @@
private val testScope = TestScope(testDispatcher)
private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+ private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null)
private lateinit var underTest: DemoMobileConnectionsRepository
- private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+ private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource
+ private lateinit var wifiDataSource: DemoModeWifiDataSource
@Before
fun setUp() {
// The data source only provides one API, so we can mock it with a flow here for convenience
- mockDataSource =
+ mobileDataSource =
mock<DemoModeMobileConnectionDataSource>().also {
whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
}
+ wifiDataSource =
+ mock<DemoModeWifiDataSource>().also {
+ whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow)
+ }
underTest =
DemoMobileConnectionsRepository(
- dataSource = mockDataSource,
+ mobileDataSource = mobileDataSource,
+ wifiDataSource = wifiDataSource,
scope = testScope.backgroundScope,
context = context,
logFactory = logFactory,
@@ -97,6 +106,22 @@
}
@Test
+ fun `wifi carrier merged event - create new subscription`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEmpty()
+
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].subscriptionId).isEqualTo(5)
+
+ job.cancel()
+ }
+
+ @Test
fun `network event - reuses subscription when same Id`() =
testScope.runTest {
var latest: List<SubscriptionModel>? = null
@@ -119,6 +144,28 @@
}
@Test
+ fun `wifi carrier merged event - reuses subscription when same Id`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEmpty()
+
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 1)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].subscriptionId).isEqualTo(5)
+
+ // Second network event comes in with the same subId, does not create a new subscription
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 2)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].subscriptionId).isEqualTo(5)
+
+ job.cancel()
+ }
+
+ @Test
fun `multiple subscriptions`() =
testScope.runTest {
var latest: List<SubscriptionModel>? = null
@@ -133,6 +180,35 @@
}
@Test
+ fun `mobile subscription and carrier merged subscription`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5)
+
+ assertThat(latest).hasSize(2)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `multiple mobile subscriptions and carrier merged subscription`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 2)
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 3)
+
+ assertThat(latest).hasSize(3)
+
+ job.cancel()
+ }
+
+ @Test
fun `mobile disabled event - disables connection - subId specified - single conn`() =
testScope.runTest {
var latest: List<SubscriptionModel>? = null
@@ -194,6 +270,112 @@
job.cancel()
}
+ @Test
+ fun `wifi network updates to disabled - carrier merged connection removed`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1)
+
+ assertThat(latest).hasSize(1)
+
+ fakeWifiEventFlow.value = FakeWifiEventModel.WifiDisabled
+
+ assertThat(latest).isEmpty()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `wifi network updates to active - carrier merged connection removed`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1)
+
+ assertThat(latest).hasSize(1)
+
+ fakeWifiEventFlow.value =
+ FakeWifiEventModel.Wifi(
+ level = 1,
+ activity = 0,
+ ssid = null,
+ validated = true,
+ )
+
+ assertThat(latest).isEmpty()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `mobile sub updates to carrier merged - only one connection`() =
+ testScope.runTest {
+ var latestSubsList: List<SubscriptionModel>? = null
+ var connections: List<DemoMobileConnectionRepository>? = null
+ val job =
+ underTest.subscriptions
+ .onEach { latestSubsList = it }
+ .onEach { infos ->
+ connections =
+ infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+ }
+ .launchIn(this)
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 3, level = 2)
+ assertThat(latestSubsList).hasSize(1)
+
+ val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1)
+ fakeWifiEventFlow.value = carrierMergedEvent
+ assertThat(latestSubsList).hasSize(1)
+ val connection = connections!!.find { it.subId == 3 }!!
+ assertCarrierMergedConnection(connection, carrierMergedEvent)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `mobile sub updates to carrier merged then back - has old mobile data`() =
+ testScope.runTest {
+ var latestSubsList: List<SubscriptionModel>? = null
+ var connections: List<DemoMobileConnectionRepository>? = null
+ val job =
+ underTest.subscriptions
+ .onEach { latestSubsList = it }
+ .onEach { infos ->
+ connections =
+ infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+ }
+ .launchIn(this)
+
+ val mobileEvent = validMobileEvent(subId = 3, level = 2)
+ fakeNetworkEventFlow.value = mobileEvent
+ assertThat(latestSubsList).hasSize(1)
+
+ val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1)
+ fakeWifiEventFlow.value = carrierMergedEvent
+ assertThat(latestSubsList).hasSize(1)
+ var connection = connections!!.find { it.subId == 3 }!!
+ assertCarrierMergedConnection(connection, carrierMergedEvent)
+
+ // WHEN the carrier merged is removed
+ fakeWifiEventFlow.value =
+ FakeWifiEventModel.Wifi(
+ level = 4,
+ activity = 0,
+ ssid = null,
+ validated = true,
+ )
+
+ // THEN the subId=3 connection goes back to the mobile information
+ connection = connections!!.find { it.subId == 3 }!!
+ assertConnection(connection, mobileEvent)
+
+ job.cancel()
+ }
+
/** Regression test for b/261706421 */
@Test
fun `multiple connections - remove all - does not throw`() =
@@ -289,6 +471,51 @@
job.cancel()
}
+ @Test
+ fun `demo connection - two connections - update carrier merged - no affect on first`() =
+ testScope.runTest {
+ var currentEvent1 = validMobileEvent(subId = 1)
+ var connection1: DemoMobileConnectionRepository? = null
+ var currentEvent2 = validCarrierMergedEvent(subId = 2)
+ var connection2: DemoMobileConnectionRepository? = null
+ var connections: List<DemoMobileConnectionRepository>? = null
+ val job =
+ underTest.subscriptions
+ .onEach { infos ->
+ connections =
+ infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+ }
+ .launchIn(this)
+
+ fakeNetworkEventFlow.value = currentEvent1
+ fakeWifiEventFlow.value = currentEvent2
+ assertThat(connections).hasSize(2)
+ connections!!.forEach {
+ when (it.subId) {
+ 1 -> connection1 = it
+ 2 -> connection2 = it
+ else -> Assert.fail("Unexpected subscription")
+ }
+ }
+
+ assertConnection(connection1!!, currentEvent1)
+ assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+ // WHEN the event changes for connection 2, it updates, and connection 1 stays the same
+ currentEvent2 = validCarrierMergedEvent(subId = 2, level = 4)
+ fakeWifiEventFlow.value = currentEvent2
+ assertConnection(connection1!!, currentEvent1)
+ assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+ // and vice versa
+ currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true)
+ fakeNetworkEventFlow.value = currentEvent1
+ assertConnection(connection1!!, currentEvent1)
+ assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+ job.cancel()
+ }
+
private fun assertConnection(
conn: DemoMobileConnectionRepository,
model: FakeNetworkEventModel
@@ -315,6 +542,21 @@
else -> {}
}
}
+
+ private fun assertCarrierMergedConnection(
+ conn: DemoMobileConnectionRepository,
+ model: FakeWifiEventModel.CarrierMerged,
+ ) {
+ val connectionInfo: MobileConnectionModel = conn.connectionInfo.value
+ assertThat(conn.subId).isEqualTo(model.subscriptionId)
+ assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level)
+ assertThat(connectionInfo.primaryLevel).isEqualTo(model.level)
+ assertThat(connectionInfo.carrierNetworkChangeActive).isEqualTo(false)
+ assertThat(connectionInfo.isRoaming).isEqualTo(false)
+ assertThat(connectionInfo.isEmergencyOnly).isFalse()
+ assertThat(connectionInfo.isGsm).isFalse()
+ assertThat(connectionInfo.dataConnectionState).isEqualTo(DataConnectionState.Connected)
+ }
}
/** Convenience to create a valid fake network event with minimal params */
@@ -339,3 +581,14 @@
roaming = roaming,
name = "demo name",
)
+
+fun validCarrierMergedEvent(
+ subId: Int = 1,
+ level: Int = 1,
+ numberOfLevels: Int = 4,
+): FakeWifiEventModel.CarrierMerged =
+ FakeWifiEventModel.CarrierMerged(
+ subscriptionId = subId,
+ level = level,
+ numberOfLevels = numberOfLevels,
+ )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
new file mode 100644
index 0000000..ea90150
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 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 com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import android.testing.AndroidTestingRunner
+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.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+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.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+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.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+class CarrierMergedConnectionRepositoryTest : SysuiTestCase() {
+
+ private lateinit var underTest: CarrierMergedConnectionRepository
+
+ private lateinit var wifiRepository: FakeWifiRepository
+ @Mock private lateinit var logger: TableLogBuffer
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ wifiRepository = FakeWifiRepository()
+
+ underTest =
+ CarrierMergedConnectionRepository(
+ SUB_ID,
+ logger,
+ NetworkNameModel.Default("name"),
+ testScope.backgroundScope,
+ wifiRepository,
+ )
+ }
+
+ @Test
+ fun connectionInfo_inactiveWifi_isDefault() =
+ testScope.runTest {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+
+ assertThat(latest).isEqualTo(MobileConnectionModel())
+
+ job.cancel()
+ }
+
+ @Test
+ fun connectionInfo_activeWifi_isDefault() =
+ testScope.runTest {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = NET_ID, level = 1))
+
+ assertThat(latest).isEqualTo(MobileConnectionModel())
+
+ job.cancel()
+ }
+
+ @Test
+ fun connectionInfo_carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() =
+ testScope.runTest {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setIsWifiEnabled(true)
+ wifiRepository.setIsWifiDefault(true)
+
+ wifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged(
+ networkId = NET_ID,
+ subscriptionId = SUB_ID,
+ level = 3,
+ )
+ )
+
+ val expected =
+ MobileConnectionModel(
+ primaryLevel = 3,
+ cdmaLevel = 3,
+ dataConnectionState = DataConnectionState.Connected,
+ dataActivityDirection =
+ DataActivityModel(
+ hasActivityIn = false,
+ hasActivityOut = false,
+ ),
+ resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType,
+ isRoaming = false,
+ isEmergencyOnly = false,
+ operatorAlphaShort = null,
+ isInService = true,
+ isGsm = false,
+ carrierNetworkChangeActive = false,
+ )
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun connectionInfo_carrierMergedWifi_wrongSubId_isDefault() =
+ testScope.runTest {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged(
+ networkId = NET_ID,
+ subscriptionId = SUB_ID + 10,
+ level = 3,
+ )
+ )
+
+ assertThat(latest).isEqualTo(MobileConnectionModel())
+ assertThat(latest!!.primaryLevel).isNotEqualTo(3)
+ assertThat(latest!!.resolvedNetworkType)
+ .isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType)
+
+ job.cancel()
+ }
+
+ // This scenario likely isn't possible, but write a test for it anyway
+ @Test
+ fun connectionInfo_carrierMergedButNotEnabled_isDefault() =
+ testScope.runTest {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged(
+ networkId = NET_ID,
+ subscriptionId = SUB_ID,
+ level = 3,
+ )
+ )
+ wifiRepository.setIsWifiEnabled(false)
+
+ assertThat(latest).isEqualTo(MobileConnectionModel())
+
+ job.cancel()
+ }
+
+ // This scenario likely isn't possible, but write a test for it anyway
+ @Test
+ fun connectionInfo_carrierMergedButWifiNotDefault_isDefault() =
+ testScope.runTest {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged(
+ networkId = NET_ID,
+ subscriptionId = SUB_ID,
+ level = 3,
+ )
+ )
+ wifiRepository.setIsWifiDefault(false)
+
+ assertThat(latest).isEqualTo(MobileConnectionModel())
+
+ job.cancel()
+ }
+
+ @Test
+ fun numberOfLevels_comesFromCarrierMerged() =
+ testScope.runTest {
+ var latest: Int? = null
+ val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged(
+ networkId = NET_ID,
+ subscriptionId = SUB_ID,
+ level = 1,
+ numberOfLevels = 6,
+ )
+ )
+
+ assertThat(latest).isEqualTo(6)
+
+ job.cancel()
+ }
+
+ @Test
+ fun dataEnabled_matchesWifiEnabled() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setIsWifiEnabled(true)
+ assertThat(latest).isTrue()
+
+ wifiRepository.setIsWifiEnabled(false)
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun cdmaRoaming_alwaysFalse() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ private companion object {
+ const val SUB_ID = 123
+ const val NET_ID = 456
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
new file mode 100644
index 0000000..c02a4df
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
@@ -0,0 +1,389 @@
+/*
+ * 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 androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+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.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * This repo acts as a dispatcher to either the `typical` or `carrier merged` versions of the
+ * repository interface it's switching on. These tests just need to verify that the entire interface
+ * properly switches over when the value of `isCarrierMerged` changes.
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class FullMobileConnectionRepositoryTest : SysuiTestCase() {
+ private lateinit var underTest: FullMobileConnectionRepository
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+ private val mobileMappings = FakeMobileMappingsProxy()
+ private val tableLogBuffer = mock<TableLogBuffer>()
+ private val mobileFactory = mock<MobileConnectionRepositoryImpl.Factory>()
+ private val carrierMergedFactory = mock<CarrierMergedConnectionRepository.Factory>()
+
+ private lateinit var connectionsRepo: FakeMobileConnectionsRepository
+ private val globalMobileDataSettingChangedEvent: Flow<Unit>
+ get() = connectionsRepo.globalMobileDataSettingChangedEvent
+
+ private lateinit var mobileRepo: FakeMobileConnectionRepository
+ private lateinit var carrierMergedRepo: FakeMobileConnectionRepository
+
+ @Before
+ fun setUp() {
+ connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogBuffer)
+
+ mobileRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
+ carrierMergedRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
+
+ whenever(
+ mobileFactory.build(
+ eq(SUB_ID),
+ any(),
+ eq(DEFAULT_NAME),
+ eq(SEP),
+ eq(globalMobileDataSettingChangedEvent),
+ )
+ )
+ .thenReturn(mobileRepo)
+ whenever(carrierMergedFactory.build(eq(SUB_ID), any(), eq(DEFAULT_NAME)))
+ .thenReturn(carrierMergedRepo)
+ }
+
+ @Test
+ fun startingIsCarrierMerged_usesCarrierMergedInitially() =
+ testScope.runTest {
+ val carrierMergedConnectionInfo =
+ MobileConnectionModel(
+ operatorAlphaShort = "Carrier Merged Operator",
+ )
+ carrierMergedRepo.setConnectionInfo(carrierMergedConnectionInfo)
+
+ initializeRepo(startingIsCarrierMerged = true)
+
+ assertThat(underTest.activeRepo.value).isEqualTo(carrierMergedRepo)
+ assertThat(underTest.connectionInfo.value).isEqualTo(carrierMergedConnectionInfo)
+ verify(mobileFactory, never())
+ .build(
+ SUB_ID,
+ tableLogBuffer,
+ DEFAULT_NAME,
+ SEP,
+ globalMobileDataSettingChangedEvent
+ )
+ }
+
+ @Test
+ fun startingNotCarrierMerged_usesTypicalInitially() =
+ testScope.runTest {
+ val mobileConnectionInfo =
+ MobileConnectionModel(
+ operatorAlphaShort = "Typical Operator",
+ )
+ mobileRepo.setConnectionInfo(mobileConnectionInfo)
+
+ initializeRepo(startingIsCarrierMerged = false)
+
+ assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo)
+ assertThat(underTest.connectionInfo.value).isEqualTo(mobileConnectionInfo)
+ verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer, DEFAULT_NAME)
+ }
+
+ @Test
+ fun activeRepo_matchesIsCarrierMerged() =
+ testScope.runTest {
+ initializeRepo(startingIsCarrierMerged = false)
+ var latest: MobileConnectionRepository? = null
+ val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)
+
+ underTest.setIsCarrierMerged(true)
+
+ assertThat(latest).isEqualTo(carrierMergedRepo)
+
+ underTest.setIsCarrierMerged(false)
+
+ assertThat(latest).isEqualTo(mobileRepo)
+
+ underTest.setIsCarrierMerged(true)
+
+ assertThat(latest).isEqualTo(carrierMergedRepo)
+
+ job.cancel()
+ }
+
+ @Test
+ fun connectionInfo_getsUpdatesFromRepo_carrierMerged() =
+ testScope.runTest {
+ initializeRepo(startingIsCarrierMerged = false)
+
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ underTest.setIsCarrierMerged(true)
+
+ val info1 =
+ MobileConnectionModel(
+ operatorAlphaShort = "Carrier Merged Operator",
+ primaryLevel = 1,
+ )
+ carrierMergedRepo.setConnectionInfo(info1)
+
+ assertThat(latest).isEqualTo(info1)
+
+ val info2 =
+ MobileConnectionModel(
+ operatorAlphaShort = "Carrier Merged Operator #2",
+ primaryLevel = 2,
+ )
+ carrierMergedRepo.setConnectionInfo(info2)
+
+ assertThat(latest).isEqualTo(info2)
+
+ val info3 =
+ MobileConnectionModel(
+ operatorAlphaShort = "Carrier Merged Operator #3",
+ primaryLevel = 3,
+ )
+ carrierMergedRepo.setConnectionInfo(info3)
+
+ assertThat(latest).isEqualTo(info3)
+
+ job.cancel()
+ }
+
+ @Test
+ fun connectionInfo_getsUpdatesFromRepo_mobile() =
+ testScope.runTest {
+ initializeRepo(startingIsCarrierMerged = false)
+
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ underTest.setIsCarrierMerged(false)
+
+ val info1 =
+ MobileConnectionModel(
+ operatorAlphaShort = "Typical Merged Operator",
+ primaryLevel = 1,
+ )
+ mobileRepo.setConnectionInfo(info1)
+
+ assertThat(latest).isEqualTo(info1)
+
+ val info2 =
+ MobileConnectionModel(
+ operatorAlphaShort = "Typical Merged Operator #2",
+ primaryLevel = 2,
+ )
+ mobileRepo.setConnectionInfo(info2)
+
+ assertThat(latest).isEqualTo(info2)
+
+ val info3 =
+ MobileConnectionModel(
+ operatorAlphaShort = "Typical Merged Operator #3",
+ primaryLevel = 3,
+ )
+ mobileRepo.setConnectionInfo(info3)
+
+ assertThat(latest).isEqualTo(info3)
+
+ job.cancel()
+ }
+
+ @Test
+ fun connectionInfo_updatesWhenCarrierMergedUpdates() =
+ testScope.runTest {
+ initializeRepo(startingIsCarrierMerged = false)
+
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ val carrierMergedInfo =
+ MobileConnectionModel(
+ operatorAlphaShort = "Carrier Merged Operator",
+ primaryLevel = 4,
+ )
+ carrierMergedRepo.setConnectionInfo(carrierMergedInfo)
+
+ val mobileInfo =
+ MobileConnectionModel(
+ operatorAlphaShort = "Typical Operator",
+ primaryLevel = 2,
+ )
+ mobileRepo.setConnectionInfo(mobileInfo)
+
+ // Start with the mobile info
+ assertThat(latest).isEqualTo(mobileInfo)
+
+ // WHEN isCarrierMerged is set to true
+ underTest.setIsCarrierMerged(true)
+
+ // THEN the carrier merged info is used
+ assertThat(latest).isEqualTo(carrierMergedInfo)
+
+ val newCarrierMergedInfo =
+ MobileConnectionModel(
+ operatorAlphaShort = "New CM Operator",
+ primaryLevel = 0,
+ )
+ carrierMergedRepo.setConnectionInfo(newCarrierMergedInfo)
+
+ assertThat(latest).isEqualTo(newCarrierMergedInfo)
+
+ // WHEN isCarrierMerged is set to false
+ underTest.setIsCarrierMerged(false)
+
+ // THEN the typical info is used
+ assertThat(latest).isEqualTo(mobileInfo)
+
+ val newMobileInfo =
+ MobileConnectionModel(
+ operatorAlphaShort = "New Mobile Operator",
+ primaryLevel = 3,
+ )
+ mobileRepo.setConnectionInfo(newMobileInfo)
+
+ assertThat(latest).isEqualTo(newMobileInfo)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `factory - reuses log buffers for same connection`() =
+ testScope.runTest {
+ val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock())
+
+ val factory =
+ FullMobileConnectionRepository.Factory(
+ scope = testScope.backgroundScope,
+ realLoggerFactory,
+ mobileFactory,
+ carrierMergedFactory,
+ )
+
+ // Create two connections for the same subId. Similar to if the connection appeared
+ // and disappeared from the connectionFactory's perspective
+ val connection1 =
+ factory.build(
+ SUB_ID,
+ startingIsCarrierMerged = false,
+ DEFAULT_NAME,
+ SEP,
+ globalMobileDataSettingChangedEvent,
+ )
+
+ val connection1Repeat =
+ factory.build(
+ SUB_ID,
+ startingIsCarrierMerged = false,
+ DEFAULT_NAME,
+ SEP,
+ globalMobileDataSettingChangedEvent,
+ )
+
+ assertThat(connection1.tableLogBuffer)
+ .isSameInstanceAs(connection1Repeat.tableLogBuffer)
+ }
+
+ @Test
+ fun `factory - reuses log buffers for same sub ID even if carrier merged`() =
+ testScope.runTest {
+ val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock())
+
+ val factory =
+ FullMobileConnectionRepository.Factory(
+ scope = testScope.backgroundScope,
+ realLoggerFactory,
+ mobileFactory,
+ carrierMergedFactory,
+ )
+
+ val connection1 =
+ factory.build(
+ SUB_ID,
+ startingIsCarrierMerged = false,
+ DEFAULT_NAME,
+ SEP,
+ globalMobileDataSettingChangedEvent,
+ )
+
+ // WHEN a connection with the same sub ID but carrierMerged = true is created
+ val connection1Repeat =
+ factory.build(
+ SUB_ID,
+ startingIsCarrierMerged = true,
+ DEFAULT_NAME,
+ SEP,
+ globalMobileDataSettingChangedEvent,
+ )
+
+ // THEN the same table is re-used
+ assertThat(connection1.tableLogBuffer)
+ .isSameInstanceAs(connection1Repeat.tableLogBuffer)
+ }
+
+ // TODO(b/238425913): Verify that the logging switches correctly (once the carrier merged repo
+ // implements logging).
+
+ private fun initializeRepo(startingIsCarrierMerged: Boolean) {
+ underTest =
+ FullMobileConnectionRepository(
+ SUB_ID,
+ startingIsCarrierMerged,
+ tableLogBuffer,
+ DEFAULT_NAME,
+ SEP,
+ globalMobileDataSettingChangedEvent,
+ testScope.backgroundScope,
+ mobileFactory,
+ carrierMergedFactory,
+ )
+ }
+
+ private companion object {
+ const val SUB_ID = 42
+ 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/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 0958970..813b0ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -37,17 +37,18 @@
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
-import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
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.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
-import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -74,6 +75,9 @@
private lateinit var underTest: MobileConnectionsRepositoryImpl
private lateinit var connectionFactory: MobileConnectionRepositoryImpl.Factory
+ private lateinit var carrierMergedFactory: CarrierMergedConnectionRepository.Factory
+ private lateinit var fullConnectionFactory: FullMobileConnectionRepository.Factory
+ private lateinit var wifiRepository: FakeWifiRepository
@Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager
@@ -100,6 +104,8 @@
mock<TableLogBuffer>()
}
+ wifiRepository = FakeWifiRepository()
+
connectionFactory =
MobileConnectionRepositoryImpl.Factory(
fakeBroadcastDispatcher,
@@ -110,7 +116,18 @@
logger = logger,
mobileMappingsProxy = mobileMappings,
scope = scope,
+ )
+ carrierMergedFactory =
+ CarrierMergedConnectionRepository.Factory(
+ scope,
+ wifiRepository,
+ )
+ fullConnectionFactory =
+ FullMobileConnectionRepository.Factory(
+ scope = scope,
logFactory = logBufferFactory,
+ mobileRepoFactory = connectionFactory,
+ carrierMergedRepoFactory = carrierMergedFactory,
)
underTest =
@@ -125,7 +142,8 @@
context,
IMMEDIATE,
scope,
- connectionFactory,
+ wifiRepository,
+ fullConnectionFactory,
)
}
@@ -180,6 +198,40 @@
}
@Test
+ fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() =
+ runBlocking(IMMEDIATE) {
+ var latest: List<SubscriptionModel>? = null
+
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).isEqualTo(listOf(MODEL_CM))
+
+ job.cancel()
+ }
+
+ @Test
+ fun testSubscriptions_carrierMergedAndOther_listHasBothWithCarrierMergedLast() =
+ runBlocking(IMMEDIATE) {
+ var latest: List<SubscriptionModel>? = null
+
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_2, SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2, MODEL_CM))
+
+ job.cancel()
+ }
+
+ @Test
fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
runBlocking(IMMEDIATE) {
assertThat(underTest.activeMobileDataSubscriptionId.value)
@@ -219,6 +271,96 @@
}
@Test
+ fun testConnectionRepository_carrierMergedSubId_isCached() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ val repo1 = underTest.getRepoForSubId(SUB_CM_ID)
+ val repo2 = underTest.getRepoForSubId(SUB_CM_ID)
+
+ assertThat(repo1).isSameInstanceAs(repo2)
+
+ job.cancel()
+ }
+
+ @Test
+ fun testConnectionRepository_carrierMergedAndMobileSubs_usesCorrectRepos() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+ val mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+ assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
+ assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun testSubscriptions_subNoLongerCarrierMerged_repoUpdates() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+ var mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+ assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
+ assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+ // WHEN the wifi network updates to be not carrier merged
+ wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 4, level = 1))
+
+ // THEN the repos update
+ val noLongerCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+ mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+ assertThat(noLongerCarrierMergedRepo.getIsCarrierMerged()).isFalse()
+ assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun testSubscriptions_subBecomesCarrierMerged_repoUpdates() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ val notYetCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+ var mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+ assertThat(notYetCarrierMergedRepo.getIsCarrierMerged()).isFalse()
+ assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+ // WHEN the wifi network updates to be carrier merged
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+
+ // THEN the repos update
+ val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+ mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+ assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
+ assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
fun testConnectionCache_clearsInvalidSubscriptions() =
runBlocking(IMMEDIATE) {
val job = underTest.subscriptions.launchIn(this)
@@ -244,6 +386,34 @@
job.cancel()
}
+ @Test
+ fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_2, SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ // Get repos to trigger caching
+ val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+ val repo2 = underTest.getRepoForSubId(SUB_2_ID)
+ val repoCarrierMerged = underTest.getRepoForSubId(SUB_CM_ID)
+
+ assertThat(underTest.getSubIdRepoCache())
+ .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2, SUB_CM_ID, repoCarrierMerged)
+
+ // SUB_2 and SUB_CM disappear
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1)
+
+ job.cancel()
+ }
+
/** Regression test for b/261706421 */
@Test
fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() =
@@ -295,13 +465,13 @@
underTest.getRepoForSubId(SUB_1_ID)
verify(logBufferFactory)
.getOrCreate(
- eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)),
+ eq(tableBufferLogName(SUB_1_ID)),
anyInt(),
)
underTest.getRepoForSubId(SUB_2_ID)
verify(logBufferFactory)
.getOrCreate(
- eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)),
+ eq(tableBufferLogName(SUB_2_ID)),
anyInt(),
)
@@ -309,46 +479,6 @@
}
@Test
- fun `connection repository factory - reuses log buffers for same connection`() =
- runBlocking(IMMEDIATE) {
- val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock())
-
- connectionFactory =
- MobileConnectionRepositoryImpl.Factory(
- fakeBroadcastDispatcher,
- context = context,
- telephonyManager = telephonyManager,
- bgDispatcher = IMMEDIATE,
- globalSettings = globalSettings,
- logger = logger,
- mobileMappingsProxy = mobileMappings,
- scope = scope,
- logFactory = realLoggerFactory,
- )
-
- // Create two connections for the same subId. Similar to if the connection appeared
- // and disappeared from the connectionFactory's perspective
- val connection1 =
- connectionFactory.build(
- 1,
- NetworkNameModel.Default("default_name"),
- "-",
- underTest.globalMobileDataSettingChangedEvent,
- )
-
- val connection1_repeat =
- connectionFactory.build(
- 1,
- NetworkNameModel.Default("default_name"),
- "-",
- underTest.globalMobileDataSettingChangedEvent,
- )
-
- assertThat(connection1.tableLogBuffer)
- .isSameInstanceAs(connection1_repeat.tableLogBuffer)
- }
-
- @Test
fun mobileConnectivity_default() {
assertThat(underTest.defaultMobileNetworkConnectivity.value)
.isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false))
@@ -461,7 +591,8 @@
context,
IMMEDIATE,
scope,
- connectionFactory,
+ wifiRepository,
+ fullConnectionFactory,
)
var latest: MobileMappings.Config? = null
@@ -571,5 +702,16 @@
private const val NET_ID = 123
private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) }
+
+ private const val SUB_CM_ID = 5
+ private val SUB_CM =
+ mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_CM_ID) }
+ private val MODEL_CM = SubscriptionModel(subscriptionId = SUB_CM_ID)
+ private val WIFI_NETWORK_CM =
+ WifiNetworkModel.CarrierMerged(
+ networkId = 3,
+ subscriptionId = SUB_CM_ID,
+ level = 1,
+ )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 61e13b8..e6be7f1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -25,6 +25,7 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
@@ -271,6 +272,23 @@
}
@Test
+ fun iconGroup_carrierMerged_usesOverride() =
+ runBlocking(IMMEDIATE) {
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ resolvedNetworkType = CarrierMergedNetworkType,
+ ),
+ )
+
+ var latest: MobileIconGroup? = null
+ val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(CarrierMergedNetworkType.iconGroupOverride)
+
+ job.cancel()
+ }
+
+ @Test
fun alwaysShowDataRatIcon_matchesParent() =
runBlocking(IMMEDIATE) {
var latest: Boolean? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
index 30ac8d4..824cebd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
@@ -16,11 +16,12 @@
package com.android.systemui.statusbar.pipeline.wifi.data.model
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.table.TableRowLogger
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MIN_VALID_LEVEL
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Companion.MIN_VALID_LEVEL
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -44,9 +45,53 @@
WifiNetworkModel.Active(NETWORK_ID, level = MAX_VALID_LEVEL + 1)
}
+ @Test(expected = IllegalArgumentException::class)
+ fun carrierMerged_invalidSubId_exceptionThrown() {
+ WifiNetworkModel.CarrierMerged(NETWORK_ID, INVALID_SUBSCRIPTION_ID, 1)
+ }
+
// Non-exhaustive logDiffs test -- just want to make sure the logging logic isn't totally broken
@Test
+ fun logDiffs_carrierMergedToInactive_resetsAllFields() {
+ val logger = TestLogger()
+ val prevVal =
+ WifiNetworkModel.CarrierMerged(
+ networkId = 5,
+ subscriptionId = 3,
+ level = 1,
+ )
+
+ WifiNetworkModel.Inactive.logDiffs(prevVal, logger)
+
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_INACTIVE))
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString()))
+ assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
+ assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
+ assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
+ }
+
+ @Test
+ fun logDiffs_inactiveToCarrierMerged_logsAllFields() {
+ val logger = TestLogger()
+ val carrierMerged =
+ WifiNetworkModel.CarrierMerged(
+ networkId = 6,
+ subscriptionId = 3,
+ level = 2,
+ )
+
+ carrierMerged.logDiffs(prevVal = WifiNetworkModel.Inactive, logger)
+
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED))
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6"))
+ assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3"))
+ assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
+ assertThat(logger.changes).contains(Pair(COL_LEVEL, "2"))
+ assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
+ }
+
+ @Test
fun logDiffs_inactiveToActive_logsAllActiveFields() {
val logger = TestLogger()
val activeNetwork =
@@ -95,8 +140,14 @@
level = 3,
ssid = "Test SSID"
)
+ val prevVal =
+ WifiNetworkModel.CarrierMerged(
+ networkId = 5,
+ subscriptionId = 3,
+ level = 1,
+ )
- activeNetwork.logDiffs(prevVal = WifiNetworkModel.CarrierMerged, logger)
+ activeNetwork.logDiffs(prevVal, logger)
assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_ACTIVE))
assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "5"))
@@ -105,7 +156,7 @@
assertThat(logger.changes).contains(Pair(COL_SSID, "Test SSID"))
}
@Test
- fun logDiffs_activeToCarrierMerged_resetsAllActiveFields() {
+ fun logDiffs_activeToCarrierMerged_logsAllFields() {
val logger = TestLogger()
val activeNetwork =
WifiNetworkModel.Active(
@@ -114,13 +165,20 @@
level = 3,
ssid = "Test SSID"
)
+ val carrierMerged =
+ WifiNetworkModel.CarrierMerged(
+ networkId = 6,
+ subscriptionId = 3,
+ level = 2,
+ )
- WifiNetworkModel.CarrierMerged.logDiffs(prevVal = activeNetwork, logger)
+ carrierMerged.logDiffs(prevVal = activeNetwork, logger)
assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED))
- assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString()))
- assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
- assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6"))
+ assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3"))
+ assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
+ assertThat(logger.changes).contains(Pair(COL_LEVEL, "2"))
assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index 8f07615..87ce8fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -26,6 +26,7 @@
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.TrafficStateCallback
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -340,7 +341,6 @@
.launchIn(this)
val wifiInfo = mock<WifiInfo>().apply {
- whenever(this.ssid).thenReturn(SSID)
whenever(this.isPrimary).thenReturn(true)
whenever(this.isCarrierMerged).thenReturn(true)
}
@@ -353,6 +353,67 @@
}
@Test
+ fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() =
+ runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ val wifiInfo = mock<WifiInfo>().apply {
+ whenever(this.isPrimary).thenReturn(true)
+ whenever(this.isCarrierMerged).thenReturn(true)
+ whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID)
+ }
+
+ getNetworkCallback().onCapabilitiesChanged(
+ NETWORK,
+ createWifiNetworkCapabilities(wifiInfo),
+ )
+
+ assertThat(latest).isInstanceOf(WifiNetworkModel.Invalid::class.java)
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_isCarrierMerged_getsCorrectValues() =
+ runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ val rssi = -57
+ val wifiInfo = mock<WifiInfo>().apply {
+ whenever(this.isPrimary).thenReturn(true)
+ whenever(this.isCarrierMerged).thenReturn(true)
+ whenever(this.rssi).thenReturn(rssi)
+ whenever(this.subscriptionId).thenReturn(567)
+ }
+
+ whenever(wifiManager.calculateSignalLevel(rssi)).thenReturn(2)
+ whenever(wifiManager.maxSignalLevel).thenReturn(5)
+
+ getNetworkCallback().onCapabilitiesChanged(
+ NETWORK,
+ createWifiNetworkCapabilities(wifiInfo),
+ )
+
+ assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
+ val latestCarrierMerged = latest as WifiNetworkModel.CarrierMerged
+ assertThat(latestCarrierMerged.networkId).isEqualTo(NETWORK_ID)
+ assertThat(latestCarrierMerged.subscriptionId).isEqualTo(567)
+ assertThat(latestCarrierMerged.level).isEqualTo(2)
+ // numberOfLevels = maxSignalLevel + 1
+ assertThat(latestCarrierMerged.numberOfLevels).isEqualTo(6)
+
+ job.cancel()
+ }
+
+ @Test
fun wifiNetwork_notValidated_networkNotValidated() = runBlocking(IMMEDIATE) {
var latest: WifiNetworkModel? = null
val job = underTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
index 01d59f9..089a170 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
@@ -84,7 +84,9 @@
@Test
fun ssid_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) {
- wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
+ wifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged(networkId = 1, subscriptionId = 2, level = 1)
+ )
var latest: String? = "default"
val job = underTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index 726e813..b932837 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -206,7 +206,8 @@
// Enabled = false => no networks shown
TestCase(
enabled = false,
- network = WifiNetworkModel.CarrierMerged,
+ network =
+ WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1),
expected = null,
),
TestCase(
@@ -228,7 +229,8 @@
// forceHidden = true => no networks shown
TestCase(
forceHidden = true,
- network = WifiNetworkModel.CarrierMerged,
+ network =
+ WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1),
expected = null,
),
TestCase(
@@ -369,7 +371,8 @@
// network = CarrierMerged => not shown
TestCase(
- network = WifiNetworkModel.CarrierMerged,
+ network =
+ WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1),
expected = null,
),
diff --git a/core/java/android/nfc/BeamShareData.aidl b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt
similarity index 63%
rename from core/java/android/nfc/BeamShareData.aidl
rename to packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt
index a47e240..7e01088 100644
--- a/core/java/android/nfc/BeamShareData.aidl
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 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.
@@ -14,6 +14,12 @@
* limitations under the License.
*/
-package android.nfc;
+package com.android.systemui.stylus
-parcelable BeamShareData;
+import android.hardware.BatteryState
+
+class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() {
+ override fun getCapacity() = capacity
+ override fun getStatus() = 0
+ override fun isPresent() = true
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt
index 6d6e40a..a08e002 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt
@@ -30,7 +30,6 @@
import com.android.systemui.util.mockito.whenever
import java.util.concurrent.Executor
import org.junit.Before
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
@@ -82,8 +81,8 @@
whenever(stylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
- // whenever(stylusDevice.bluetoothAddress).thenReturn(null)
- // whenever(btStylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
+ whenever(stylusDevice.bluetoothAddress).thenReturn(null)
+ whenever(btStylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice)
whenever(inputManager.getInputDevice(STYLUS_DEVICE_ID)).thenReturn(stylusDevice)
@@ -170,7 +169,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onInputDeviceAdded_btStylus_firstUsed_callsCallbacksOnStylusFirstUsed() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -178,7 +176,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onInputDeviceAdded_btStylus_firstUsed_setsFlag() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -186,7 +183,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onInputDeviceAdded_btStylus_callsCallbacksWithAddress() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -215,10 +211,9 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_multipleRegisteredCallbacks_callsAll() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
- // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
+ whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
stylusManager.registerCallback(otherStylusCallback)
stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID)
@@ -230,10 +225,9 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_stylusNewBtConnection_callsCallbacks() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
- // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
+ whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID)
@@ -242,10 +236,9 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_stylusLostBtConnection_callsCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
- // whenever(btStylusDevice.bluetoothAddress).thenReturn(null)
+ whenever(btStylusDevice.bluetoothAddress).thenReturn(null)
stylusManager.onInputDeviceChanged(BT_STYLUS_DEVICE_ID)
@@ -254,7 +247,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_btConnection_stylusAlreadyBtConnected_onlyCallsListenersOnce() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -265,7 +257,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_noBtConnection_stylusNeverBtConnected_doesNotCallCallbacks() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
@@ -317,7 +308,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onInputDeviceRemoved_btStylus_callsCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -331,7 +321,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onStylusBluetoothConnected_registersMetadataListener() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -339,7 +328,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onStylusBluetoothConnected_noBluetoothDevice_doesNotRegisterMetadataListener() {
whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(null)
@@ -349,7 +337,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onStylusBluetoothDisconnected_unregistersMetadataListener() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -359,7 +346,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
stylusManager.registerBatteryCallback(otherStylusBatteryCallback)
@@ -377,7 +363,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -392,7 +377,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_chargingStateFalse_executesBatteryCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -407,7 +391,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_chargingStateNoDevice_doesNotExecuteBatteryCallbacks() {
stylusManager.onMetadataChanged(
bluetoothDevice,
@@ -419,7 +402,6 @@
}
@Test
- @Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_notChargingState_doesNotExecuteBatteryCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -434,7 +416,6 @@
}
@Test
- @Ignore("TODO(b/261826950): remove on main")
fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_updateEverUsedFlag() {
whenever(batteryState.isPresent).thenReturn(true)
@@ -444,7 +425,6 @@
}
@Test
- @Ignore("TODO(b/261826950): remove on main")
fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_executesStylusFirstUsed() {
whenever(batteryState.isPresent).thenReturn(true)
@@ -454,7 +434,6 @@
}
@Test
- @Ignore("TODO(b/261826950): remove on main")
fun onBatteryStateChanged_batteryPresent_stylusUsed_doesNotUpdateEverUsedFlag() {
whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(true)
whenever(batteryState.isPresent).thenReturn(true)
@@ -465,7 +444,6 @@
}
@Test
- @Ignore("TODO(b/261826950): remove on main")
fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateEverUsedFlag() {
whenever(batteryState.isPresent).thenReturn(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt
index 117e00d..1cccd65c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt
@@ -85,6 +85,13 @@
}
@Test
+ fun start_initStylusUsiPowerUi() {
+ startable.start()
+
+ verify(stylusUsiPowerUi, times(1)).init()
+ }
+
+ @Test
fun onStylusBluetoothConnected_refreshesNotification() {
startable.onStylusBluetoothConnected(STYLUS_DEVICE_ID, "ANY")
@@ -99,13 +106,21 @@
}
@Test
- fun onStylusUsiBatteryStateChanged_batteryPresent_refreshesNotification() {
- val batteryState = mock(BatteryState::class.java)
- whenever(batteryState.isPresent).thenReturn(true)
+ fun onStylusUsiBatteryStateChanged_batteryPresentValidCapacity_refreshesNotification() {
+ val batteryState = FixedCapacityBatteryState(0.1f)
startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
- verify(stylusUsiPowerUi, times(1)).updateBatteryState(batteryState)
+ verify(stylusUsiPowerUi, times(1)).updateBatteryState(STYLUS_DEVICE_ID, batteryState)
+ }
+
+ @Test
+ fun onStylusUsiBatteryStateChanged_batteryPresentInvalidCapacity_noop() {
+ val batteryState = FixedCapacityBatteryState(0f)
+
+ startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
+
+ verifyNoMoreInteractions(stylusUsiPowerUi)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
index a7951f4..1e81dc7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
@@ -17,8 +17,11 @@
package com.android.systemui.stylus
import android.app.Notification
-import android.hardware.BatteryState
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
import android.hardware.input.InputManager
+import android.os.Bundle
import android.os.Handler
import android.testing.AndroidTestingRunner
import android.view.InputDevice
@@ -27,8 +30,10 @@
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
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.whenever
+import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertEquals
import org.junit.Before
import org.junit.Ignore
@@ -37,7 +42,10 @@
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.doNothing
import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
@@ -53,11 +61,16 @@
@Captor lateinit var notificationCaptor: ArgumentCaptor<Notification>
private lateinit var stylusUsiPowerUi: StylusUsiPowerUI
+ private lateinit var broadcastReceiver: BroadcastReceiver
+ private lateinit var contextSpy: Context
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ contextSpy = spy(mContext)
+ doNothing().whenever(contextSpy).startActivity(any())
+
whenever(handler.post(any())).thenAnswer {
(it.arguments[0] as Runnable).run()
true
@@ -68,12 +81,20 @@
whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
// whenever(btStylusDevice.bluetoothAddress).thenReturn("SO:ME:AD:DR:ES")
- stylusUsiPowerUi = StylusUsiPowerUI(mContext, notificationManager, inputManager, handler)
+ stylusUsiPowerUi = StylusUsiPowerUI(contextSpy, notificationManager, inputManager, handler)
+ broadcastReceiver = stylusUsiPowerUi.receiver
+ }
+
+ @Test
+ fun updateBatteryState_capacityZero_noop() {
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0f))
+
+ verifyNoMoreInteractions(notificationManager)
}
@Test
fun updateBatteryState_capacityBelowThreshold_notifies() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
verify(notificationManager, times(1))
.notify(eq(R.string.stylus_battery_low_percentage), any())
@@ -82,7 +103,7 @@
@Test
fun updateBatteryState_capacityAboveThreshold_cancelsNotificattion() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f))
verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage)
verifyNoMoreInteractions(notificationManager)
@@ -90,8 +111,8 @@
@Test
fun updateBatteryState_existingNotification_capacityAboveThreshold_cancelsNotification() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f))
inOrder(notificationManager).let {
it.verify(notificationManager, times(1))
@@ -103,8 +124,8 @@
@Test
fun updateBatteryState_existingNotification_capacityBelowThreshold_updatesNotification() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.15f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.15f))
verify(notificationManager, times(2))
.notify(eq(R.string.stylus_battery_low_percentage), notificationCaptor.capture())
@@ -121,9 +142,9 @@
@Test
fun updateBatteryState_capacityAboveThenBelowThreshold_hidesThenShowsNotification() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.5f))
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.5f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
inOrder(notificationManager).let {
it.verify(notificationManager, times(1))
@@ -145,7 +166,7 @@
@Test
fun updateSuppression_existingNotification_cancelsNotification() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
stylusUsiPowerUi.updateSuppression(true)
@@ -159,18 +180,7 @@
@Test
@Ignore("TODO(b/257936830): get bt address once input api available")
- fun refresh_hasConnectedBluetoothStylus_doesNotNotify() {
- whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0))
-
- stylusUsiPowerUi.refresh()
-
- verifyNoMoreInteractions(notificationManager)
- }
-
- @Test
- @Ignore("TODO(b/257936830): get bt address once input api available")
- fun refresh_hasConnectedBluetoothStylus_existingNotification_cancelsNotification() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+ fun refresh_hasConnectedBluetoothStylus_cancelsNotification() {
whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0))
stylusUsiPowerUi.refresh()
@@ -178,9 +188,38 @@
verify(notificationManager).cancel(R.string.stylus_battery_low_percentage)
}
- class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() {
- override fun getCapacity() = capacity
- override fun getStatus() = 0
- override fun isPresent() = true
+ @Test
+ @Ignore("TODO(b/257936830): get bt address once input api available")
+ fun refresh_hasConnectedBluetoothStylus_existingNotification_cancelsNotification() {
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+ whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0))
+
+ stylusUsiPowerUi.refresh()
+
+ verify(notificationManager).cancel(R.string.stylus_battery_low_percentage)
+ }
+
+ @Test
+ fun broadcastReceiver_clicked_hasInputDeviceId_startsUsiDetailsActivity() {
+ val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY)
+ val activityIntentCaptor = argumentCaptor<Intent>()
+ stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.15f))
+ broadcastReceiver.onReceive(contextSpy, intent)
+
+ verify(contextSpy, times(1)).startActivity(activityIntentCaptor.capture())
+ assertThat(activityIntentCaptor.value.action)
+ .isEqualTo(StylusUsiPowerUI.ACTION_STYLUS_USI_DETAILS)
+ val args =
+ activityIntentCaptor.value.getExtra(StylusUsiPowerUI.KEY_SETTINGS_FRAGMENT_ARGS)
+ as Bundle
+ assertThat(args.getInt(StylusUsiPowerUI.KEY_DEVICE_INPUT_ID)).isEqualTo(1)
+ }
+
+ @Test
+ fun broadcastReceiver_clicked_nullInputDeviceId_doesNotStartActivity() {
+ val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY)
+ broadcastReceiver.onReceive(contextSpy, intent)
+
+ verify(contextSpy, never()).startActivity(any())
}
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 010189a..b28ab7a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -3248,7 +3248,7 @@
if (targetName.equals(MAGNIFICATION_CONTROLLER_NAME)) {
final boolean enabled =
!getMagnificationController().getFullScreenMagnificationController()
- .isMagnifying(displayId);
+ .isActivated(displayId);
logAccessibilityShortcutActivated(mContext, MAGNIFICATION_COMPONENT_NAME, shortcutType,
enabled);
sendAccessibilityButtonToInputFilter(displayId);
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
index 6cfbfb8..de7184c 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -126,8 +126,6 @@
private boolean mUnregisterPending;
private boolean mDeleteAfterUnregister;
- private boolean mForceShowMagnifiableBounds;
-
private final int mDisplayId;
private int mIdOfLastServiceToMagnify = INVALID_SERVICE_ID;
@@ -213,8 +211,8 @@
return mRegistered;
}
- boolean isMagnifying() {
- return mCurrentMagnificationSpec.scale > 1.0f;
+ boolean isActivated() {
+ return mMagnificationActivated;
}
float getScale() {
@@ -370,12 +368,6 @@
@GuardedBy("mLock")
void onMagnificationChangedLocked() {
final float scale = getScale();
- final boolean lastMagnificationActivated = mMagnificationActivated;
- mMagnificationActivated = scale > 1.0f;
- if (mMagnificationActivated != lastMagnificationActivated) {
- mMagnificationInfoChangedCallback.onFullScreenMagnificationActivationState(
- mDisplayId, mMagnificationActivated);
- }
final MagnificationConfig config = new MagnificationConfig.Builder()
.setMode(MAGNIFICATION_MODE_FULLSCREEN)
@@ -384,7 +376,7 @@
.setCenterY(getCenterY()).build();
mMagnificationInfoChangedCallback.onFullScreenMagnificationChanged(mDisplayId,
mMagnificationRegion, config);
- if (mUnregisterPending && !isMagnifying()) {
+ if (mUnregisterPending && !isActivated()) {
unregister(mDeleteAfterUnregister);
}
}
@@ -476,21 +468,22 @@
}
@GuardedBy("mLock")
- void setForceShowMagnifiableBounds(boolean show) {
- if (mRegistered) {
- mForceShowMagnifiableBounds = show;
- if (traceEnabled()) {
- logTrace("setForceShowMagnifiableBounds",
- "displayID=" + mDisplayId + ";show=" + show);
- }
- mControllerCtx.getWindowManager().setForceShowMagnifiableBounds(
- mDisplayId, show);
+ private boolean setActivated(boolean activated) {
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "setActivated(activated = " + activated + ")");
}
- }
- @GuardedBy("mLock")
- boolean isForceShowMagnifiableBounds() {
- return mRegistered && mForceShowMagnifiableBounds;
+ final boolean changed = (mMagnificationActivated != activated);
+
+ if (changed) {
+ mMagnificationActivated = activated;
+ mMagnificationInfoChangedCallback.onFullScreenMagnificationActivationState(
+ mDisplayId, mMagnificationActivated);
+ mControllerCtx.getWindowManager().setForceShowMagnifiableBounds(
+ mDisplayId, activated);
+ }
+
+ return changed;
}
@GuardedBy("mLock")
@@ -504,13 +497,13 @@
return false;
}
final MagnificationSpec spec = mCurrentMagnificationSpec;
- final boolean changed = !spec.isNop();
+ final boolean changed = isActivated();
+ setActivated(false);
if (changed) {
spec.clear();
onMagnificationChangedLocked();
}
mIdOfLastServiceToMagnify = INVALID_SERVICE_ID;
- mForceShowMagnifiableBounds = false;
sendSpecToAnimation(spec, animationCallback);
return changed;
}
@@ -554,9 +547,10 @@
+ ", centerY = " + centerY + ", endCallback = "
+ animationCallback + ", id = " + id + ")");
}
- final boolean changed = updateMagnificationSpecLocked(scale, centerX, centerY);
+ boolean changed = setActivated(true);
+ changed |= updateMagnificationSpecLocked(scale, centerX, centerY);
sendSpecToAnimation(mCurrentMagnificationSpec, animationCallback);
- if (isMagnifying() && (id != INVALID_SERVICE_ID)) {
+ if (isActivated() && (id != INVALID_SERVICE_ID)) {
mIdOfLastServiceToMagnify = id;
mMagnificationInfoChangedCallback.onRequestMagnificationSpec(mDisplayId,
mIdOfLastServiceToMagnify);
@@ -779,7 +773,7 @@
if (display == null) {
return;
}
- if (!display.isMagnifying()) {
+ if (!display.isActivated()) {
return;
}
final Rect magnifiedRegionBounds = mTempRect;
@@ -831,16 +825,16 @@
/**
* @param displayId The logical display id.
- * @return {@code true} if magnification is active, e.g. the scale
- * is > 1, {@code false} otherwise
+ * @return {@code true} if magnification is activated,
+ * {@code false} otherwise
*/
- public boolean isMagnifying(int displayId) {
+ public boolean isActivated(int displayId) {
synchronized (mLock) {
final DisplayMagnification display = mDisplays.get(displayId);
if (display == null) {
return false;
}
- return display.isMagnifying();
+ return display.isActivated();
}
}
@@ -1166,6 +1160,9 @@
*/
public void persistScale(int displayId) {
final float scale = getScale(Display.DEFAULT_DISPLAY);
+ if (scale < 2.0f) {
+ return;
+ }
mScaleProvider.putScale(scale, displayId);
}
@@ -1177,7 +1174,8 @@
* scale if none is available
*/
public float getPersistedScale(int displayId) {
- return mScaleProvider.getScale(displayId);
+ return MathUtils.constrain(mScaleProvider.getScale(displayId),
+ 2.0f, MagnificationScaleProvider.MAX_SCALE);
}
/**
@@ -1198,12 +1196,12 @@
*
* @param displayId The logical display id.
* @param animate whether the animate the transition
- * @return whether was {@link #isMagnifying(int) magnifying}
+ * @return whether was {@link #isActivated(int)} activated}
*/
boolean resetIfNeeded(int displayId, boolean animate) {
synchronized (mLock) {
final DisplayMagnification display = mDisplays.get(displayId);
- if (display == null || !display.isMagnifying()) {
+ if (display == null || !display.isActivated()) {
return false;
}
display.reset(animate);
@@ -1221,7 +1219,7 @@
boolean resetIfNeeded(int displayId, int connectionId) {
synchronized (mLock) {
final DisplayMagnification display = mDisplays.get(displayId);
- if (display == null || !display.isMagnifying()
+ if (display == null || !display.isActivated()
|| connectionId != display.getIdOfLastServiceToMagnify()) {
return false;
}
@@ -1230,16 +1228,6 @@
}
}
- void setForceShowMagnifiableBounds(int displayId, boolean show) {
- synchronized (mLock) {
- final DisplayMagnification display = mDisplays.get(displayId);
- if (display == null) {
- return;
- }
- display.setForceShowMagnifiableBounds(show);
- }
- }
-
/**
* Notifies that the IME window visibility changed.
*
@@ -1251,21 +1239,6 @@
mMagnificationInfoChangedCallback.onImeWindowVisibilityChanged(displayId, shown);
}
- /**
- * Returns {@code true} if the magnifiable regions of the display is forced to be shown.
- *
- * @param displayId The logical display id.
- */
- public boolean isForceShowMagnifiableBounds(int displayId) {
- synchronized (mLock) {
- final DisplayMagnification display = mDisplays.get(displayId);
- if (display == null) {
- return false;
- }
- return display.isForceShowMagnifiableBounds();
- }
- }
-
private void onScreenTurnedOff() {
final Message m = PooledLambda.obtainMessage(
FullScreenMagnificationController::resetAllIfNeeded, this, false);
@@ -1295,7 +1268,7 @@
}
return;
}
- if (!display.isMagnifying()) {
+ if (!display.isActivated()) {
display.unregister(delete);
} else {
display.unregisterPending(delete);
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index dc39b01..6bf37a1 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -122,7 +122,7 @@
// The MIN_SCALE is different from MagnificationScaleProvider.MIN_SCALE due
// to AccessibilityService.MagnificationController#setScale() has
// different scale range
- private static final float MIN_SCALE = 2.0f;
+ private static final float MIN_SCALE = 1.0f;
private static final float MAX_SCALE = MagnificationScaleProvider.MAX_SCALE;
@VisibleForTesting final FullScreenMagnificationController mFullScreenMagnificationController;
@@ -220,14 +220,19 @@
@Override
public void handleShortcutTriggered() {
- boolean wasMagnifying = mFullScreenMagnificationController.resetIfNeeded(mDisplayId,
- /* animate */ true);
- if (wasMagnifying) {
+ final boolean isActivated = mFullScreenMagnificationController.isActivated(mDisplayId);
+
+ if (isActivated) {
+ zoomOff();
clearAndTransitionToStateDetecting();
} else {
- mPromptController.showNotificationIfNeeded();
mDetectingState.toggleShortcutTriggered();
}
+
+ if (mDetectingState.isShortcutTriggered()) {
+ mPromptController.showNotificationIfNeeded();
+ zoomToScale(1.0f, Float.NaN, Float.NaN);
+ }
}
@Override
@@ -441,7 +446,12 @@
final class ViewportDraggingState implements State {
/** Whether to disable zoom after dragging ends */
- boolean mZoomedInBeforeDrag;
+ @VisibleForTesting boolean mActivatedBeforeDrag;
+ /** Whether to restore scale after dragging ends */
+ private boolean mZoomedInTemporary;
+ /** The cached scale for recovering after dragging ends */
+ private float mScaleBeforeZoomedInTemporary;
+
private boolean mLastMoveOutsideMagnifiedRegion;
@Override
@@ -474,7 +484,13 @@
case ACTION_UP:
case ACTION_CANCEL: {
- if (!mZoomedInBeforeDrag) zoomOff();
+ if (mActivatedBeforeDrag) {
+ if (mZoomedInTemporary) {
+ zoomToScale(mScaleBeforeZoomedInTemporary, event.getX(), event.getY());
+ }
+ } else {
+ zoomOff();
+ }
clear();
transitionTo(mDetectingState);
}
@@ -488,15 +504,27 @@
}
}
+ public void prepareForZoomInTemporary() {
+ mViewportDraggingState.mActivatedBeforeDrag =
+ mFullScreenMagnificationController.isActivated(mDisplayId);
+
+ mViewportDraggingState.mZoomedInTemporary = true;
+ mViewportDraggingState.mScaleBeforeZoomedInTemporary =
+ mFullScreenMagnificationController.getScale(mDisplayId);
+ }
+
@Override
public void clear() {
mLastMoveOutsideMagnifiedRegion = false;
+
+ mZoomedInTemporary = false;
+ mScaleBeforeZoomedInTemporary = 1.0f;
}
@Override
public String toString() {
return "ViewportDraggingState{"
- + "mZoomedInBeforeDrag=" + mZoomedInBeforeDrag
+ + "mActivatedBeforeDrag=" + mActivatedBeforeDrag
+ ", mLastMoveOutsideMagnifiedRegion=" + mLastMoveOutsideMagnifiedRegion
+ '}';
}
@@ -625,10 +653,10 @@
transitionToDelegatingStateAndClear();
} else if (mDetectTripleTap
- // If magnified, delay an ACTION_DOWN for mMultiTapMaxDelay
+ // If activated, delay an ACTION_DOWN for mMultiTapMaxDelay
// to ensure reachability of
// STATE_PANNING_SCALING(triggerable with ACTION_POINTER_DOWN)
- || mFullScreenMagnificationController.isMagnifying(mDisplayId)) {
+ || isActivated()) {
afterMultiTapTimeoutTransitionToDelegatingState();
@@ -640,8 +668,7 @@
}
break;
case ACTION_POINTER_DOWN: {
- if (mFullScreenMagnificationController.isMagnifying(mDisplayId)
- && event.getPointerCount() == 2) {
+ if (isActivated() && event.getPointerCount() == 2) {
storeSecondPointerDownLocation(event);
mHandler.sendEmptyMessageDelayed(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE,
ViewConfiguration.getTapTimeout());
@@ -665,13 +692,13 @@
// (which is a rare combo to be used aside from magnification)
if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) {
transitionToViewportDraggingStateAndClear(event);
- } else if (isMagnifying() && event.getPointerCount() == 2) {
+ } else if (isActivated() && event.getPointerCount() == 2) {
//Primary pointer is swiping, so transit to PanningScalingState
transitToPanningScalingStateAndClear();
} else {
transitionToDelegatingStateAndClear();
}
- } else if (isMagnifying() && secondPointerDownValid()
+ } else if (isActivated() && secondPointerDownValid()
&& distanceClosestPointerToPoint(
mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) {
//Second pointer is swiping, so transit to PanningScalingState
@@ -734,7 +761,7 @@
// Only log the triple tap event, use numTaps to filter.
if (multitapTriggered && numTaps > 2) {
- final boolean enabled = mFullScreenMagnificationController.isMagnifying(mDisplayId);
+ final boolean enabled = isActivated();
logMagnificationTripleTap(enabled);
}
return multitapTriggered;
@@ -862,24 +889,33 @@
mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
}
+ /**
+ * This method could be triggered by both 2 cases.
+ * 1. direct three tap gesture
+ * 2. one tap while shortcut triggered (it counts as two taps).
+ */
private void onTripleTap(MotionEvent up) {
if (DEBUG_DETECTING) {
Slog.i(mLogTag, "onTripleTap(); delayed: "
+ MotionEventInfo.toString(mDelayedEventQueue));
}
- clear();
- // Toggle zoom
- if (mFullScreenMagnificationController.isMagnifying(mDisplayId)) {
- zoomOff();
- } else {
+ // We put mShortcutTriggered into conditions.
+ // The reason is when the shortcut is triggered,
+ // the magnifier is activated and keeps in scale 1.0,
+ // and in this case, we still want to zoom on the magnifier.
+ if (!isActivated() || mShortcutTriggered) {
mPromptController.showNotificationIfNeeded();
zoomOn(up.getX(), up.getY());
+ } else {
+ zoomOff();
}
+
+ clear();
}
- private boolean isMagnifying() {
- return mFullScreenMagnificationController.isMagnifying(mDisplayId);
+ private boolean isActivated() {
+ return mFullScreenMagnificationController.isActivated(mDisplayId);
}
void transitionToViewportDraggingStateAndClear(MotionEvent down) {
@@ -887,14 +923,13 @@
if (DEBUG_DETECTING) Slog.i(mLogTag, "onTripleTapAndHold()");
clear();
- mViewportDraggingState.mZoomedInBeforeDrag =
- mFullScreenMagnificationController.isMagnifying(mDisplayId);
-
// Triple tap and hold also belongs to triple tap event.
- final boolean enabled = !mViewportDraggingState.mZoomedInBeforeDrag;
+ final boolean enabled = !isActivated();
logMagnificationTripleTap(enabled);
- zoomOn(down.getX(), down.getY());
+ mViewportDraggingState.prepareForZoomInTemporary();
+
+ zoomInTemporary(down.getX(), down.getY());
transitionTo(mViewportDraggingState);
}
@@ -919,7 +954,10 @@
if (DEBUG_DETECTING) Slog.i(mLogTag, "setShortcutTriggered(" + state + ")");
mShortcutTriggered = state;
- mFullScreenMagnificationController.setForceShowMagnifiableBounds(mDisplayId, state);
+ }
+
+ private boolean isShortcutTriggered() {
+ return mShortcutTriggered;
}
/**
@@ -948,12 +986,29 @@
}
}
+ private void zoomInTemporary(float centerX, float centerY) {
+ final float currentScale = mFullScreenMagnificationController.getScale(mDisplayId);
+ final float persistedScale = MathUtils.constrain(
+ mFullScreenMagnificationController.getPersistedScale(mDisplayId),
+ MIN_SCALE, MAX_SCALE);
+
+ final float scale = MathUtils.constrain(Math.max(currentScale + 1.0f, persistedScale),
+ MIN_SCALE, MAX_SCALE);
+
+ zoomToScale(scale, centerX, centerY);
+ }
+
private void zoomOn(float centerX, float centerY) {
if (DEBUG_DETECTING) Slog.i(mLogTag, "zoomOn(" + centerX + ", " + centerY + ")");
final float scale = MathUtils.constrain(
mFullScreenMagnificationController.getPersistedScale(mDisplayId),
MIN_SCALE, MAX_SCALE);
+ zoomToScale(scale, centerX, centerY);
+ }
+
+ private void zoomToScale(float scale, float centerX, float centerY) {
+ scale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE);
mFullScreenMagnificationController.setScaleAndCenter(mDisplayId,
scale, centerX, centerY,
/* animate */ true,
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
index 02e810f..129bc16 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
@@ -254,13 +254,15 @@
final DisableMagnificationCallback animationEndCallback =
new DisableMagnificationCallback(transitionCallBack, displayId, targetMode,
scale, currentCenter, true);
+
+ setDisableMagnificationCallbackLocked(displayId, animationEndCallback);
+
if (targetMode == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW) {
screenMagnificationController.reset(displayId, animationEndCallback);
} else {
windowMagnificationMgr.disableWindowMagnification(displayId, false,
animationEndCallback);
}
- setDisableMagnificationCallbackLocked(displayId, animationEndCallback);
}
/**
@@ -481,17 +483,17 @@
*/
private boolean shouldNotifyMagnificationChange(int displayId, int changeMode) {
synchronized (mLock) {
- final boolean fullScreenMagnifying = mFullScreenMagnificationController != null
- && mFullScreenMagnificationController.isMagnifying(displayId);
+ final boolean fullScreenActivated = mFullScreenMagnificationController != null
+ && mFullScreenMagnificationController.isActivated(displayId);
final boolean windowEnabled = mWindowMagnificationMgr != null
&& mWindowMagnificationMgr.isWindowMagnifierEnabled(displayId);
final Integer transitionMode = mTransitionModes.get(displayId);
- if (((changeMode == MAGNIFICATION_MODE_FULLSCREEN && fullScreenMagnifying)
+ if (((changeMode == MAGNIFICATION_MODE_FULLSCREEN && fullScreenActivated)
|| (changeMode == MAGNIFICATION_MODE_WINDOW && windowEnabled))
&& (transitionMode == null)) {
return true;
}
- if ((!fullScreenMagnifying && !windowEnabled)
+ if ((!fullScreenActivated && !windowEnabled)
&& (transitionMode == null)) {
return true;
}
@@ -742,7 +744,7 @@
mWindowMagnificationMgr.getCenterY(displayId));
} else {
if (mFullScreenMagnificationController == null
- || !mFullScreenMagnificationController.isMagnifying(displayId)) {
+ || !mFullScreenMagnificationController.isActivated(displayId)) {
return null;
}
mTempPoint.set(mFullScreenMagnificationController.getCenterX(displayId),
@@ -766,9 +768,7 @@
if (mFullScreenMagnificationController == null) {
return false;
}
- isActivated = mFullScreenMagnificationController.isMagnifying(displayId)
- || mFullScreenMagnificationController.isForceShowMagnifiableBounds(
- displayId);
+ isActivated = mFullScreenMagnificationController.isActivated(displayId);
}
} else if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW) {
synchronized (mLock) {
@@ -829,7 +829,7 @@
final FullScreenMagnificationController screenMagnificationController =
getFullScreenMagnificationController();
if (mCurrentMode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN
- && !screenMagnificationController.isMagnifying(mDisplayId)) {
+ && !screenMagnificationController.isActivated(mDisplayId)) {
MagnificationConfig.Builder configBuilder =
new MagnificationConfig.Builder();
Region region = new Region();
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java
index a356ae6..75fe026 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java
@@ -313,13 +313,13 @@
}
/**
- * {@link FullScreenMagnificationController#isMagnifying(int)}
+ * {@link FullScreenMagnificationController#isActivated(int)}
* {@link WindowMagnificationManager#isWindowMagnifierEnabled(int)}
*/
public boolean isMagnifying(int displayId) {
int mode = getControllingMode(displayId);
if (mode == MAGNIFICATION_MODE_FULLSCREEN) {
- return mController.getFullScreenMagnificationController().isMagnifying(displayId);
+ return mController.getFullScreenMagnificationController().isActivated(displayId);
} else if (mode == MAGNIFICATION_MODE_WINDOW) {
return mController.getWindowMagnificationMgr().isWindowMagnifierEnabled(displayId);
}
diff --git a/services/api/current.txt b/services/api/current.txt
index aab6a6c..3926b39 100644
--- a/services/api/current.txt
+++ b/services/api/current.txt
@@ -38,7 +38,8 @@
package com.android.server.am {
public interface ActivityManagerLocal {
- method public boolean bindSdkSandboxService(@NonNull android.content.Intent, @NonNull android.content.ServiceConnection, int, @NonNull String, @NonNull String, int) throws android.os.RemoteException;
+ method public boolean bindSdkSandboxService(@NonNull android.content.Intent, @NonNull android.content.ServiceConnection, int, @NonNull android.os.IBinder, @NonNull String, @NonNull String, int) throws android.os.RemoteException;
+ method @Deprecated public boolean bindSdkSandboxService(@NonNull android.content.Intent, @NonNull android.content.ServiceConnection, int, @NonNull String, @NonNull String, int) throws android.os.RemoteException;
method public boolean canStartForegroundService(int, int, @NonNull String);
method public void killSdkSandboxClientAppProcess(@NonNull android.os.IBinder);
}
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index 54f77b1..6b61e97 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -67,6 +67,7 @@
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.TimeUtils;
+import android.view.autofill.AutofillFeatureFlags;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillManager.AutofillCommitReason;
@@ -298,12 +299,12 @@
private void onDeviceConfigChange(@NonNull Set<String> keys) {
for (String key : keys) {
switch (key) {
- case AutofillManager.DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES:
- case AutofillManager.DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT:
- case AutofillManager.DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT:
+ case AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES:
+ case AutofillFeatureFlags.DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT:
+ case AutofillFeatureFlags.DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT:
setDeviceConfigProperties();
break;
- case AutofillManager.DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES:
+ case AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES:
updateCachedServices();
break;
default:
@@ -567,15 +568,15 @@
synchronized (mLock) {
mAugmentedServiceIdleUnbindTimeoutMs = DeviceConfig.getInt(
DeviceConfig.NAMESPACE_AUTOFILL,
- AutofillManager.DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT,
+ AutofillFeatureFlags.DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT,
(int) AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS);
mAugmentedServiceRequestTimeoutMs = DeviceConfig.getInt(
DeviceConfig.NAMESPACE_AUTOFILL,
- AutofillManager.DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT,
+ AutofillFeatureFlags.DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT,
DEFAULT_AUGMENTED_AUTOFILL_REQUEST_TIMEOUT_MILLIS);
mSupportedSmartSuggestionModes = DeviceConfig.getInt(
DeviceConfig.NAMESPACE_AUTOFILL,
- AutofillManager.DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES,
+ AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES,
AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM);
if (verbose) {
Slog.v(mTag, "setDeviceConfigProperties(): "
@@ -729,7 +730,7 @@
private String getAllowedCompatModePackagesFromDeviceConfig() {
String config = DeviceConfig.getString(
DeviceConfig.NAMESPACE_AUTOFILL,
- AutofillManager.DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES,
+ AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES,
/* defaultValue */ null);
if (!TextUtils.isEmpty(config)) {
return config;
diff --git a/services/backup/java/com/android/server/backup/TransportManager.java b/services/backup/java/com/android/server/backup/TransportManager.java
index a4ea698..05327dc 100644
--- a/services/backup/java/com/android/server/backup/TransportManager.java
+++ b/services/backup/java/com/android/server/backup/TransportManager.java
@@ -156,14 +156,16 @@
} catch (IllegalArgumentException ex) {
// packageName doesn't exist: likely due to a race with it being uninstalled.
if (MORE_DEBUG) {
- Slog.d(TAG, "Package " + packageName + " was changed, but no longer exists.");
+ Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName
+ + " was changed, but no longer exists."));
}
return;
}
switch (enabled) {
case COMPONENT_ENABLED_STATE_ENABLED: {
if (MORE_DEBUG) {
- Slog.d(TAG, "Package " + packageName + " was enabled.");
+ Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName
+ + " was enabled."));
}
onPackageEnabled(packageName);
return;
@@ -173,28 +175,31 @@
// Unless explicitly specified in manifest, the default enabled state
// is 'enabled'. Here, we assume that default state always means enabled.
if (MORE_DEBUG) {
- Slog.d(TAG, "Package " + packageName
- + " was put in default enabled state.");
+ Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName
+ + " was put in default enabled state."));
}
onPackageEnabled(packageName);
return;
}
case COMPONENT_ENABLED_STATE_DISABLED: {
if (MORE_DEBUG) {
- Slog.d(TAG, "Package " + packageName + " was disabled.");
+ Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName
+ + " was disabled."));
}
onPackageDisabled(packageName);
return;
}
case COMPONENT_ENABLED_STATE_DISABLED_USER: {
if (MORE_DEBUG) {
- Slog.d(TAG, "Package " + packageName + " was disabled by user.");
+ Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName
+ + " was disabled by user."));
}
onPackageDisabled(packageName);
return;
}
default: {
- Slog.w(TAG, "Package " + packageName + " enabled setting: " + enabled);
+ Slog.w(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName
+ + " enabled setting: " + enabled));
return;
}
}
@@ -405,7 +410,8 @@
TransportDescription description =
mRegisteredTransportsDescriptionMap.get(transportComponent);
if (description == null) {
- Slog.e(TAG, "Transport " + name + " not registered tried to change description");
+ Slog.e(TAG, addUserIdToLogMessage(mUserId, "Transport " + name
+ + " not registered tried to change description"));
return;
}
description.name = name;
@@ -413,7 +419,8 @@
description.currentDestinationString = currentDestinationString;
description.dataManagementIntent = dataManagementIntent;
description.dataManagementLabel = dataManagementLabel;
- Slog.d(TAG, "Transport " + name + " updated its attributes");
+ Slog.d(TAG, addUserIdToLogMessage(mUserId, "Transport " + name
+ + " updated its attributes"));
}
}
@@ -493,7 +500,8 @@
try {
return getTransportClientOrThrow(transportName, caller);
} catch (TransportNotRegisteredException e) {
- Slog.w(TAG, "Transport " + transportName + " not registered");
+ Slog.w(TAG, addUserIdToLogMessage(mUserId, "Transport " + transportName
+ + " not registered"));
return null;
}
}
@@ -620,7 +628,7 @@
selectTransport(getTransportName(transportComponent));
return BackupManager.SUCCESS;
} catch (TransportNotRegisteredException e) {
- Slog.wtf(TAG, "Transport got unregistered");
+ Slog.wtf(TAG, addUserIdToLogMessage(mUserId, "Transport got unregistered"));
return BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
}
}
@@ -637,7 +645,8 @@
try {
mPackageManager.getPackageInfoAsUser(packageName, 0, mUserId);
} catch (PackageManager.NameNotFoundException e) {
- Slog.e(TAG, "Trying to register transports from package not found " + packageName);
+ Slog.e(TAG, addUserIdToLogMessage(mUserId,
+ "Trying to register transports from package not found " + packageName));
return;
}
@@ -668,7 +677,8 @@
if (!mTransportWhitelist.contains(transport)) {
Slog.w(
TAG,
- "BackupTransport " + transport.flattenToShortString() + " not whitelisted.");
+ addUserIdToLogMessage(mUserId, "BackupTransport "
+ + transport.flattenToShortString() + " not whitelisted."));
return false;
}
try {
@@ -676,11 +686,12 @@
mPackageManager.getPackageInfoAsUser(transport.getPackageName(), 0, mUserId);
if ((packInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED)
== 0) {
- Slog.w(TAG, "Transport package " + transport.getPackageName() + " not privileged");
+ Slog.w(TAG, addUserIdToLogMessage(mUserId, "Transport package "
+ + transport.getPackageName() + " not privileged"));
return false;
}
} catch (PackageManager.NameNotFoundException e) {
- Slog.w(TAG, "Package not found.", e);
+ Slog.w(TAG, addUserIdToLogMessage(mUserId, "Package not found."), e);
return false;
}
return true;
@@ -716,7 +727,8 @@
try {
transport = transportConnection.connectOrThrow(callerLogString);
} catch (TransportNotAvailableException e) {
- Slog.e(TAG, "Couldn't connect to transport " + transportString + " for registration");
+ Slog.e(TAG, addUserIdToLogMessage(mUserId, "Couldn't connect to transport "
+ + transportString + " for registration"));
mTransportConnectionManager.disposeOfTransportClient(transportConnection,
callerLogString);
return BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
@@ -728,11 +740,13 @@
String transportDirName = transport.transportDirName();
registerTransport(transportComponent, transport);
// If registerTransport() hasn't thrown...
- Slog.d(TAG, "Transport " + transportString + " registered");
+ Slog.d(TAG, addUserIdToLogMessage(mUserId, "Transport " + transportString
+ + " registered"));
mOnTransportRegisteredListener.onTransportRegistered(transportName, transportDirName);
result = BackupManager.SUCCESS;
} catch (RemoteException e) {
- Slog.e(TAG, "Transport " + transportString + " died while registering");
+ Slog.e(TAG, addUserIdToLogMessage(mUserId, "Transport " + transportString
+ + " died while registering"));
result = BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
}
@@ -798,4 +812,8 @@
this.dataManagementLabel = dataManagementLabel;
}
}
+
+ private static String addUserIdToLogMessage(int userId, String message) {
+ return "[UserID:" + userId + "] " + message;
+ }
}
diff --git a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
new file mode 100644
index 0000000..279981b
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
@@ -0,0 +1,24 @@
+{
+ "presubmit": [
+ {
+ "name": "CtsVirtualDevicesTestCases",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ },
+ {
+ "name": "CtsVirtualDevicesTestCases",
+ "options": [
+ {
+ "include-filter": "android.hardware.input.cts.tests"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ],
+ "file_patterns": ["Virtual[^/]*\\.java"]
+ }
+ ]
+}
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 17b6f5d..ec09acd 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -170,7 +170,7 @@
"android.hardware.ir-V1-java",
"android.hardware.rebootescrow-V1-java",
"android.hardware.soundtrigger-V2.3-java",
- "android.hardware.power.stats-V1-java",
+ "android.hardware.power.stats-V2-java",
"android.hardware.power-V4-java",
"android.hidl.manager-V1.2-java",
"icu4j_calendar_astronomer",
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index 68722d2..d485441 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -29,8 +29,11 @@
import android.compat.annotation.ChangeId;
import android.content.ComponentName;
import android.content.Context;
+import android.content.pm.ApexStagedEvent;
import android.content.pm.ApplicationInfo;
import android.content.pm.IBackgroundInstallControlService;
+import android.content.pm.IPackageManagerNative;
+import android.content.pm.IStagedApexObserver;
import android.content.pm.InstallSourceInfo;
import android.content.pm.ModuleInfo;
import android.content.pm.PackageInfo;
@@ -129,7 +132,6 @@
private String mVbmetaDigest;
// the system time (in ms) the last measurement was taken
private long mMeasurementsLastRecordedMs;
- private PackageManagerInternal mPackageManagerInternal;
/**
* Guards whether or not measurements of MBA to be performed. When this change is enabled,
@@ -1047,7 +1049,6 @@
mServiceImpl = new BinaryTransparencyServiceImpl();
mVbmetaDigest = VBMETA_DIGEST_UNINITIALIZED;
mMeasurementsLastRecordedMs = 0;
- mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
}
/**
@@ -1062,38 +1063,6 @@
} catch (Throwable t) {
Slog.e(TAG, "Failed to start BinaryTransparencyService.", t);
}
-
- // register a package observer to detect updates to preloads
- mPackageManagerInternal.getPackageList(new PackageManagerInternal.PackageListObserver() {
- @Override
- public void onPackageAdded(String packageName, int uid) {
-
- }
-
- @Override
- public void onPackageChanged(String packageName, int uid) {
- // check if the updated package is a preloaded app.
- PackageManager pm = mContext.getPackageManager();
- try {
- pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(
- PackageManager.MATCH_FACTORY_ONLY));
- } catch (PackageManager.NameNotFoundException e) {
- // this means that this package is not a preloaded app
- return;
- }
-
- Slog.d(TAG, "Preload " + packageName + " was updated. Scheduling measurement...");
- UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext,
- BinaryTransparencyService.this);
- }
-
- @Override
- public void onPackageRemoved(String packageName, int uid) {
-
- }
- });
-
- // TODO(b/264428429): Register observer for updates to APEXs.
}
/**
@@ -1104,19 +1073,22 @@
*/
@Override
public void onBootPhase(int phase) {
-
- // we are only interested in doing things at PHASE_BOOT_COMPLETED
- if (phase == PHASE_BOOT_COMPLETED) {
- Slog.i(TAG, "Boot completed. Getting VBMeta Digest.");
- getVBMetaDigestInformation();
-
- // to avoid the risk of holding up boot time, computations to measure APEX, Module, and
- // MBA digests are scheduled here, but only executed when the device is idle and plugged
- // in.
- Slog.i(TAG, "Scheduling measurements to be taken.");
- UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext,
- BinaryTransparencyService.this);
+ // so far we are only doing things in the PHASE_BOOT_COMPLETED phase
+ if (phase != PHASE_BOOT_COMPLETED) {
+ return;
}
+
+ Slog.i(TAG, "Boot completed. Getting VBMeta Digest.");
+ getVBMetaDigestInformation();
+
+ // to avoid the risk of holding up boot time, computations to measure APEX, Module, and
+ // MBA digests are scheduled here, but only executed when the device is idle and plugged
+ // in.
+ Slog.i(TAG, "Scheduling measurements to be taken.");
+ UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext,
+ BinaryTransparencyService.this);
+
+ registerPackageUpdateObservers();
}
/**
@@ -1212,6 +1184,67 @@
FrameworkStatsLog.write(FrameworkStatsLog.VBMETA_DIGEST_REPORTED, mVbmetaDigest);
}
+ /**
+ * Register observers for APK and APEX updates.
+ * The observers will be invoked when i) APK update and ii) APEX staging happens. This will
+ * then be used as signals to schedule measurement for the relevant binaries.
+ */
+ private void registerPackageUpdateObservers() {
+ Slog.d(TAG, "Registering APK updates...");
+ // first, register a package observer to detect updates to preloads
+ PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
+ pmi.getPackageList(new PackageManagerInternal.PackageListObserver() {
+ @Override
+ public void onPackageAdded(String packageName, int uid) {
+ }
+
+ @Override
+ public void onPackageChanged(String packageName, int uid) {
+ // check if the updated package is a preloaded app.
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(
+ PackageManager.MATCH_FACTORY_ONLY));
+ } catch (PackageManager.NameNotFoundException e) {
+ // this means that this package is not a preloaded app
+ return;
+ }
+
+ Slog.d(TAG, "Preload " + packageName + " was updated. Scheduling measurement...");
+ UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext,
+ BinaryTransparencyService.this);
+ }
+
+ @Override
+ public void onPackageRemoved(String packageName, int uid) {
+ }
+ });
+
+ // then, register an observer for staged APEXs
+ Slog.d(TAG, "Registering APEX updates...");
+ IPackageManagerNative iPackageManagerNative = IPackageManagerNative.Stub.asInterface(
+ ServiceManager.getService("package_native"));
+ if (iPackageManagerNative == null) {
+ Slog.e(TAG, "IPackageManagerNative is null");
+ return;
+ }
+
+ try {
+ iPackageManagerNative.registerStagedApexObserver(new IStagedApexObserver.Stub() {
+ @Override
+ public void onApexStaged(ApexStagedEvent event) throws RemoteException {
+ Slog.d(TAG, "A new APEX has been staged for update. There are currently "
+ + event.stagedApexModuleNames.length + " APEX(s) staged for update. "
+ + "Scheduling measurement...");
+ UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext,
+ BinaryTransparencyService.this);
+ }
+ });
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to register a StagedApexObserver.");
+ }
+ }
+
private String translateContentDigestAlgorithmIdToString(int algorithmId) {
switch (algorithmId) {
case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256:
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index bcea40e5..ece7254 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -3181,7 +3181,8 @@
int bindServiceLocked(IApplicationThread caller, IBinder token, Intent service,
String resolvedType, final IServiceConnection connection, int flags,
String instanceName, boolean isSdkSandboxService, int sdkSandboxClientAppUid,
- String sdkSandboxClientAppPackage, String callingPackage, final int userId)
+ String sdkSandboxClientAppPackage, IApplicationThread sdkSandboxClientApplicationThread,
+ String callingPackage, final int userId)
throws TransactionTooLargeException {
if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "bindService: " + service
+ " type=" + resolvedType + " conn=" + connection.asBinder()
@@ -3271,6 +3272,10 @@
final boolean allowInstant = (flags & Context.BIND_ALLOW_INSTANT) != 0;
final boolean inSharedIsolatedProcess = (flags & Context.BIND_SHARED_ISOLATED_PROCESS) != 0;
+ ProcessRecord attributedApp = null;
+ if (sdkSandboxClientAppUid > 0) {
+ attributedApp = mAm.getRecordForAppLOSP(sdkSandboxClientApplicationThread);
+ }
ServiceLookupResult res = retrieveServiceLocked(service, instanceName,
isSdkSandboxService, sdkSandboxClientAppUid, sdkSandboxClientAppPackage,
resolvedType, callingPackage, callingPid, callingUid, userId, true, callerFg,
@@ -3283,7 +3288,7 @@
return -1;
}
ServiceRecord s = res.record;
- final AppBindRecord b = s.retrieveAppBindingLocked(service, callerApp);
+ final AppBindRecord b = s.retrieveAppBindingLocked(service, callerApp, attributedApp);
final ProcessServiceRecord clientPsr = b.client.mServices;
if (clientPsr.numberOfConnections() >= mAm.mConstants.mMaxServiceConnectionsPerProcess) {
Slog.w(TAG, "bindService exceeded max service connection number per process, "
diff --git a/services/core/java/com/android/server/am/ActivityManagerLocal.java b/services/core/java/com/android/server/am/ActivityManagerLocal.java
index 5175a31..fa0972a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerLocal.java
+++ b/services/core/java/com/android/server/am/ActivityManagerLocal.java
@@ -76,6 +76,8 @@
* @param conn Receives information as the service is started and stopped.
* This must be a valid ServiceConnection object; it must not be null.
* @param clientAppUid Uid of the app for which the sdk sandbox process needs to be spawned.
+ * @param clientApplicationThread ApplicationThread object of the app for which the sdk sandboox
+ * is spawned.
* @param clientAppPackage Package of the app for which the sdk sandbox process needs to
* be spawned. This package must belong to the clientAppUid.
* @param processName Unique identifier for the service instance. Each unique name here will
@@ -91,6 +93,19 @@
*/
@SuppressLint("RethrowRemoteException")
boolean bindSdkSandboxService(@NonNull Intent service, @NonNull ServiceConnection conn,
+ int clientAppUid, @NonNull IBinder clientApplicationThread,
+ @NonNull String clientAppPackage, @NonNull String processName,
+ @Context.BindServiceFlags int flags)
+ throws RemoteException;
+
+ /**
+ * @deprecated Please use
+ * {@link #bindSdkSandboxService(Intent, ServiceConnection, int, IBinder, String, String, int)}
+ *
+ * This API can't be deleted yet because it can be used by early AdService module versions.
+ */
+ @SuppressLint("RethrowRemoteException")
+ boolean bindSdkSandboxService(@NonNull Intent service, @NonNull ServiceConnection conn,
int clientAppUid, @NonNull String clientAppPackage, @NonNull String processName,
@Context.BindServiceFlags int flags)
throws RemoteException;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1bc312e..a386baf 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -13107,13 +13107,15 @@
String resolvedType, IServiceConnection connection, int flags, String instanceName,
String callingPackage, int userId) throws TransactionTooLargeException {
return bindServiceInstance(caller, token, service, resolvedType, connection, flags,
- instanceName, false, INVALID_UID, null, callingPackage, userId);
+ instanceName, false, INVALID_UID, null, null, callingPackage, userId);
}
private int bindServiceInstance(IApplicationThread caller, IBinder token, Intent service,
String resolvedType, IServiceConnection connection, int flags, String instanceName,
boolean isSdkSandboxService, int sdkSandboxClientAppUid,
- String sdkSandboxClientAppPackage, String callingPackage, int userId)
+ String sdkSandboxClientAppPackage,
+ IApplicationThread sdkSandboxClientApplicationThread,
+ String callingPackage, int userId)
throws TransactionTooLargeException {
enforceNotIsolatedCaller("bindService");
enforceAllowedToStartOrBindServiceIfSdkSandbox(service);
@@ -13152,7 +13154,8 @@
synchronized (this) {
return mServices.bindServiceLocked(caller, token, service, resolvedType, connection,
flags, instanceName, isSdkSandboxService, sdkSandboxClientAppUid,
- sdkSandboxClientAppPackage, callingPackage, userId);
+ sdkSandboxClientAppPackage, sdkSandboxClientApplicationThread,
+ callingPackage, userId);
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
@@ -13517,12 +13520,17 @@
public Intent registerReceiverWithFeature(IApplicationThread caller, String callerPackage,
String callerFeatureId, String receiverId, IIntentReceiver receiver,
IntentFilter filter, String permission, int userId, int flags) {
+ enforceNotIsolatedCaller("registerReceiver");
+
// Allow Sandbox process to register only unexported receivers.
- if ((flags & Context.RECEIVER_NOT_EXPORTED) != 0) {
- enforceNotIsolatedCaller("registerReceiver");
- } else if (mSdkSandboxSettings.isBroadcastReceiverRestrictionsEnforced()) {
- enforceNotIsolatedOrSdkSandboxCaller("registerReceiver");
+ boolean unexported = (flags & Context.RECEIVER_NOT_EXPORTED) != 0;
+ if (mSdkSandboxSettings.isBroadcastReceiverRestrictionsEnforced()
+ && Process.isSdkSandboxUid(Binder.getCallingUid())
+ && !unexported) {
+ throw new SecurityException("SDK sandbox process not allowed to call "
+ + "registerReceiver");
}
+
ArrayList<Intent> stickyIntents = null;
ProcessRecord callerApp = null;
final boolean visibleToInstantApps
@@ -16959,7 +16967,8 @@
@Override
public boolean bindSdkSandboxService(Intent service, ServiceConnection conn,
- int clientAppUid, String clientAppPackage, String processName, int flags)
+ int clientAppUid, IBinder clientApplicationThread, String clientAppPackage,
+ String processName, int flags)
throws RemoteException {
if (service == null) {
throw new IllegalArgumentException("intent is null");
@@ -16984,14 +16993,40 @@
}
Handler handler = mContext.getMainThreadHandler();
-
+ IApplicationThread clientApplicationThreadVerified = null;
+ if (clientApplicationThread != null) {
+ // Make sure this is a valid application process
+ synchronized (this) {
+ final ProcessRecord rec = getRecordForAppLOSP(clientApplicationThread);
+ if (rec == null) {
+ // This could happen if the calling process has disappeared; no need for the
+ // sandbox to be even started in this case.
+ Slog.i(TAG, "clientApplicationThread process not found.");
+ return false;
+ }
+ if (rec.info.uid != clientAppUid) {
+ throw new IllegalArgumentException("clientApplicationThread does not match "
+ + " client uid");
+ }
+ clientApplicationThreadVerified = rec.getThread();
+ }
+ }
final IServiceConnection sd = mContext.getServiceDispatcher(conn, handler, flags);
service.prepareToLeaveProcess(mContext);
return ActivityManagerService.this.bindServiceInstance(
mContext.getIApplicationThread(), mContext.getActivityToken(), service,
service.resolveTypeIfNeeded(mContext.getContentResolver()), sd, flags,
processName, /*isSdkSandboxService*/ true, clientAppUid, clientAppPackage,
- mContext.getOpPackageName(), UserHandle.getUserId(clientAppUid)) != 0;
+ clientApplicationThreadVerified, mContext.getOpPackageName(),
+ UserHandle.getUserId(clientAppUid)) != 0;
+ }
+
+ @Override
+ public boolean bindSdkSandboxService(Intent service, ServiceConnection conn,
+ int clientAppUid, String clientAppPackage, String processName, int flags)
+ throws RemoteException {
+ return bindSdkSandboxService(service, conn, clientAppUid,
+ null /* clientApplicationThread */, clientAppPackage, processName, flags);
}
@Override
diff --git a/services/core/java/com/android/server/am/AppBindRecord.java b/services/core/java/com/android/server/am/AppBindRecord.java
index 28756a4..f7b3d3a 100644
--- a/services/core/java/com/android/server/am/AppBindRecord.java
+++ b/services/core/java/com/android/server/am/AppBindRecord.java
@@ -28,13 +28,15 @@
final ServiceRecord service; // The running service.
final IntentBindRecord intent; // The intent we are bound to.
final ProcessRecord client; // Who has started/bound the service.
-
+ final ProcessRecord attributedClient; // The binding was done by the system on behalf
+ // of 'attributedClient'
final ArraySet<ConnectionRecord> connections = new ArraySet<>();
// All ConnectionRecord for this client.
void dump(PrintWriter pw, String prefix) {
pw.println(prefix + "service=" + service);
pw.println(prefix + "client=" + client);
+ pw.println(prefix + "attributedClient=" + attributedClient);
dumpInIntentBind(pw, prefix);
}
@@ -50,10 +52,11 @@
}
AppBindRecord(ServiceRecord _service, IntentBindRecord _intent,
- ProcessRecord _client) {
+ ProcessRecord _client, ProcessRecord _attributedClient) {
service = _service;
intent = _intent;
client = _client;
+ attributedClient = _attributedClient;
}
public String toString() {
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 8c242743..def51b0 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -1063,7 +1063,7 @@
}
public AppBindRecord retrieveAppBindingLocked(Intent intent,
- ProcessRecord app) {
+ ProcessRecord app, ProcessRecord attributedApp) {
Intent.FilterComparison filter = new Intent.FilterComparison(intent);
IntentBindRecord i = bindings.get(filter);
if (i == null) {
@@ -1074,7 +1074,7 @@
if (a != null) {
return a;
}
- a = new AppBindRecord(this, i, app);
+ a = new AppBindRecord(this, i, app, attributedApp);
i.apps.put(app, a);
return a;
}
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index 7edd911..919b850 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -202,15 +202,7 @@
}
}
mCurrentCsd = currentCsd;
- mDoseRecords.addAll(Arrays.asList(records));
- long totalDuration = 0;
- for (SoundDoseRecord record : records) {
- Log.i(TAG, " new record: csd=" + record.value
- + " averageMel=" + record.averageMel + " timestamp=" + record.timestamp
- + " duration=" + record.duration);
- totalDuration += record.duration;
- }
- mLogger.enqueue(SoundDoseEvent.getDoseUpdateEvent(currentCsd, totalDuration));
+ updateSoundDoseRecords(records, currentCsd);
}
};
@@ -626,6 +618,29 @@
return null;
}
+ private void updateSoundDoseRecords(SoundDoseRecord[] newRecords, float currentCsd) {
+ long totalDuration = 0;
+ for (SoundDoseRecord record : newRecords) {
+ Log.i(TAG, " new record: " + record);
+ totalDuration += record.duration;
+
+ if (record.value < 0) {
+ // Negative value means the record timestamp exceeded the CSD rolling window size
+ // and needs to be removed
+ if (!mDoseRecords.removeIf(
+ r -> r.value == -record.value && r.timestamp == record.timestamp
+ && r.averageMel == record.averageMel
+ && r.duration == record.duration)) {
+ Log.w(TAG, "Could not find cached record to remove: " + record);
+ }
+ } else {
+ mDoseRecords.add(record);
+ }
+ }
+
+ mLogger.enqueue(SoundDoseEvent.getDoseUpdateEvent(currentCsd, totalDuration));
+ }
+
// StreamVolumeCommand contains the information needed to defer the process of
// setStreamVolume() in case the user has to acknowledge the safe volume warning message.
private static class StreamVolumeCommand {
diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java
index 1b20e43..59d8afa 100644
--- a/services/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -84,7 +84,8 @@
private static final String WALLPAPER_IMAGE_KEY = WallpaperBackupHelper.WALLPAPER_IMAGE_KEY;
private static final Set<String> sEligibleForMultiUser = Sets.newArraySet(
- PERMISSION_HELPER, NOTIFICATION_HELPER, SYNC_SETTINGS_HELPER, APP_LOCALES_HELPER);
+ PERMISSION_HELPER, NOTIFICATION_HELPER, SYNC_SETTINGS_HELPER, APP_LOCALES_HELPER,
+ ACCOUNT_MANAGER_HELPER);
private int mUserId = UserHandle.USER_SYSTEM;
@@ -100,7 +101,7 @@
addHelper(PERMISSION_HELPER, new PermissionBackupHelper(mUserId));
addHelper(USAGE_STATS_HELPER, new UsageStatsBackupHelper(this));
addHelper(SHORTCUT_MANAGER_HELPER, new ShortcutBackupHelper());
- addHelper(ACCOUNT_MANAGER_HELPER, new AccountManagerBackupHelper());
+ addHelper(ACCOUNT_MANAGER_HELPER, new AccountManagerBackupHelper(mUserId));
addHelper(SLICES_HELPER, new SliceBackupHelper(this));
addHelper(PEOPLE_HELPER, new PeopleBackupHelper(mUserId));
addHelper(APP_LOCALES_HELPER, new AppSpecificLocalesBackupHelper(mUserId));
diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java
index bde14ee..dcc98e1 100644
--- a/services/core/java/com/android/server/content/SyncManager.java
+++ b/services/core/java/com/android/server/content/SyncManager.java
@@ -49,7 +49,6 @@
import android.content.IntentFilter;
import android.content.PeriodicSync;
import android.content.ServiceConnection;
-import android.content.SharedPreferences;
import android.content.SyncActivityTooManyDeletes;
import android.content.SyncAdapterType;
import android.content.SyncAdaptersCache;
@@ -206,6 +205,13 @@
*/
private static final long SYNC_DELAY_ON_CONFLICT = 10*1000; // 10 seconds
+ /**
+ * Generate job ids in the range [MIN_SYNC_JOB_ID, MAX_SYNC_JOB_ID) to avoid conflicts with
+ * other jobs scheduled by the system process.
+ */
+ private static final int MIN_SYNC_JOB_ID = 100000;
+ private static final int MAX_SYNC_JOB_ID = 110000;
+
private static final String SYNC_WAKE_LOCK_PREFIX = "*sync*/";
private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarm";
private static final String SYNC_LOOP_WAKE_LOCK = "SyncLoopWakeLock";
@@ -223,9 +229,6 @@
private static final int SYNC_ADAPTER_CONNECTION_FLAGS = Context.BIND_AUTO_CREATE
| Context.BIND_NOT_FOREGROUND | Context.BIND_ALLOW_OOM_MANAGEMENT;
- private static final String PREF_KEY_SYNC_JOB_NAMESPACE_MIGRATED =
- "sync_job_namespace_migrated";
-
/** Singleton instance. */
@GuardedBy("SyncManager.class")
private static SyncManager sInstance;
@@ -239,11 +242,12 @@
volatile private PowerManager.WakeLock mSyncManagerWakeLock;
volatile private boolean mDataConnectionIsConnected = false;
- private volatile int mNextJobId = 0;
+ private volatile int mNextJobIdOffset = 0;
private final NotificationManager mNotificationMgr;
private final IBatteryStats mBatteryStats;
private JobScheduler mJobScheduler;
+ private JobSchedulerInternal mJobSchedulerInternal;
private SyncStorageEngine mSyncStorageEngine;
@@ -277,19 +281,24 @@
}
private int getUnusedJobIdH() {
- final List<JobInfo> pendingJobs = mJobScheduler.getAllPendingJobs();
- while (isJobIdInUseLockedH(mNextJobId, pendingJobs)) {
- // SyncManager jobs are placed in their own namespace. Since there's no chance of
- // conflicting with other parts of the system, we can just keep incrementing until
- // we find an unused ID.
- mNextJobId++;
+ final int maxNumSyncJobIds = MAX_SYNC_JOB_ID - MIN_SYNC_JOB_ID;
+ final List<JobInfo> pendingJobs = mJobSchedulerInternal.getSystemScheduledPendingJobs();
+ for (int i = 0; i < maxNumSyncJobIds; ++i) {
+ int newJobId = MIN_SYNC_JOB_ID + ((mNextJobIdOffset + i) % maxNumSyncJobIds);
+ if (!isJobIdInUseLockedH(newJobId, pendingJobs)) {
+ mNextJobIdOffset = (mNextJobIdOffset + i + 1) % maxNumSyncJobIds;
+ return newJobId;
+ }
}
- return mNextJobId;
+ // We've used all 10,000 intended job IDs.... We're probably in a world of pain right now :/
+ Slog.wtf(TAG, "All " + maxNumSyncJobIds + " possible sync job IDs are taken :/");
+ mNextJobIdOffset = (mNextJobIdOffset + 1) % maxNumSyncJobIds;
+ return MIN_SYNC_JOB_ID + mNextJobIdOffset;
}
private List<SyncOperation> getAllPendingSyncs() {
verifyJobScheduler();
- List<JobInfo> pendingJobs = mJobScheduler.getAllPendingJobs();
+ List<JobInfo> pendingJobs = mJobSchedulerInternal.getSystemScheduledPendingJobs();
final int numJobs = pendingJobs.size();
final List<SyncOperation> pendingSyncs = new ArrayList<>(numJobs);
for (int i = 0; i < numJobs; ++i) {
@@ -297,8 +306,6 @@
SyncOperation op = SyncOperation.maybeCreateFromJobExtras(job.getExtras());
if (op != null) {
pendingSyncs.add(op);
- } else {
- Slog.wtf(TAG, "Non-sync job inside of SyncManager's namespace");
}
}
return pendingSyncs;
@@ -484,31 +491,6 @@
});
}
- /**
- * Migrate syncs from the default job namespace to SyncManager's namespace if they haven't been
- * migrated already.
- */
- private void migrateSyncJobNamespaceIfNeeded() {
- final SharedPreferences prefs = mContext.getSharedPreferences(
- mSyncStorageEngine.getSyncDir(), Context.MODE_PRIVATE);
- if (prefs.getBoolean(PREF_KEY_SYNC_JOB_NAMESPACE_MIGRATED, false)) {
- return;
- }
- final List<JobInfo> pendingJobs = getJobSchedulerInternal().getSystemScheduledPendingJobs();
- final JobScheduler jobSchedulerDefaultNamespace =
- mContext.getSystemService(JobScheduler.class);
- for (int i = pendingJobs.size() - 1; i >= 0; --i) {
- final JobInfo job = pendingJobs.get(i);
- final SyncOperation op = SyncOperation.maybeCreateFromJobExtras(job.getExtras());
- if (op != null) {
- // This is a sync. Move it over to SyncManager's namespace.
- mJobScheduler.schedule(job);
- jobSchedulerDefaultNamespace.cancel(job.getId());
- }
- }
- prefs.edit().putBoolean(PREF_KEY_SYNC_JOB_NAMESPACE_MIGRATED, true).apply();
- }
-
private synchronized void verifyJobScheduler() {
if (mJobScheduler != null) {
return;
@@ -518,12 +500,10 @@
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.d(TAG, "initializing JobScheduler object.");
}
- // Use a dedicated namespace to avoid conflicts with other jobs
- // scheduled by the system process.
- mJobScheduler = mContext.getSystemService(JobScheduler.class)
- .forNamespace("SyncManager");
- migrateSyncJobNamespaceIfNeeded();
- // Get all persisted syncs from JobScheduler in the SyncManager namespace.
+ mJobScheduler = (JobScheduler) mContext.getSystemService(
+ Context.JOB_SCHEDULER_SERVICE);
+ mJobSchedulerInternal = getJobSchedulerInternal();
+ // Get all persisted syncs from JobScheduler
List<JobInfo> pendingJobs = mJobScheduler.getAllPendingJobs();
int numPersistedPeriodicSyncs = 0;
@@ -539,8 +519,6 @@
// shown on the settings activity.
mSyncStorageEngine.markPending(op.target, true);
}
- } else {
- Slog.wtf(TAG, "Non-sync job inside of SyncManager namespace");
}
}
final String summary = "Loaded persisted syncs: "
diff --git a/services/core/java/com/android/server/content/SyncStorageEngine.java b/services/core/java/com/android/server/content/SyncStorageEngine.java
index f7468fc..9c1cf38 100644
--- a/services/core/java/com/android/server/content/SyncStorageEngine.java
+++ b/services/core/java/com/android/server/content/SyncStorageEngine.java
@@ -21,7 +21,6 @@
import android.accounts.Account;
import android.accounts.AccountAndUser;
import android.accounts.AccountManager;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.backup.BackupManager;
import android.content.ComponentName;
@@ -575,11 +574,6 @@
return sSyncStorageEngine;
}
- @NonNull
- File getSyncDir() {
- return mSyncDir;
- }
-
protected void setOnSyncRequestListener(OnSyncRequestListener listener) {
if (mSyncRequestListener == null) {
mSyncRequestListener = listener;
diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java
index 6b5af88..59aa3f9 100644
--- a/services/core/java/com/android/server/hdmi/Constants.java
+++ b/services/core/java/com/android/server/hdmi/Constants.java
@@ -620,6 +620,12 @@
@interface HpdSignalType {}
static final String DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE = "soundbar_mode";
+ static final String DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX = "enable_earc_tx";
+ @StringDef({
+ DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE,
+ DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX
+ })
+ @interface FeatureFlag {}
private Constants() {
/* cannot be instantiated */
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index f6566d8..7ac8fd0 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -18,6 +18,7 @@
import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_ADD_DEVICE;
import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE;
+import static android.hardware.hdmi.HdmiControlManager.EARC_FEATURE_DISABLED;
import static android.hardware.hdmi.HdmiControlManager.EARC_FEATURE_ENABLED;
import static android.hardware.hdmi.HdmiControlManager.HDMI_CEC_CONTROL_ENABLED;
import static android.hardware.hdmi.HdmiControlManager.SOUNDBAR_MODE_DISABLED;
@@ -454,6 +455,9 @@
private boolean mSoundbarModeFeatureFlagEnabled = false;
@ServiceThreadOnly
+ private boolean mEarcTxFeatureFlagEnabled = false;
+
+ @ServiceThreadOnly
private int mActivePortId = Constants.INVALID_PORT_ID;
// Set to true while the input change by MHL is allowed.
@@ -666,9 +670,18 @@
setProhibitMode(false);
mHdmiControlEnabled = mHdmiCecConfig.getIntValue(
HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED);
+
+ mSoundbarModeFeatureFlagEnabled = mDeviceConfig.getBoolean(
+ Constants.DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE, false);
+ mEarcTxFeatureFlagEnabled = mDeviceConfig.getBoolean(
+ Constants.DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX, false);
+
synchronized (mLock) {
mEarcEnabled = (mHdmiCecConfig.getIntValue(
HdmiControlManager.SETTING_NAME_EARC_ENABLED) == EARC_FEATURE_ENABLED);
+ if (isTvDevice()) {
+ mEarcEnabled &= mEarcTxFeatureFlagEnabled;
+ }
}
setHdmiCecVolumeControlEnabledInternal(getHdmiCecConfig().getIntValue(
HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE));
@@ -812,20 +825,42 @@
}
}
}, mServiceThreadExecutor);
+
+ if (isTvDevice()) {
+ mDeviceConfig.addOnPropertiesChangedListener(getContext().getMainExecutor(),
+ new DeviceConfig.OnPropertiesChangedListener() {
+ @Override
+ public void onPropertiesChanged(DeviceConfig.Properties properties) {
+ mEarcTxFeatureFlagEnabled = properties.getBoolean(
+ Constants.DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX,
+ false);
+ boolean earcEnabledSetting = mHdmiCecConfig.getIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED)
+ == EARC_FEATURE_ENABLED;
+ setEarcEnabled(earcEnabledSetting && mEarcTxFeatureFlagEnabled
+ ? EARC_FEATURE_ENABLED : EARC_FEATURE_DISABLED);
+ }
+ });
+ }
+
mHdmiCecConfig.registerChangeListener(HdmiControlManager.SETTING_NAME_EARC_ENABLED,
new HdmiCecConfig.SettingChangeListener() {
@Override
public void onChange(String setting) {
- @HdmiControlManager.HdmiCecControl int enabled = mHdmiCecConfig.getIntValue(
- HdmiControlManager.SETTING_NAME_EARC_ENABLED);
- setEarcEnabled(enabled);
+ if (isTvDevice()) {
+ boolean earcEnabledSetting = mHdmiCecConfig.getIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED)
+ == EARC_FEATURE_ENABLED;
+ setEarcEnabled(earcEnabledSetting && mEarcTxFeatureFlagEnabled
+ ? EARC_FEATURE_ENABLED : EARC_FEATURE_DISABLED);
+ } else {
+ setEarcEnabled(mHdmiCecConfig.getIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED));
+ }
}
},
mServiceThreadExecutor);
- mSoundbarModeFeatureFlagEnabled = mDeviceConfig.getBoolean(
- Constants.DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE, false);
-
mDeviceConfig.addOnPropertiesChangedListener(getContext().getMainExecutor(),
new DeviceConfig.OnPropertiesChangedListener() {
@Override
@@ -4564,6 +4599,11 @@
startArcAction(false, new IHdmiControlCallback.Stub() {
@Override
public void onComplete(int result) throws RemoteException {
+ if (result != HdmiControlManager.RESULT_SUCCESS) {
+ Slog.w(TAG,
+ "ARC termination before enabling eARC in the HAL failed with "
+ + "result: " + result);
+ }
// Independently of the result (i.e. independently of whether the ARC RX device
// responded with <Terminate ARC> or not), we always end up terminating ARC in
// the HAL. As soon as we do that, we can enable eARC in the HAL.
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index ca42614..4d03e44 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -213,4 +213,10 @@
* @param enabled When true, stylus buttons will not be reported through motion events.
*/
public abstract void setStylusButtonMotionEventsEnabled(boolean enabled);
+
+ /**
+ * Notify whether any user activity occurred. This includes any input activity on any
+ * display, external peripherals, fingerprint sensor, etc.
+ */
+ public abstract void notifyUserActivity();
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index ea7f0bb..be4373a 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -70,6 +70,7 @@
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.VibrationEffect;
import android.os.vibrator.StepSegment;
@@ -158,6 +159,11 @@
private static final AdditionalDisplayInputProperties
DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties();
+ // To disable Keyboard backlight control via Framework, run:
+ // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart)
+ private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean(
+ "persist.input.keyboard.backlight_control.enabled", true);
+
private final NativeInputManagerService mNative;
private final Context mContext;
@@ -305,7 +311,7 @@
private final BatteryController mBatteryController;
// Manages Keyboard backlight
- private final KeyboardBacklightController mKeyboardBacklightController;
+ private final KeyboardBacklightControllerInterface mKeyboardBacklightController;
// Manages Keyboard modifier keys remapping
private final KeyRemapper mKeyRemapper;
@@ -422,8 +428,10 @@
mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore,
injector.getLooper());
mBatteryController = new BatteryController(mContext, mNative, injector.getLooper());
- mKeyboardBacklightController = new KeyboardBacklightController(mContext, mNative,
- mDataStore, injector.getLooper());
+ mKeyboardBacklightController =
+ KEYBOARD_BACKLIGHT_CONTROL_ENABLED ? new KeyboardBacklightController(mContext,
+ mNative, mDataStore, injector.getLooper())
+ : new KeyboardBacklightControllerInterface() {};
mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());
mUseDevInputEventForAudioJack =
@@ -3263,6 +3271,7 @@
public void setInteractive(boolean interactive) {
mNative.setInteractive(interactive);
mBatteryController.onInteractiveChanged(interactive);
+ mKeyboardBacklightController.onInteractiveChanged(interactive);
}
@Override
@@ -3346,10 +3355,12 @@
public void onInputMethodSubtypeChangedForKeyboardLayoutMapping(@UserIdInt int userId,
@Nullable InputMethodSubtypeHandle subtypeHandle,
@Nullable InputMethodSubtype subtype) {
- if (DEBUG) {
- Slog.i(TAG, "InputMethodSubtype changed: userId=" + userId
- + " subtypeHandle=" + subtypeHandle);
- }
+ mKeyboardLayoutManager.onInputMethodSubtypeChanged(userId, subtypeHandle, subtype);
+ }
+
+ @Override
+ public void notifyUserActivity() {
+ mKeyboardBacklightController.notifyUserActivity();
}
@Override
@@ -3478,4 +3489,15 @@
applyAdditionalDisplayInputPropertiesLocked(properties);
}
}
+
+ interface KeyboardBacklightControllerInterface {
+ default void incrementKeyboardBacklight(int deviceId) {}
+ default void decrementKeyboardBacklight(int deviceId) {}
+ default void registerKeyboardBacklightListener(IKeyboardBacklightListener l, int pid) {}
+ default void unregisterKeyboardBacklightListener(IKeyboardBacklightListener l, int pid) {}
+ default void onInteractiveChanged(boolean isInteractive) {}
+ default void notifyUserActivity() {}
+ default void systemRunning() {}
+ default void dump(PrintWriter pw) {}
+ }
}
diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java
index 77b0d4f..e1e3dd9 100644
--- a/services/core/java/com/android/server/input/KeyboardBacklightController.java
+++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java
@@ -17,7 +17,6 @@
package com.android.server.input;
import android.annotation.BinderThread;
-import android.annotation.ColorInt;
import android.content.Context;
import android.graphics.Color;
import android.hardware.input.IKeyboardBacklightListener;
@@ -29,6 +28,7 @@
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
@@ -39,15 +39,17 @@
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
+import java.time.Duration;
+import java.util.Arrays;
import java.util.Objects;
import java.util.OptionalInt;
-import java.util.TreeSet;
/**
* A thread-safe component of {@link InputManagerService} responsible for managing the keyboard
* backlight for supported keyboards.
*/
-final class KeyboardBacklightController implements InputManager.InputDeviceListener {
+final class KeyboardBacklightController implements
+ InputManagerService.KeyboardBacklightControllerInterface, InputManager.InputDeviceListener {
private static final String TAG = "KbdBacklightController";
@@ -58,12 +60,20 @@
private enum Direction {
DIRECTION_UP, DIRECTION_DOWN
}
- private static final int MSG_INCREMENT_KEYBOARD_BACKLIGHT = 1;
- private static final int MSG_DECREMENT_KEYBOARD_BACKLIGHT = 2;
+ private static final int MSG_UPDATE_EXISTING_DEVICES = 1;
+ private static final int MSG_INCREMENT_KEYBOARD_BACKLIGHT = 2;
+ private static final int MSG_DECREMENT_KEYBOARD_BACKLIGHT = 3;
+ private static final int MSG_NOTIFY_USER_ACTIVITY = 4;
+ private static final int MSG_NOTIFY_USER_INACTIVITY = 5;
+ private static final int MSG_INTERACTIVE_STATE_CHANGED = 6;
private static final int MAX_BRIGHTNESS = 255;
private static final int NUM_BRIGHTNESS_CHANGE_STEPS = 10;
+
@VisibleForTesting
- static final TreeSet<Integer> BRIGHTNESS_LEVELS = new TreeSet<>();
+ static final long USER_INACTIVITY_THRESHOLD_MILLIS = Duration.ofSeconds(30).toMillis();
+
+ @VisibleForTesting
+ static final int[] BRIGHTNESS_VALUE_FOR_LEVEL = new int[NUM_BRIGHTNESS_CHANGE_STEPS + 1];
private final Context mContext;
private final NativeInputManagerService mNative;
@@ -71,7 +81,12 @@
@GuardedBy("mDataStore")
private final PersistentDataStore mDataStore;
private final Handler mHandler;
- private final SparseArray<Light> mKeyboardBacklights = new SparseArray<>();
+ // Always access on handler thread or need to lock this for synchronization.
+ private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1);
+ // Maintains state if all backlights should be on or turned off
+ private boolean mIsBacklightOn = false;
+ // Maintains state if currently the device is interactive or not
+ private boolean mIsInteractive = true;
// List of currently registered keyboard backlight listeners
@GuardedBy("mKeyboardBacklightListenerRecords")
@@ -83,8 +98,8 @@
// device brightness range to [0-255]
// Levels are: 0, 25, 51, ..., 255
for (int i = 0; i <= NUM_BRIGHTNESS_CHANGE_STEPS; i++) {
- BRIGHTNESS_LEVELS.add(
- (int) Math.floor(((float) i * MAX_BRIGHTNESS) / NUM_BRIGHTNESS_CHANGE_STEPS));
+ BRIGHTNESS_VALUE_FOR_LEVEL[i] = (int) Math.floor(
+ ((float) i * MAX_BRIGHTNESS) / NUM_BRIGHTNESS_CHANGE_STEPS);
}
}
@@ -96,57 +111,64 @@
mHandler = new Handler(looper, this::handleMessage);
}
- void systemRunning() {
+ @Override
+ public void systemRunning() {
InputManager inputManager = Objects.requireNonNull(
mContext.getSystemService(InputManager.class));
inputManager.registerInputDeviceListener(this, mHandler);
- // Circle through all the already added input devices
- for (int deviceId : inputManager.getInputDeviceIds()) {
- onInputDeviceAdded(deviceId);
- }
+
+ Message msg = Message.obtain(mHandler, MSG_UPDATE_EXISTING_DEVICES,
+ inputManager.getInputDeviceIds());
+ mHandler.sendMessage(msg);
}
+ @Override
public void incrementKeyboardBacklight(int deviceId) {
Message msg = Message.obtain(mHandler, MSG_INCREMENT_KEYBOARD_BACKLIGHT, deviceId);
mHandler.sendMessage(msg);
}
+ @Override
public void decrementKeyboardBacklight(int deviceId) {
Message msg = Message.obtain(mHandler, MSG_DECREMENT_KEYBOARD_BACKLIGHT, deviceId);
mHandler.sendMessage(msg);
}
+ @Override
+ public void notifyUserActivity() {
+ Message msg = Message.obtain(mHandler, MSG_NOTIFY_USER_ACTIVITY);
+ mHandler.sendMessage(msg);
+ }
+
+ @Override
+ public void onInteractiveChanged(boolean isInteractive) {
+ Message msg = Message.obtain(mHandler, MSG_INTERACTIVE_STATE_CHANGED, isInteractive);
+ mHandler.sendMessage(msg);
+ }
+
private void updateKeyboardBacklight(int deviceId, Direction direction) {
InputDevice inputDevice = getInputDevice(deviceId);
- Light keyboardBacklight = mKeyboardBacklights.get(deviceId);
- if (inputDevice == null || keyboardBacklight == null) {
+ KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
+ if (inputDevice == null || state == null) {
return;
}
+ Light keyboardBacklight = state.mLight;
// Follow preset levels of brightness defined in BRIGHTNESS_LEVELS
- int currBrightness = BRIGHTNESS_LEVELS.floor(Color.alpha(
- mNative.getLightColor(deviceId, keyboardBacklight.getId())));
- int newBrightness;
+ final int currBrightnessLevel = state.mBrightnessLevel;
+ final int newBrightnessLevel;
if (direction == Direction.DIRECTION_UP) {
- newBrightness = currBrightness != MAX_BRIGHTNESS ? BRIGHTNESS_LEVELS.higher(
- currBrightness) : currBrightness;
+ newBrightnessLevel = Math.min(currBrightnessLevel + 1, NUM_BRIGHTNESS_CHANGE_STEPS);
} else {
- newBrightness = currBrightness != 0 ? BRIGHTNESS_LEVELS.lower(currBrightness)
- : currBrightness;
+ newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0);
}
- @ColorInt int newColor = Color.argb(newBrightness, 0, 0, 0);
- mNative.setLightColor(deviceId, keyboardBacklight.getId(), newColor);
- if (DEBUG) {
- Slog.d(TAG, "Changing brightness from " + currBrightness + " to " + newBrightness);
- }
-
- notifyKeyboardBacklightChanged(deviceId, BRIGHTNESS_LEVELS.headSet(newBrightness).size(),
- true/* isTriggeredByKeyPress */);
+ updateBacklightState(deviceId, keyboardBacklight, newBrightnessLevel,
+ true /* isTriggeredByKeyPress */);
synchronized (mDataStore) {
try {
mDataStore.setKeyboardBacklightBrightness(inputDevice.getDescriptor(),
keyboardBacklight.getId(),
- newBrightness);
+ BRIGHTNESS_VALUE_FOR_LEVEL[newBrightnessLevel]);
} finally {
mDataStore.saveIfNeeded();
}
@@ -159,23 +181,83 @@
brightness = mDataStore.getKeyboardBacklightBrightness(
inputDevice.getDescriptor(), keyboardBacklight.getId());
}
- if (!brightness.isEmpty()) {
- mNative.setLightColor(inputDevice.getId(), keyboardBacklight.getId(),
- Color.argb(brightness.getAsInt(), 0, 0, 0));
+ if (brightness.isPresent()) {
+ int brightnessValue = Math.max(0, Math.min(MAX_BRIGHTNESS, brightness.getAsInt()));
+ int brightnessLevel = Arrays.binarySearch(BRIGHTNESS_VALUE_FOR_LEVEL, brightnessValue);
+ updateBacklightState(inputDevice.getId(), keyboardBacklight, brightnessLevel,
+ false /* isTriggeredByKeyPress */);
if (DEBUG) {
Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt());
}
}
}
+ private void handleUserActivity() {
+ // Ignore user activity if device is not interactive. When device becomes interactive, we
+ // will send another user activity to turn backlight on.
+ if (!mIsInteractive) {
+ return;
+ }
+ if (!mIsBacklightOn) {
+ mIsBacklightOn = true;
+ for (int i = 0; i < mKeyboardBacklights.size(); i++) {
+ int deviceId = mKeyboardBacklights.keyAt(i);
+ KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
+ updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel,
+ false /* isTriggeredByKeyPress */);
+ }
+ }
+ mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY);
+ mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY,
+ SystemClock.uptimeMillis() + USER_INACTIVITY_THRESHOLD_MILLIS);
+ }
+
+ private void handleUserInactivity() {
+ if (mIsBacklightOn) {
+ mIsBacklightOn = false;
+ for (int i = 0; i < mKeyboardBacklights.size(); i++) {
+ int deviceId = mKeyboardBacklights.keyAt(i);
+ KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
+ updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel,
+ false /* isTriggeredByKeyPress */);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void handleInteractiveStateChange(boolean isInteractive) {
+ // Interactive state changes should force the keyboard to turn on/off irrespective of
+ // whether time out occurred or not.
+ mIsInteractive = isInteractive;
+ if (isInteractive) {
+ handleUserActivity();
+ } else {
+ handleUserInactivity();
+ }
+ }
+
private boolean handleMessage(Message msg) {
switch (msg.what) {
+ case MSG_UPDATE_EXISTING_DEVICES:
+ for (int deviceId : (int[]) msg.obj) {
+ onInputDeviceAdded(deviceId);
+ }
+ return true;
case MSG_INCREMENT_KEYBOARD_BACKLIGHT:
updateKeyboardBacklight((int) msg.obj, Direction.DIRECTION_UP);
return true;
case MSG_DECREMENT_KEYBOARD_BACKLIGHT:
updateKeyboardBacklight((int) msg.obj, Direction.DIRECTION_DOWN);
return true;
+ case MSG_NOTIFY_USER_ACTIVITY:
+ handleUserActivity();
+ return true;
+ case MSG_NOTIFY_USER_INACTIVITY:
+ handleUserInactivity();
+ return true;
+ case MSG_INTERACTIVE_STATE_CHANGED:
+ handleInteractiveStateChange((boolean) msg.obj);
+ return true;
}
return false;
}
@@ -204,12 +286,12 @@
mKeyboardBacklights.remove(deviceId);
return;
}
- final Light oldBacklight = mKeyboardBacklights.get(deviceId);
- if (oldBacklight != null && oldBacklight.getId() == keyboardBacklight.getId()) {
+ KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
+ if (state != null && state.mLight.getId() == keyboardBacklight.getId()) {
return;
}
// The keyboard backlight was added or changed.
- mKeyboardBacklights.put(deviceId, keyboardBacklight);
+ mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(keyboardBacklight));
restoreBacklightBrightness(inputDevice, keyboardBacklight);
}
@@ -232,6 +314,7 @@
/** Register the keyboard backlight listener for a process. */
@BinderThread
+ @Override
public void registerKeyboardBacklightListener(IKeyboardBacklightListener listener,
int pid) {
synchronized (mKeyboardBacklightListenerRecords) {
@@ -252,6 +335,7 @@
/** Unregister the keyboard backlight listener for a process. */
@BinderThread
+ @Override
public void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener,
int pid) {
synchronized (mKeyboardBacklightListenerRecords) {
@@ -269,13 +353,29 @@
}
}
- private void notifyKeyboardBacklightChanged(int deviceId, int currentBacklightLevel,
+ private void updateBacklightState(int deviceId, Light light, int brightnessLevel,
boolean isTriggeredByKeyPress) {
+ KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
+ if (state == null) {
+ return;
+ }
+
+ mNative.setLightColor(deviceId, light.getId(),
+ mIsBacklightOn ? Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel], 0, 0, 0)
+ : 0);
+ if (DEBUG) {
+ Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel
+ + "(isBacklightOn = " + mIsBacklightOn + ")");
+ }
+ state.mBrightnessLevel = brightnessLevel;
+
synchronized (mKeyboardBacklightListenerRecords) {
for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) {
+ IKeyboardBacklightState callbackState = new IKeyboardBacklightState();
+ callbackState.brightnessLevel = brightnessLevel;
+ callbackState.maxBrightnessLevel = NUM_BRIGHTNESS_CHANGE_STEPS;
mKeyboardBacklightListenerRecords.valueAt(i).notifyKeyboardBacklightChanged(
- deviceId, new KeyboardBacklightState(currentBacklightLevel),
- isTriggeredByKeyPress);
+ deviceId, callbackState, isTriggeredByKeyPress);
}
}
}
@@ -286,13 +386,17 @@
}
}
- void dump(PrintWriter pw) {
+ @Override
+ public void dump(PrintWriter pw) {
IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
- ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights");
+ ipw.println(
+ TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights, isBacklightOn = "
+ + mIsBacklightOn);
+
ipw.increaseIndent();
for (int i = 0; i < mKeyboardBacklights.size(); i++) {
- Light light = mKeyboardBacklights.get(i);
- ipw.println(i + ": { id: " + light.getId() + ", name: " + light.getName() + " }");
+ KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
+ ipw.println(i + ": " + state.toString());
}
ipw.decreaseIndent();
}
@@ -327,17 +431,18 @@
}
}
- private static class KeyboardBacklightState extends IKeyboardBacklightState {
+ private static class KeyboardBacklightState {
+ private final Light mLight;
+ private int mBrightnessLevel;
- KeyboardBacklightState(int brightnessLevel) {
- this.brightnessLevel = brightnessLevel;
- this.maxBrightnessLevel = NUM_BRIGHTNESS_CHANGE_STEPS;
+ KeyboardBacklightState(Light light) {
+ mLight = light;
}
@Override
public String toString() {
- return "KeyboardBacklightState{brightnessLevel=" + brightnessLevel
- + ", maxBrightnessLevel=" + maxBrightnessLevel
+ return "KeyboardBacklightState{Light=" + mLight.getId()
+ + ", BrightnessLevel=" + mBrightnessLevel
+ "}";
}
}
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index b8eb901..9e8b9f1 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -37,6 +37,8 @@
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
import android.hardware.input.KeyboardLayout;
+import android.icu.lang.UScript;
+import android.icu.util.ULocale;
import android.os.Bundle;
import android.os.Handler;
import android.os.LocaleList;
@@ -45,6 +47,8 @@
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.FeatureFlagUtils;
import android.util.Log;
import android.util.Slog;
import android.view.InputDevice;
@@ -54,6 +58,7 @@
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.util.XmlUtils;
@@ -63,10 +68,12 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
@@ -94,10 +101,18 @@
@GuardedBy("mDataStore")
private final PersistentDataStore mDataStore;
private final Handler mHandler;
+
private final List<InputDevice> mKeyboardsWithMissingLayouts = new ArrayList<>();
private boolean mKeyboardLayoutNotificationShown = false;
private Toast mSwitchedKeyboardLayoutToast;
+ // This cache stores "best-matched" layouts so that we don't need to run the matching
+ // algorithm repeatedly.
+ @GuardedBy("mKeyboardLayoutCache")
+ private final Map<String, String> mKeyboardLayoutCache = new ArrayMap<>();
+ @Nullable
+ private ImeInfo mCurrentImeInfo;
+
KeyboardLayoutManager(Context context, NativeInputManagerService nativeService,
PersistentDataStore dataStore, Looper looper) {
mContext = context;
@@ -139,8 +154,10 @@
@Override
public void onInputDeviceRemoved(int deviceId) {
- mKeyboardsWithMissingLayouts.removeIf(device -> device.getId() == deviceId);
- maybeUpdateNotification();
+ if (!useNewSettingsUi()) {
+ mKeyboardsWithMissingLayouts.removeIf(device -> device.getId() == deviceId);
+ maybeUpdateNotification();
+ }
}
@Override
@@ -149,18 +166,21 @@
if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
return;
}
- synchronized (mDataStore) {
- String layout = getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier());
- if (layout == null) {
- layout = getDefaultKeyboardLayout(inputDevice);
- if (layout != null) {
- setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout);
- } else {
- mKeyboardsWithMissingLayouts.add(inputDevice);
+ if (!useNewSettingsUi()) {
+ synchronized (mDataStore) {
+ String layout = getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier());
+ if (layout == null) {
+ layout = getDefaultKeyboardLayout(inputDevice);
+ if (layout != null) {
+ setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout);
+ } else {
+ mKeyboardsWithMissingLayouts.add(inputDevice);
+ }
}
+ maybeUpdateNotification();
}
- maybeUpdateNotification();
}
+ // TODO(b/259530132): Show notification for new Settings UI
}
private String getDefaultKeyboardLayout(final InputDevice inputDevice) {
@@ -244,6 +264,12 @@
}
}
+ synchronized (mKeyboardLayoutCache) {
+ // Invalidate the cache: With packages being installed/removed, existing cache of
+ // auto-selected layout might not be the best layouts anymore.
+ mKeyboardLayoutCache.clear();
+ }
+
// Reload keyboard layouts.
reloadKeyboardLayouts();
}
@@ -256,6 +282,9 @@
public KeyboardLayout[] getKeyboardLayoutsForInputDevice(
final InputDeviceIdentifier identifier) {
+ if (useNewSettingsUi()) {
+ return new KeyboardLayout[0];
+ }
final String[] enabledLayoutDescriptors =
getEnabledKeyboardLayoutsForInputDevice(identifier);
final ArrayList<KeyboardLayout> enabledLayouts =
@@ -296,6 +325,7 @@
KeyboardLayout[]::new);
}
+ @Nullable
public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) {
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
@@ -434,21 +464,25 @@
return LocaleList.forLanguageTags(languageTags.replace('|', ','));
}
- /**
- * Builds a layout descriptor for the vendor/product. This returns the
- * descriptor for ids that aren't useful (such as the default 0, 0).
- */
- private String getLayoutDescriptor(InputDeviceIdentifier identifier) {
+ private static String getLayoutDescriptor(@NonNull InputDeviceIdentifier identifier) {
Objects.requireNonNull(identifier, "identifier must not be null");
Objects.requireNonNull(identifier.getDescriptor(), "descriptor must not be null");
if (identifier.getVendorId() == 0 && identifier.getProductId() == 0) {
return identifier.getDescriptor();
}
+ // If vendor id and product id is available, use it as keys. This allows us to have the
+ // same setup for all keyboards with same product and vendor id. i.e. User can swap 2
+ // identical keyboards and still get the same setup.
return "vendor:" + identifier.getVendorId() + ",product:" + identifier.getProductId();
}
+ @Nullable
public String getCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier) {
+ if (useNewSettingsUi()) {
+ Slog.e(TAG, "getCurrentKeyboardLayoutForInputDevice API not supported");
+ return null;
+ }
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
String layout;
@@ -468,9 +502,13 @@
public void setCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
+ if (useNewSettingsUi()) {
+ Slog.e(TAG, "setCurrentKeyboardLayoutForInputDevice API not supported");
+ return;
+ }
+
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
-
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
try {
@@ -489,6 +527,10 @@
}
public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
+ if (useNewSettingsUi()) {
+ Slog.e(TAG, "getEnabledKeyboardLayoutsForInputDevice API not supported");
+ return new String[0];
+ }
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
String[] layouts = mDataStore.getKeyboardLayouts(key);
@@ -502,6 +544,10 @@
public void addKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
+ if (useNewSettingsUi()) {
+ Slog.e(TAG, "addKeyboardLayoutForInputDevice API not supported");
+ return;
+ }
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
@@ -525,6 +571,10 @@
public void removeKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
+ if (useNewSettingsUi()) {
+ Slog.e(TAG, "removeKeyboardLayoutForInputDevice API not supported");
+ return;
+ }
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
@@ -551,31 +601,11 @@
}
}
- public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
- @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
- @Nullable InputMethodSubtype imeSubtype) {
- // TODO(b/259530132): Implement the new keyboard layout API: Returning non-IME specific
- // layout for now.
- return getCurrentKeyboardLayoutForInputDevice(identifier);
- }
-
- public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
- @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
- @Nullable InputMethodSubtype imeSubtype, String keyboardLayoutDescriptor) {
- // TODO(b/259530132): Implement the new keyboard layout API: setting non-IME specific
- // layout for now.
- setCurrentKeyboardLayoutForInputDevice(identifier, keyboardLayoutDescriptor);
- }
-
- public KeyboardLayout[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier,
- @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
- @Nullable InputMethodSubtype imeSubtype) {
- // TODO(b/259530132): Implement the new keyboard layout API: Returning list of all
- // layouts for now.
- return getKeyboardLayouts();
- }
-
public void switchKeyboardLayout(int deviceId, int direction) {
+ if (useNewSettingsUi()) {
+ Slog.e(TAG, "switchKeyboardLayout API not supported");
+ return;
+ }
mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, deviceId, direction).sendToTarget();
}
@@ -616,8 +646,21 @@
}
}
+ @Nullable
public String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) {
- String keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
+ String keyboardLayoutDescriptor;
+ if (useNewSettingsUi()) {
+ if (mCurrentImeInfo == null) {
+ // Haven't received onInputMethodSubtypeChanged() callback from IMMS. Will reload
+ // keyboard layouts once we receive the callback.
+ return null;
+ }
+
+ keyboardLayoutDescriptor = getKeyboardLayoutForInputDeviceInternal(identifier,
+ mCurrentImeInfo);
+ } else {
+ keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
+ }
if (keyboardLayoutDescriptor == null) {
return null;
}
@@ -640,6 +683,287 @@
return result;
}
+ @Nullable
+ public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+ @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+ @Nullable InputMethodSubtype imeSubtype) {
+ if (!useNewSettingsUi()) {
+ Slog.e(TAG, "getKeyboardLayoutForInputDevice() API not supported");
+ return null;
+ }
+ InputMethodSubtypeHandle subtypeHandle = InputMethodSubtypeHandle.of(imeInfo, imeSubtype);
+ String layout = getKeyboardLayoutForInputDeviceInternal(identifier,
+ new ImeInfo(userId, subtypeHandle, imeSubtype));
+ if (DEBUG) {
+ Slog.d(TAG, "getKeyboardLayoutForInputDevice() " + identifier.toString() + ", userId : "
+ + userId + ", subtypeHandle = " + subtypeHandle + " -> " + layout);
+ }
+ return layout;
+ }
+
+ public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+ @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+ @Nullable InputMethodSubtype imeSubtype,
+ String keyboardLayoutDescriptor) {
+ if (!useNewSettingsUi()) {
+ Slog.e(TAG, "setKeyboardLayoutForInputDevice() API not supported");
+ return;
+ }
+ Objects.requireNonNull(keyboardLayoutDescriptor,
+ "keyboardLayoutDescriptor must not be null");
+ String key = createLayoutKey(identifier, userId,
+ InputMethodSubtypeHandle.of(imeInfo, imeSubtype));
+ synchronized (mDataStore) {
+ try {
+ // Key for storing into data store = <device descriptor>,<userId>,<subtypeHandle>
+ if (mDataStore.setKeyboardLayout(getLayoutDescriptor(identifier), key,
+ keyboardLayoutDescriptor)) {
+ if (DEBUG) {
+ Slog.d(TAG, "setKeyboardLayoutForInputDevice() " + identifier
+ + " key: " + key
+ + " keyboardLayoutDescriptor: " + keyboardLayoutDescriptor);
+ }
+ mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
+ }
+ } finally {
+ mDataStore.saveIfNeeded();
+ }
+ }
+ }
+
+ public KeyboardLayout[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier,
+ @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+ @Nullable InputMethodSubtype imeSubtype) {
+ if (!useNewSettingsUi()) {
+ Slog.e(TAG, "getKeyboardLayoutListForInputDevice() API not supported");
+ return new KeyboardLayout[0];
+ }
+ return getKeyboardLayoutListForInputDeviceInternal(identifier, new ImeInfo(userId,
+ InputMethodSubtypeHandle.of(imeInfo, imeSubtype), imeSubtype));
+ }
+
+ private KeyboardLayout[] getKeyboardLayoutListForInputDeviceInternal(
+ InputDeviceIdentifier identifier, ImeInfo imeInfo) {
+ String key = createLayoutKey(identifier, imeInfo.mUserId, imeInfo.mImeSubtypeHandle);
+
+ // Fetch user selected layout and always include it in layout list.
+ String userSelectedLayout;
+ synchronized (mDataStore) {
+ userSelectedLayout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key);
+ }
+
+ final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<>();
+ String imeLanguageTag;
+ if (imeInfo.mImeSubtype == null) {
+ imeLanguageTag = "";
+ } else {
+ ULocale imeLocale = imeInfo.mImeSubtype.getPhysicalKeyboardHintLanguageTag();
+ imeLanguageTag = imeLocale != null ? imeLocale.toLanguageTag()
+ : imeInfo.mImeSubtype.getCanonicalizedLanguageTag();
+ }
+
+ visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
+ boolean mDeviceSpecificLayoutAvailable;
+
+ @Override
+ public void visitKeyboardLayout(Resources resources,
+ int keyboardLayoutResId, KeyboardLayout layout) {
+ // Next find any potential layouts that aren't yet enabled for the device. For
+ // devices that have special layouts we assume there's a reason that the generic
+ // layouts don't work for them, so we don't want to return them since it's likely
+ // to result in a poor user experience.
+ if (layout.getVendorId() == identifier.getVendorId()
+ && layout.getProductId() == identifier.getProductId()) {
+ if (!mDeviceSpecificLayoutAvailable) {
+ mDeviceSpecificLayoutAvailable = true;
+ potentialLayouts.clear();
+ }
+ potentialLayouts.add(layout);
+ } else if (layout.getVendorId() == -1 && layout.getProductId() == -1
+ && !mDeviceSpecificLayoutAvailable && isLayoutCompatibleWithLanguageTag(
+ layout, imeLanguageTag)) {
+ potentialLayouts.add(layout);
+ } else if (layout.getDescriptor().equals(userSelectedLayout)) {
+ potentialLayouts.add(layout);
+ }
+ }
+ });
+ // Sort the Keyboard layouts. This is done first by priority then by label. So, system
+ // layouts will come above 3rd party layouts.
+ Collections.sort(potentialLayouts);
+ return potentialLayouts.toArray(new KeyboardLayout[0]);
+ }
+
+ public void onInputMethodSubtypeChanged(@UserIdInt int userId,
+ @Nullable InputMethodSubtypeHandle subtypeHandle,
+ @Nullable InputMethodSubtype subtype) {
+ if (!useNewSettingsUi()) {
+ Slog.e(TAG, "onInputMethodSubtypeChanged() API not supported");
+ return;
+ }
+ if (subtypeHandle == null) {
+ if (DEBUG) {
+ Slog.d(TAG, "No InputMethod is running, ignoring change");
+ }
+ return;
+ }
+ if (mCurrentImeInfo == null || !subtypeHandle.equals(mCurrentImeInfo.mImeSubtypeHandle)
+ || mCurrentImeInfo.mUserId != userId) {
+ mCurrentImeInfo = new ImeInfo(userId, subtypeHandle, subtype);
+ mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
+ if (DEBUG) {
+ Slog.d(TAG, "InputMethodSubtype changed: userId=" + userId
+ + " subtypeHandle=" + subtypeHandle);
+ }
+ }
+ }
+
+ @Nullable
+ private String getKeyboardLayoutForInputDeviceInternal(InputDeviceIdentifier identifier,
+ ImeInfo imeInfo) {
+ InputDevice inputDevice = getInputDevice(identifier);
+ if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
+ return null;
+ }
+ String key = createLayoutKey(identifier, imeInfo.mUserId, imeInfo.mImeSubtypeHandle);
+ String layout;
+ synchronized (mDataStore) {
+ layout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key);
+ }
+ if (layout == null) {
+ synchronized (mKeyboardLayoutCache) {
+ // Check Auto-selected layout cache to see if layout had been previously selected
+ if (mKeyboardLayoutCache.containsKey(key)) {
+ layout = mKeyboardLayoutCache.get(key);
+ } else {
+ // NOTE: This list is already filtered based on IME Script code
+ KeyboardLayout[] layoutList = getKeyboardLayoutListForInputDeviceInternal(
+ identifier, imeInfo);
+ // Call auto-matching algorithm to find the best matching layout
+ layout = getDefaultKeyboardLayoutBasedOnImeInfo(inputDevice, imeInfo,
+ layoutList);
+ mKeyboardLayoutCache.put(key, layout);
+ }
+ }
+ }
+ return layout;
+ }
+
+ @Nullable
+ private static String getDefaultKeyboardLayoutBasedOnImeInfo(InputDevice inputDevice,
+ ImeInfo imeInfo, KeyboardLayout[] layoutList) {
+ if (imeInfo.mImeSubtypeHandle == null) {
+ return null;
+ }
+
+ Arrays.sort(layoutList);
+
+ // Check <VendorID, ProductID> matching for explicitly declared custom KCM files.
+ for (KeyboardLayout layout : layoutList) {
+ if (layout.getVendorId() == inputDevice.getVendorId()
+ && layout.getProductId() == inputDevice.getProductId()) {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on "
+ + "vendor and product Ids. " + inputDevice.getIdentifier()
+ + " : " + layout.getDescriptor());
+ }
+ return layout.getDescriptor();
+ }
+ }
+
+ // Check layout type, language tag information from InputDevice for matching
+ String inputLanguageTag = inputDevice.getKeyboardLanguageTag();
+ if (inputLanguageTag != null) {
+ String layoutDesc = getMatchingLayoutForProvidedLanguageTagAndLayoutType(layoutList,
+ inputLanguageTag, inputDevice.getKeyboardLayoutType());
+
+ if (layoutDesc != null) {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on "
+ + "HW information (Language tag and Layout type). "
+ + inputDevice.getIdentifier() + " : " + layoutDesc);
+ }
+ return layoutDesc;
+ }
+ }
+
+ InputMethodSubtype subtype = imeInfo.mImeSubtype;
+ // Can't auto select layout based on IME if subtype or language tag is null
+ if (subtype == null) {
+ return null;
+ }
+
+ // Check layout type, language tag information from IME for matching
+ ULocale pkLocale = subtype.getPhysicalKeyboardHintLanguageTag();
+ String pkLanguageTag =
+ pkLocale != null ? pkLocale.toLanguageTag() : subtype.getCanonicalizedLanguageTag();
+ String layoutDesc = getMatchingLayoutForProvidedLanguageTagAndLayoutType(layoutList,
+ pkLanguageTag, subtype.getPhysicalKeyboardHintLayoutType());
+ if (DEBUG) {
+ Slog.d(TAG,
+ "getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on "
+ + "IME locale matching. " + inputDevice.getIdentifier() + " : "
+ + layoutDesc);
+ }
+ return layoutDesc;
+ }
+
+ @Nullable
+ private static String getMatchingLayoutForProvidedLanguageTagAndLayoutType(
+ KeyboardLayout[] layoutList, @NonNull String languageTag, @Nullable String layoutType) {
+ if (layoutType == null || !KeyboardLayout.isLayoutTypeValid(layoutType)) {
+ layoutType = KeyboardLayout.LAYOUT_TYPE_UNDEFINED;
+ }
+ List<KeyboardLayout> layoutsFilteredByLayoutType = new ArrayList<>();
+ for (KeyboardLayout layout : layoutList) {
+ if (layout.getLayoutType().equals(layoutType)) {
+ layoutsFilteredByLayoutType.add(layout);
+ }
+ }
+ String layoutDesc = getMatchingLayoutForProvidedLanguageTag(layoutsFilteredByLayoutType,
+ languageTag);
+ if (layoutDesc != null) {
+ return layoutDesc;
+ }
+
+ return getMatchingLayoutForProvidedLanguageTag(Arrays.asList(layoutList), languageTag);
+ }
+
+ @Nullable
+ private static String getMatchingLayoutForProvidedLanguageTag(List<KeyboardLayout> layoutList,
+ @NonNull String languageTag) {
+ Locale locale = Locale.forLanguageTag(languageTag);
+ String layoutMatchingLanguage = null;
+ String layoutMatchingLanguageAndCountry = null;
+
+ for (KeyboardLayout layout : layoutList) {
+ final LocaleList locales = layout.getLocales();
+ for (int i = 0; i < locales.size(); i++) {
+ final Locale l = locales.get(i);
+ if (l == null) {
+ continue;
+ }
+ if (l.getLanguage().equals(locale.getLanguage())) {
+ if (layoutMatchingLanguage == null) {
+ layoutMatchingLanguage = layout.getDescriptor();
+ }
+ if (l.getCountry().equals(locale.getCountry())) {
+ if (layoutMatchingLanguageAndCountry == null) {
+ layoutMatchingLanguageAndCountry = layout.getDescriptor();
+ }
+ if (l.getVariant().equals(locale.getVariant())) {
+ return layout.getDescriptor();
+ }
+ }
+ }
+ }
+ }
+ return layoutMatchingLanguageAndCountry != null
+ ? layoutMatchingLanguageAndCountry : layoutMatchingLanguage;
+ }
+
private void reloadKeyboardLayouts() {
if (DEBUG) {
Slog.d(TAG, "Reloading keyboard layouts.");
@@ -734,11 +1058,65 @@
}
}
+ private boolean useNewSettingsUi() {
+ return FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI);
+ }
+
+ @Nullable
private InputDevice getInputDevice(int deviceId) {
InputManager inputManager = mContext.getSystemService(InputManager.class);
return inputManager != null ? inputManager.getInputDevice(deviceId) : null;
}
+ @Nullable
+ private InputDevice getInputDevice(InputDeviceIdentifier identifier) {
+ InputManager inputManager = mContext.getSystemService(InputManager.class);
+ return inputManager != null ? inputManager.getInputDeviceByDescriptor(
+ identifier.getDescriptor()) : null;
+ }
+
+ private static String createLayoutKey(InputDeviceIdentifier identifier, int userId,
+ @NonNull InputMethodSubtypeHandle subtypeHandle) {
+ Objects.requireNonNull(subtypeHandle, "subtypeHandle must not be null");
+ return "layoutDescriptor:" + getLayoutDescriptor(identifier) + ",userId:" + userId
+ + ",subtypeHandle:" + subtypeHandle.toStringHandle();
+ }
+
+ private static boolean isLayoutCompatibleWithLanguageTag(KeyboardLayout layout,
+ @NonNull String languageTag) {
+ final int[] scriptsFromLanguageTag = UScript.getCode(Locale.forLanguageTag(languageTag));
+ if (scriptsFromLanguageTag.length == 0) {
+ // If no scripts inferred from languageTag then allowing the layout
+ return true;
+ }
+ LocaleList locales = layout.getLocales();
+ if (locales.isEmpty()) {
+ // KCM file doesn't have an associated language tag. This can be from
+ // a 3rd party app so need to include it as a potential layout.
+ return true;
+ }
+ for (int i = 0; i < locales.size(); i++) {
+ final Locale locale = locales.get(i);
+ if (locale == null) {
+ continue;
+ }
+ int[] scripts = UScript.getCode(locale);
+ if (scripts != null && haveCommonValue(scripts, scriptsFromLanguageTag)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean haveCommonValue(int[] arr1, int[] arr2) {
+ for (int a1 : arr1) {
+ for (int a2 : arr2) {
+ if (a1 == a2) return true;
+ }
+ }
+ return false;
+ }
+
private static final class KeyboardLayoutDescriptor {
public String packageName;
public String receiverName;
@@ -767,6 +1145,19 @@
}
}
+ private static class ImeInfo {
+ @UserIdInt int mUserId;
+ @NonNull InputMethodSubtypeHandle mImeSubtypeHandle;
+ @Nullable InputMethodSubtype mImeSubtype;
+
+ ImeInfo(@UserIdInt int userId, @NonNull InputMethodSubtypeHandle imeSubtypeHandle,
+ @Nullable InputMethodSubtype imeSubtype) {
+ mUserId = userId;
+ mImeSubtypeHandle = imeSubtypeHandle;
+ mImeSubtype = imeSubtype;
+ }
+ }
+
private interface KeyboardLayoutVisitor {
void visitKeyboardLayout(Resources resources,
int keyboardLayoutResId, KeyboardLayout layout);
diff --git a/services/core/java/com/android/server/input/PersistentDataStore.java b/services/core/java/com/android/server/input/PersistentDataStore.java
index 375377a7..a2b18362 100644
--- a/services/core/java/com/android/server/input/PersistentDataStore.java
+++ b/services/core/java/com/android/server/input/PersistentDataStore.java
@@ -18,6 +18,7 @@
import android.annotation.Nullable;
import android.hardware.input.TouchCalibration;
+import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseIntArray;
@@ -42,6 +43,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
@@ -121,6 +123,7 @@
return false;
}
+ @Nullable
public String getCurrentKeyboardLayout(String inputDeviceDescriptor) {
InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
return state != null ? state.getCurrentKeyboardLayout() : null;
@@ -136,6 +139,22 @@
return false;
}
+ @Nullable
+ public String getKeyboardLayout(String inputDeviceDescriptor, String key) {
+ InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
+ return state != null ? state.getKeyboardLayout(key) : null;
+ }
+
+ public boolean setKeyboardLayout(String inputDeviceDescriptor, String key,
+ String keyboardLayoutDescriptor) {
+ InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor);
+ if (state.setKeyboardLayout(key, keyboardLayoutDescriptor)) {
+ setDirty();
+ return true;
+ }
+ return false;
+ }
+
public String[] getKeyboardLayouts(String inputDeviceDescriptor) {
InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
if (state == null) {
@@ -387,6 +406,8 @@
private final ArrayList<String> mKeyboardLayouts = new ArrayList<String>();
private final SparseIntArray mKeyboardBacklightBrightnessMap = new SparseIntArray();
+ private final Map<String, String> mKeyboardLayoutMap = new ArrayMap<>();
+
public TouchCalibration getTouchCalibration(int surfaceRotation) {
try {
return mTouchCalibration[surfaceRotation];
@@ -410,6 +431,15 @@
}
@Nullable
+ public String getKeyboardLayout(String key) {
+ return mKeyboardLayoutMap.get(key);
+ }
+
+ public boolean setKeyboardLayout(String key, String keyboardLayout) {
+ return !Objects.equals(mKeyboardLayoutMap.put(key, keyboardLayout), keyboardLayout);
+ }
+
+ @Nullable
public String getCurrentKeyboardLayout() {
return mCurrentKeyboardLayout;
}
@@ -507,6 +537,18 @@
changed = true;
}
}
+ List<String> removedEntries = new ArrayList<>();
+ for (String key : mKeyboardLayoutMap.keySet()) {
+ if (!availableKeyboardLayouts.contains(mKeyboardLayoutMap.get(key))) {
+ removedEntries.add(key);
+ }
+ }
+ if (!removedEntries.isEmpty()) {
+ for (String key : removedEntries) {
+ mKeyboardLayoutMap.remove(key);
+ }
+ changed = true;
+ }
return changed;
}
@@ -534,6 +576,18 @@
}
mCurrentKeyboardLayout = descriptor;
}
+ } else if (parser.getName().equals("keyed-keyboard-layout")) {
+ String key = parser.getAttributeValue(null, "key");
+ if (key == null) {
+ throw new XmlPullParserException(
+ "Missing key attribute on keyed-keyboard-layout.");
+ }
+ String layout = parser.getAttributeValue(null, "layout");
+ if (layout == null) {
+ throw new XmlPullParserException(
+ "Missing layout attribute on keyed-keyboard-layout.");
+ }
+ mKeyboardLayoutMap.put(key, layout);
} else if (parser.getName().equals("light-info")) {
int lightId = parser.getAttributeInt(null, "light-id");
int lightBrightness = parser.getAttributeInt(null, "light-brightness");
@@ -607,6 +661,13 @@
serializer.endTag(null, "keyboard-layout");
}
+ for (String key : mKeyboardLayoutMap.keySet()) {
+ serializer.startTag(null, "keyed-keyboard-layout");
+ serializer.attribute(null, "key", key);
+ serializer.attribute(null, "layout", mKeyboardLayoutMap.get(key));
+ serializer.endTag(null, "keyed-keyboard-layout");
+ }
+
for (int i = 0; i < mKeyboardBacklightBrightnessMap.size(); i++) {
serializer.startTag(null, "light-info");
serializer.attributeInt(null, "light-id", mKeyboardBacklightBrightnessMap.keyAt(i));
diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
index 86a0857..e21895a 100644
--- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
@@ -21,7 +21,10 @@
import static com.android.server.EventLogTags.IMF_HIDE_IME;
import static com.android.server.EventLogTags.IMF_SHOW_IME;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_NOT_ALWAYS;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME_IMPLICIT;
import android.annotation.Nullable;
import android.os.Binder;
@@ -30,6 +33,7 @@
import android.util.EventLog;
import android.util.Slog;
import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethodManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.inputmethod.InputMethodDebug;
@@ -124,6 +128,12 @@
@Override
public void applyImeVisibility(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
@ImeVisibilityStateComputer.VisibilityState int state) {
+ applyImeVisibility(windowToken, statsToken, state, -1 /* ignore reason */);
+ }
+
+ @GuardedBy("ImfLock.class")
+ void applyImeVisibility(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ @ImeVisibilityStateComputer.VisibilityState int state, int reason) {
switch (state) {
case STATE_SHOW_IME:
ImeTracker.get().onProgress(statsToken,
@@ -148,6 +158,17 @@
ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
}
break;
+ case STATE_HIDE_IME_EXPLICIT:
+ mService.hideCurrentInputLocked(windowToken, statsToken, 0, null, reason);
+ break;
+ case STATE_HIDE_IME_NOT_ALWAYS:
+ mService.hideCurrentInputLocked(windowToken, statsToken,
+ InputMethodManager.HIDE_NOT_ALWAYS, null, reason);
+ break;
+ case STATE_SHOW_IME_IMPLICIT:
+ mService.showCurrentInputLocked(windowToken, statsToken,
+ InputMethodManager.SHOW_IMPLICIT, null, reason);
+ break;
default:
throw new IllegalArgumentException("Invalid IME visibility state: " + state);
}
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index a2655f4..795e4bf 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -18,11 +18,13 @@
import static android.accessibilityservice.AccessibilityService.SHOW_MODE_HIDDEN;
import static android.server.inputmethod.InputMethodManagerServiceProto.ACCESSIBILITY_REQUESTING_NO_SOFT_KEYBOARD;
+import static android.server.inputmethod.InputMethodManagerServiceProto.INPUT_SHOWN;
import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_EXPLICITLY_REQUESTED;
import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_FORCED;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED;
import static android.view.WindowManager.LayoutParams.SoftInputModeFlags;
@@ -32,6 +34,7 @@
import android.accessibilityservice.AccessibilityService;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.content.res.Configuration;
import android.os.IBinder;
import android.util.PrintWriterPrinter;
import android.util.Printer;
@@ -42,6 +45,8 @@
import android.view.inputmethod.InputMethod;
import android.view.inputmethod.InputMethodManager;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
import com.android.server.LocalServices;
import com.android.server.wm.WindowManagerInternal;
@@ -88,6 +93,11 @@
*/
boolean mShowForced;
+ /**
+ * Set if we last told the input method to show itself.
+ */
+ private boolean mInputShown;
+
/** Represent the invalid IME visibility state */
public static final int STATE_INVALID = -1;
@@ -105,6 +115,12 @@
/** State to handle showing an IME preview surface during the app was loosing the IME focus */
public static final int STATE_SHOW_IME_SNAPSHOT = 4;
+
+ public static final int STATE_HIDE_IME_EXPLICIT = 5;
+
+ public static final int STATE_HIDE_IME_NOT_ALWAYS = 6;
+
+ public static final int STATE_SHOW_IME_IMPLICIT = 7;
@IntDef({
STATE_INVALID,
STATE_HIDE_IME,
@@ -112,6 +128,9 @@
STATE_SHOW_IME_ABOVE_OVERLAY,
STATE_SHOW_IME_BEHIND_OVERLAY,
STATE_SHOW_IME_SNAPSHOT,
+ STATE_HIDE_IME_EXPLICIT,
+ STATE_HIDE_IME_NOT_ALWAYS,
+ STATE_SHOW_IME_IMPLICIT,
})
@interface VisibilityState {}
@@ -120,11 +139,38 @@
*/
private final ImeVisibilityPolicy mPolicy;
- public ImeVisibilityStateComputer(InputMethodManagerService service) {
+ public ImeVisibilityStateComputer(@NonNull InputMethodManagerService service) {
+ this(service,
+ LocalServices.getService(WindowManagerInternal.class),
+ LocalServices.getService(WindowManagerInternal.class)::getDisplayImePolicy,
+ new ImeVisibilityPolicy());
+ }
+
+ @VisibleForTesting
+ public ImeVisibilityStateComputer(@NonNull InputMethodManagerService service,
+ @NonNull Injector injector) {
+ this(service, injector.getWmService(), injector.getImeValidator(),
+ new ImeVisibilityPolicy());
+ }
+
+ interface Injector {
+ default WindowManagerInternal getWmService() {
+ return null;
+ }
+
+ default InputMethodManagerService.ImeDisplayValidator getImeValidator() {
+ return null;
+ }
+ }
+
+ private ImeVisibilityStateComputer(InputMethodManagerService service,
+ WindowManagerInternal wmService,
+ InputMethodManagerService.ImeDisplayValidator imeDisplayValidator,
+ ImeVisibilityPolicy imePolicy) {
mService = service;
- mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
- mImeDisplayValidator = mWindowManagerInternal::getDisplayImePolicy;
- mPolicy = new ImeVisibilityPolicy();
+ mWindowManagerInternal = wmService;
+ mImeDisplayValidator = imeDisplayValidator;
+ mPolicy = imePolicy;
}
/**
@@ -187,6 +233,7 @@
void clearImeShowFlags() {
mRequestedShowExplicitly = false;
mShowForced = false;
+ mInputShown = false;
}
int computeImeDisplayId(@NonNull ImeTargetWindowState state, int displayId) {
@@ -207,15 +254,22 @@
* {@link #STATE_HIDE_IME}.
*/
void requestImeVisibility(IBinder windowToken, boolean showIme) {
- final ImeTargetWindowState state = getOrCreateWindowState(windowToken);
- state.setRequestedImeVisible(showIme);
- setWindowState(windowToken, state);
+ ImeTargetWindowState state = getOrCreateWindowState(windowToken);
+ if (!mPolicy.mPendingA11yRequestingHideKeyboard) {
+ state.setRequestedImeVisible(showIme);
+ } else {
+ // As A11y requests no IME is just a temporary, so we don't change the requested IME
+ // visible in case the last visibility state goes wrong after leaving from the a11y
+ // policy.
+ mPolicy.mPendingA11yRequestingHideKeyboard = false;
+ }
+ setWindowStateInner(windowToken, state);
}
ImeTargetWindowState getOrCreateWindowState(IBinder windowToken) {
ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken);
if (state == null) {
- state = new ImeTargetWindowState(SOFT_INPUT_STATE_UNSPECIFIED, false, false);
+ state = new ImeTargetWindowState(SOFT_INPUT_STATE_UNSPECIFIED, 0, false, false, false);
}
return state;
}
@@ -229,16 +283,176 @@
ImeTargetWindowState state = getWindowStateOrNull(windowToken);
if (state != null) {
state.setRequestImeToken(token);
- setWindowState(windowToken, state);
+ setWindowStateInner(windowToken, state);
}
}
- void setWindowState(IBinder windowToken, ImeTargetWindowState newState) {
- if (DEBUG) Slog.d(TAG, "setWindowState, windowToken=" + windowToken
+ void setWindowState(IBinder windowToken, @NonNull ImeTargetWindowState newState) {
+ final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken);
+ if (state != null && newState.hasEdiorFocused()) {
+ // Inherit the last requested IME visible state when the target window is still
+ // focused with an editor.
+ newState.setRequestedImeVisible(state.mRequestedImeVisible);
+ }
+ setWindowStateInner(windowToken, newState);
+ }
+
+ private void setWindowStateInner(IBinder windowToken, @NonNull ImeTargetWindowState newState) {
+ if (DEBUG) Slog.d(TAG, "setWindowStateInner, windowToken=" + windowToken
+ ", state=" + newState);
mRequestWindowStateMap.put(windowToken, newState);
}
+ static class ImeVisibilityResult {
+ private final @VisibilityState int mState;
+ private final @SoftInputShowHideReason int mReason;
+
+ ImeVisibilityResult(@VisibilityState int state, @SoftInputShowHideReason int reason) {
+ mState = state;
+ mReason = reason;
+ }
+
+ @VisibilityState int getState() {
+ return mState;
+ }
+
+ @SoftInputShowHideReason int getReason() {
+ return mReason;
+ }
+ }
+
+ ImeVisibilityResult computeState(ImeTargetWindowState state, boolean allowVisible) {
+ // TODO: Output the request IME visibility state according to the requested window state
+ final int softInputVisibility = state.mSoftInputModeState & SOFT_INPUT_MASK_STATE;
+ // Should we auto-show the IME even if the caller has not
+ // specified what should be done with it?
+ // We only do this automatically if the window can resize
+ // to accommodate the IME (so what the user sees will give
+ // them good context without input information being obscured
+ // by the IME) or if running on a large screen where there
+ // is more room for the target window + IME.
+ final boolean doAutoShow =
+ (state.mSoftInputModeState & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST)
+ == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+ || mService.mRes.getConfiguration().isLayoutSizeAtLeast(
+ Configuration.SCREENLAYOUT_SIZE_LARGE);
+ final boolean isForwardNavigation = (state.mSoftInputModeState
+ & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0;
+
+ // We shows the IME when the system allows the IME focused target window to restore the
+ // IME visibility (e.g. switching to the app task when last time the IME is visible).
+ // Note that we don't restore IME visibility for some cases (e.g. when the soft input
+ // state is ALWAYS_HIDDEN or STATE_HIDDEN with forward navigation).
+ // Because the app might leverage these flags to hide soft-keyboard with showing their own
+ // UI for input.
+ if (state.hasEdiorFocused() && shouldRestoreImeVisibility(state)) {
+ if (DEBUG) Slog.v(TAG, "Will show input to restore visibility");
+ // Inherit the last requested IME visible state when the target window is still
+ // focused with an editor.
+ state.setRequestedImeVisible(true);
+ setWindowStateInner(getWindowTokenFrom(state), state);
+ return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT,
+ SoftInputShowHideReason.SHOW_RESTORE_IME_VISIBILITY);
+ }
+
+ switch (softInputVisibility) {
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:
+ if (state.hasImeFocusChanged() && (!state.hasEdiorFocused() || !doAutoShow)) {
+ if (WindowManager.LayoutParams.mayUseInputMethod(state.getWindowFlags())) {
+ // There is no focus view, and this window will
+ // be behind any soft input window, so hide the
+ // soft input window if it is shown.
+ if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
+ return new ImeVisibilityResult(STATE_HIDE_IME_NOT_ALWAYS,
+ SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW);
+ }
+ } else if (state.hasEdiorFocused() && doAutoShow && isForwardNavigation) {
+ // There is a focus view, and we are navigating forward
+ // into the window, so show the input window for the user.
+ // We only do this automatically if the window can resize
+ // to accommodate the IME (so what the user sees will give
+ // them good context without input information being obscured
+ // by the IME) or if running on a large screen where there
+ // is more room for the target window + IME.
+ if (DEBUG) Slog.v(TAG, "Unspecified window will show input");
+ return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT,
+ SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
+ // Do nothing.
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN:
+ if (isForwardNavigation) {
+ if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");
+ return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
+ SoftInputShowHideReason.HIDE_STATE_HIDDEN_FORWARD_NAV);
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
+ if (state.hasImeFocusChanged()) {
+ if (DEBUG) Slog.v(TAG, "Window asks to hide input");
+ return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
+ SoftInputShowHideReason.HIDE_ALWAYS_HIDDEN_STATE);
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE:
+ if (isForwardNavigation) {
+ if (allowVisible) {
+ if (DEBUG) Slog.v(TAG, "Window asks to show input going forward");
+ return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT,
+ SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV);
+ } else {
+ Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"
+ + " there is no focused view that also returns true from"
+ + " View#onCheckIsTextEditor()");
+ }
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
+ if (DEBUG) Slog.v(TAG, "Window asks to always show input");
+ if (allowVisible) {
+ if (state.hasImeFocusChanged()) {
+ return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT,
+ SoftInputShowHideReason.SHOW_STATE_ALWAYS_VISIBLE);
+ }
+ } else {
+ Slog.e(TAG, "SOFT_INPUT_STATE_ALWAYS_VISIBLE is ignored because"
+ + " there is no focused view that also returns true from"
+ + " View#onCheckIsTextEditor()");
+ }
+ break;
+ }
+
+ if (!state.hasImeFocusChanged()) {
+ // On previous platforms, when Dialogs re-gained focus, the Activity behind
+ // would briefly gain focus first, and dismiss the IME.
+ // On R that behavior has been fixed, but unfortunately apps have come
+ // to rely on this behavior to hide the IME when the editor no longer has focus
+ // To maintain compatibility, we are now hiding the IME when we don't have
+ // an editor upon refocusing a window.
+ if (state.isStartInputByGainFocus()) {
+ if (DEBUG) Slog.v(TAG, "Same window without editor will hide input");
+ return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
+ SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
+ }
+ }
+ if (!state.hasEdiorFocused() && mInputShown && state.isStartInputByGainFocus()
+ && mService.mInputMethodDeviceConfigs.shouldHideImeWhenNoEditorFocus()) {
+ // Hide the soft-keyboard when the system do nothing for softInputModeState
+ // of the window being gained focus without an editor. This behavior benefits
+ // to resolve some unexpected IME visible cases while that window with following
+ // configurations being switched from an IME shown window:
+ // 1) SOFT_INPUT_STATE_UNCHANGED state without an editor
+ // 2) SOFT_INPUT_STATE_VISIBLE state without an editor
+ // 3) SOFT_INPUT_STATE_ALWAYS_VISIBLE state without an editor
+ if (DEBUG) Slog.v(TAG, "Window without editor will hide input");
+ return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
+ SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR);
+ }
+ return null;
+ }
+
IBinder getWindowTokenFrom(IBinder requestImeToken) {
for (IBinder windowToken : mRequestWindowStateMap.keySet()) {
final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken);
@@ -273,11 +487,20 @@
return mWindowManagerInternal.shouldRestoreImeVisibility(getWindowTokenFrom(state));
}
+ boolean isInputShown() {
+ return mInputShown;
+ }
+
+ void setInputShown(boolean inputShown) {
+ mInputShown = inputShown;
+ }
+
void dumpDebug(ProtoOutputStream proto, long fieldId) {
proto.write(SHOW_EXPLICITLY_REQUESTED, mRequestedShowExplicitly);
proto.write(SHOW_FORCED, mShowForced);
proto.write(ACCESSIBILITY_REQUESTING_NO_SOFT_KEYBOARD,
mPolicy.isA11yRequestNoSoftKeyboard());
+ proto.write(INPUT_SHOWN, mInputShown);
}
void dump(PrintWriter pw) {
@@ -285,6 +508,7 @@
p.println(" mRequestedShowExplicitly=" + mRequestedShowExplicitly
+ " mShowForced=" + mShowForced);
p.println(" mImeHiddenByDisplayPolicy=" + mPolicy.isImeHiddenByDisplayPolicy());
+ p.println(" mInputShown=" + mInputShown);
}
/**
@@ -309,6 +533,14 @@
*/
private boolean mA11yRequestingNoSoftKeyboard;
+ /**
+ * Used when A11y request to hide IME temporary when receiving
+ * {@link AccessibilityService#SHOW_MODE_HIDDEN} from
+ * {@link android.provider.Settings.Secure#ACCESSIBILITY_SOFT_KEYBOARD_MODE} without
+ * changing the requested IME visible state.
+ */
+ private boolean mPendingA11yRequestingHideKeyboard;
+
void setImeHiddenByDisplayPolicy(boolean hideIme) {
mImeHiddenByDisplayPolicy = hideIme;
}
@@ -320,6 +552,9 @@
void setA11yRequestNoSoftKeyboard(int keyboardShowMode) {
mA11yRequestingNoSoftKeyboard =
(keyboardShowMode & AccessibilityService.SHOW_MODE_MASK) == SHOW_MODE_HIDDEN;
+ if (mA11yRequestingNoSoftKeyboard) {
+ mPendingA11yRequestingHideKeyboard = true;
+ }
}
boolean isA11yRequestNoSoftKeyboard() {
@@ -335,11 +570,14 @@
* A class that represents the current state of the IME target window.
*/
static class ImeTargetWindowState {
- ImeTargetWindowState(@SoftInputModeFlags int softInputModeState, boolean imeFocusChanged,
- boolean hasFocusedEditor) {
+ ImeTargetWindowState(@SoftInputModeFlags int softInputModeState, int windowFlags,
+ boolean imeFocusChanged, boolean hasFocusedEditor,
+ boolean isStartInputByGainFocus) {
mSoftInputModeState = softInputModeState;
+ mWindowFlags = windowFlags;
mImeFocusChanged = imeFocusChanged;
mHasFocusedEditor = hasFocusedEditor;
+ mIsStartInputByGainFocus = isStartInputByGainFocus;
}
/**
@@ -347,6 +585,8 @@
*/
private final @SoftInputModeFlags int mSoftInputModeState;
+ private final int mWindowFlags;
+
/**
* {@code true} means the IME focus changed from the previous window, {@code false}
* otherwise.
@@ -358,6 +598,8 @@
*/
private final boolean mHasFocusedEditor;
+ private final boolean mIsStartInputByGainFocus;
+
/**
* Set if the client has asked for the input method to be shown.
*/
@@ -382,10 +624,18 @@
return mHasFocusedEditor;
}
+ boolean isStartInputByGainFocus() {
+ return mIsStartInputByGainFocus;
+ }
+
int getSoftInputModeState() {
return mSoftInputModeState;
}
+ int getWindowFlags() {
+ return mWindowFlags;
+ }
+
private void setImeDisplayId(int imeDisplayId) {
mImeDisplayId = imeDisplayId;
}
@@ -418,6 +668,7 @@
+ " requestedImeVisible " + mRequestedImeVisible
+ " imeDisplayId " + mImeDisplayId
+ " softInputModeState " + softInputModeToString(mSoftInputModeState)
+ + " isStartInputByGainFocus " + mIsStartInputByGainFocus
+ "}";
}
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index 079234c..187de930 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -350,7 +350,7 @@
// should now try to restart the service for us.
mLastBindTime = SystemClock.uptimeMillis();
clearCurMethodAndSessions();
- mService.clearInputShowRequestLocked();
+ mService.clearInputShownLocked();
mService.unbindCurrentClientLocked(UnbindReason.DISCONNECT_IME);
}
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 2dbbb10..a94c90c 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -33,13 +33,11 @@
import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_TOKEN_DISPLAY_ID;
import static android.server.inputmethod.InputMethodManagerServiceProto.HAVE_CONNECTION;
import static android.server.inputmethod.InputMethodManagerServiceProto.IME_WINDOW_VISIBILITY;
-import static android.server.inputmethod.InputMethodManagerServiceProto.INPUT_SHOWN;
import static android.server.inputmethod.InputMethodManagerServiceProto.IN_FULLSCREEN_MODE;
import static android.server.inputmethod.InputMethodManagerServiceProto.IS_INTERACTIVE;
import static android.server.inputmethod.InputMethodManagerServiceProto.LAST_IME_TARGET_WINDOW_NAME;
import static android.server.inputmethod.InputMethodManagerServiceProto.LAST_SWITCH_USER_ID;
import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_IME_WITH_HARD_KEYBOARD;
-import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_REQUESTED;
import static android.server.inputmethod.InputMethodManagerServiceProto.SYSTEM_READY;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
@@ -47,6 +45,7 @@
import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult;
import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT;
import static com.android.server.inputmethod.InputMethodUtils.isSoftInputModeStateVisibleAllowed;
@@ -77,7 +76,6 @@
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
-import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Matrix;
@@ -624,16 +622,6 @@
return mBindingController.hasConnection();
}
- /**
- * Set if the client has asked for the input method to be shown.
- */
- private boolean mShowRequested;
-
- /**
- * Set if we last told the input method to show itself.
- */
- private boolean mInputShown;
-
/** The token tracking the current IME request or {@code null} otherwise. */
@Nullable
private ImeTracker.Token mCurStatsToken;
@@ -689,7 +677,7 @@
* The display ID of the input method indicates the fallback display which returned by
* {@link #computeImeDisplayIdForTarget}.
*/
- private static final int FALLBACK_DISPLAY_ID = DEFAULT_DISPLAY;
+ static final int FALLBACK_DISPLAY_ID = DEFAULT_DISPLAY;
/**
* If non-null, this is the input method service we are currently connected
@@ -1174,12 +1162,10 @@
mVisibilityStateComputer.getImePolicy().setA11yRequestNoSoftKeyboard(
accessibilitySoftKeyboardSetting);
if (mVisibilityStateComputer.getImePolicy().isA11yRequestNoSoftKeyboard()) {
- final boolean showRequested = mShowRequested;
hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
0 /* flags */, null /* resultReceiver */,
SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE);
- mShowRequested = showRequested;
- } else if (mShowRequested) {
+ } else if (isShowRequestedForCurrentWindow()) {
showCurrentInputImplicitLocked(mCurFocusedWindow,
SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE);
}
@@ -2299,9 +2285,20 @@
}
@GuardedBy("ImfLock.class")
- void clearInputShowRequestLocked() {
- mShowRequested = mInputShown;
- mInputShown = false;
+ void clearInputShownLocked() {
+ mVisibilityStateComputer.setInputShown(false);
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean isInputShown() {
+ return mVisibilityStateComputer.isInputShown();
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean isShowRequestedForCurrentWindow() {
+ final ImeTargetWindowState state = mVisibilityStateComputer.getWindowStateOrNull(
+ mCurFocusedWindow);
+ return state != null && state.isRequestedImeVisible();
}
@GuardedBy("ImfLock.class")
@@ -2340,7 +2337,7 @@
setEnabledSessionLocked(session);
session.mMethod.startInput(startInputToken, mCurInputConnection, mCurEditorInfo, restarting,
navButtonFlags, mCurImeDispatcher);
- if (mShowRequested) {
+ if (isShowRequestedForCurrentWindow()) {
if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
// Re-use current statsToken, if it exists.
final ImeTracker.Token statsToken = mCurStatsToken;
@@ -2559,7 +2556,7 @@
if (!mPreventImeStartupUnlessTextEditor) {
return false;
}
- if (mShowRequested) {
+ if (isShowRequestedForCurrentWindow()) {
return false;
}
if (isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags)) {
@@ -3370,9 +3367,6 @@
ImeTracker.ORIGIN_SERVER_START_INPUT, reason);
}
- // TODO(b/246309664): make mShowRequested as per-window state.
- mShowRequested = true;
-
if (!mVisibilityStateComputer.onImeShowFlags(statsToken, flags)) {
return false;
}
@@ -3398,8 +3392,7 @@
}
mVisibilityApplier.performShowIme(windowToken, statsToken,
mVisibilityStateComputer.getImeShowFlags(), resultReceiver, reason);
- // TODO(b/246309664): make mInputShown tracked by the Ime visibility computer.
- mInputShown = true;
+ mVisibilityStateComputer.setInputShown(true);
return true;
} else {
ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
@@ -3417,7 +3410,7 @@
"InputMethodManagerService#hideSoftInput");
synchronized (ImfLock.class) {
if (!canInteractWithImeLocked(uid, client, "hideSoftInput", statsToken)) {
- if (mInputShown) {
+ if (isInputShown()) {
ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
} else {
ImeTracker.get().onCancelled(statsToken,
@@ -3468,10 +3461,10 @@
// application process as a valid request, and have even promised such a behavior with CTS
// since Android Eclair. That's why we need to accept IMM#hideSoftInput() even when only
// IMMS#InputShown indicates that the software keyboard is shown.
- // TODO(b/246309664): Clean up, IMMS#mInputShown, IMMS#mImeWindowVis and mShowRequested.
+ // TODO(b/246309664): Clean up IMMS#mImeWindowVis
IInputMethodInvoker curMethod = getCurMethodLocked();
- final boolean shouldHideSoftInput = (curMethod != null)
- && (mInputShown || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
+ final boolean shouldHideSoftInput = curMethod != null
+ && (isInputShown() || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
mVisibilityStateComputer.requestImeVisibility(windowToken, false);
if (shouldHideSoftInput) {
@@ -3486,8 +3479,6 @@
}
mBindingController.setCurrentMethodNotVisible();
mVisibilityStateComputer.clearImeShowFlags();
- mInputShown = false;
- mShowRequested = false;
// Cancel existing statsToken for show IME as we got a hide request.
ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
mCurStatsToken = null;
@@ -3666,8 +3657,8 @@
// Init the focused window state (e.g. whether the editor has focused or IME focus has
// changed from another window).
- final ImeTargetWindowState windowState = new ImeTargetWindowState(
- softInputMode, !sameWindowFocused, isTextEditor);
+ final ImeTargetWindowState windowState = new ImeTargetWindowState(softInputMode,
+ windowFlags, !sameWindowFocused, isTextEditor, startInputByWinGainedFocus);
mVisibilityStateComputer.setWindowState(windowToken, windowState);
if (sameWindowFocused && isTextEditor) {
@@ -3692,74 +3683,21 @@
mCurFocusedWindowClient = cs;
mCurPerceptible = true;
- // Should we auto-show the IME even if the caller has not
- // specified what should be done with it?
- // We only do this automatically if the window can resize
- // to accommodate the IME (so what the user sees will give
- // them good context without input information being obscured
- // by the IME) or if running on a large screen where there
- // is more room for the target window + IME.
- final boolean doAutoShow =
- (softInputMode & LayoutParams.SOFT_INPUT_MASK_ADJUST)
- == LayoutParams.SOFT_INPUT_ADJUST_RESIZE
- || mRes.getConfiguration().isLayoutSizeAtLeast(
- Configuration.SCREENLAYOUT_SIZE_LARGE);
-
// We want to start input before showing the IME, but after closing
// it. We want to do this after closing it to help the IME disappear
// more quickly (not get stuck behind it initializing itself for the
// new focused input, even if its window wants to hide the IME).
boolean didStart = false;
-
InputBindResult res = null;
- // We show the IME when the system allows the IME focused target window to restore the
- // IME visibility (e.g. switching to the app task when last time the IME is visible).
- // Note that we don't restore IME visibility for some cases (e.g. when the soft input
- // state is ALWAYS_HIDDEN or STATE_HIDDEN with forward navigation).
- // Because the app might leverage these flags to hide soft-keyboard with showing their own
- // UI for input.
- if (isTextEditor && editorInfo != null
- && mVisibilityStateComputer.shouldRestoreImeVisibility(windowState)) {
- if (DEBUG) Slog.v(TAG, "Will show input to restore visibility");
- res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection,
- editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion,
- imeDispatcher);
- showCurrentInputImplicitLocked(windowToken,
- SoftInputShowHideReason.SHOW_RESTORE_IME_VISIBILITY);
- return res;
- }
- switch (softInputMode & LayoutParams.SOFT_INPUT_MASK_STATE) {
- case LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:
- if (!sameWindowFocused && (!isTextEditor || !doAutoShow)) {
- if (LayoutParams.mayUseInputMethod(windowFlags)) {
- // There is no focus view, and this window will
- // be behind any soft input window, so hide the
- // soft input window if it is shown.
- if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
- hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
- InputMethodManager.HIDE_NOT_ALWAYS, null /* resultReceiver */,
- SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW);
-
- // If focused display changed, we should unbind current method
- // to make app window in previous display relayout after Ime
- // window token removed.
- // Note that we can trust client's display ID as long as it matches
- // to the display ID obtained from the window.
- if (cs.mSelfReportedDisplayId != mCurTokenDisplayId) {
- mBindingController.unbindCurrentMethod();
- }
- }
- } else if (isTextEditor && doAutoShow
- && (softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
- // There is a focus view, and we are navigating forward
- // into the window, so show the input window for the user.
- // We only do this automatically if the window can resize
- // to accommodate the IME (so what the user sees will give
- // them good context without input information being obscured
- // by the IME) or if running on a large screen where there
- // is more room for the target window + IME.
- if (DEBUG) Slog.v(TAG, "Unspecified window will show input");
+ final ImeVisibilityResult imeVisRes = mVisibilityStateComputer.computeState(windowState,
+ isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags));
+ if (imeVisRes != null) {
+ switch (imeVisRes.getReason()) {
+ case SoftInputShowHideReason.SHOW_RESTORE_IME_VISIBILITY:
+ case SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV:
+ case SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV:
+ case SoftInputShowHideReason.SHOW_STATE_ALWAYS_VISIBLE:
if (editorInfo != null) {
res = startInputUncheckedLocked(cs, inputContext,
remoteAccessibilityInputConnection, editorInfo, startInputFlags,
@@ -3767,106 +3705,25 @@
imeDispatcher);
didStart = true;
}
- showCurrentInputImplicitLocked(windowToken,
- SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
- }
- break;
- case LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
- if (DEBUG) {
- Slog.v(TAG, "Window asks to keep the input in whatever state it was last in");
- }
- // Do nothing.
- break;
- case LayoutParams.SOFT_INPUT_STATE_HIDDEN:
- if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
- if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");
- hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
- null /* resultReceiver */,
- SoftInputShowHideReason.HIDE_STATE_HIDDEN_FORWARD_NAV);
- }
- break;
- case LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
- if (!sameWindowFocused) {
- if (DEBUG) Slog.v(TAG, "Window asks to hide input");
- hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
- null /* resultReceiver */,
- SoftInputShowHideReason.HIDE_ALWAYS_HIDDEN_STATE);
- }
- break;
- case LayoutParams.SOFT_INPUT_STATE_VISIBLE:
- if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
- if (DEBUG) Slog.v(TAG, "Window asks to show input going forward");
- if (isSoftInputModeStateVisibleAllowed(
- unverifiedTargetSdkVersion, startInputFlags)) {
- if (editorInfo != null) {
- res = startInputUncheckedLocked(cs, inputContext,
- remoteAccessibilityInputConnection, editorInfo, startInputFlags,
- startInputReason, unverifiedTargetSdkVersion,
- imeDispatcher);
- didStart = true;
- }
- showCurrentInputImplicitLocked(windowToken,
- SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV);
- } else {
- Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"
- + " there is no focused view that also returns true from"
- + " View#onCheckIsTextEditor()");
- }
- }
- break;
- case LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
- if (DEBUG) Slog.v(TAG, "Window asks to always show input");
- if (isSoftInputModeStateVisibleAllowed(
- unverifiedTargetSdkVersion, startInputFlags)) {
- if (!sameWindowFocused) {
- if (editorInfo != null) {
- res = startInputUncheckedLocked(cs, inputContext,
- remoteAccessibilityInputConnection, editorInfo, startInputFlags,
- startInputReason, unverifiedTargetSdkVersion,
- imeDispatcher);
- didStart = true;
- }
- showCurrentInputImplicitLocked(windowToken,
- SoftInputShowHideReason.SHOW_STATE_ALWAYS_VISIBLE);
- }
- } else {
- Slog.e(TAG, "SOFT_INPUT_STATE_ALWAYS_VISIBLE is ignored because"
- + " there is no focused view that also returns true from"
- + " View#onCheckIsTextEditor()");
- }
- break;
- }
+ break;
+ }
+ mVisibilityApplier.applyImeVisibility(mCurFocusedWindow, null /* statsToken */,
+ imeVisRes.getState(), imeVisRes.getReason());
+
+ if (imeVisRes.getReason() == SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW) {
+ // If focused display changed, we should unbind current method
+ // to make app window in previous display relayout after Ime
+ // window token removed.
+ // Note that we can trust client's display ID as long as it matches
+ // to the display ID obtained from the window.
+ if (cs.mSelfReportedDisplayId != mCurTokenDisplayId) {
+ mBindingController.unbindCurrentMethod();
+ }
+ }
+ }
if (!didStart) {
if (editorInfo != null) {
- if (sameWindowFocused) {
- // On previous platforms, when Dialogs re-gained focus, the Activity behind
- // would briefly gain focus first, and dismiss the IME.
- // On R that behavior has been fixed, but unfortunately apps have come
- // to rely on this behavior to hide the IME when the editor no longer has focus
- // To maintain compatibility, we are now hiding the IME when we don't have
- // an editor upon refocusing a window.
- if (startInputByWinGainedFocus) {
- if (DEBUG) Slog.v(TAG, "Same window without editor will hide input");
- hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
- 0 /* flags */, null /* resultReceiver */,
- SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
- }
- }
- if (!isTextEditor && mInputShown && startInputByWinGainedFocus
- && mInputMethodDeviceConfigs.shouldHideImeWhenNoEditorFocus()) {
- // Hide the soft-keyboard when the system do nothing for softInputModeState
- // of the window being gained focus without an editor. This behavior benefits
- // to resolve some unexpected IME visible cases while that window with following
- // configurations being switched from an IME shown window:
- // 1) SOFT_INPUT_STATE_UNCHANGED state without an editor
- // 2) SOFT_INPUT_STATE_VISIBLE state without an editor
- // 3) SOFT_INPUT_STATE_ALWAYS_VISIBLE state without an editor
- if (DEBUG) Slog.v(TAG, "Window without editor will hide input");
- hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
- null /* resultReceiver */,
- SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR);
- }
res = startInputUncheckedLocked(cs, inputContext,
remoteAccessibilityInputConnection, editorInfo, startInputFlags,
startInputReason, unverifiedTargetSdkVersion,
@@ -4639,9 +4496,7 @@
mCurEditorInfo.dumpDebug(proto, CUR_ATTRIBUTE);
}
proto.write(CUR_ID, getCurIdLocked());
- proto.write(SHOW_REQUESTED, mShowRequested);
mVisibilityStateComputer.dumpDebug(proto, fieldId);
- proto.write(INPUT_SHOWN, mInputShown);
proto.write(IN_FULLSCREEN_MODE, mInFullscreenMode);
proto.write(CUR_TOKEN, Objects.toString(getCurTokenLocked()));
proto.write(CUR_TOKEN_DISPLAY_ID, mCurTokenDisplayId);
@@ -4785,6 +4640,16 @@
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
+ @VisibleForTesting
+ ImeVisibilityStateComputer getVisibilityStateComputer() {
+ return mVisibilityStateComputer;
+ }
+
+ @VisibleForTesting
+ ImeVisibilityApplier getVisibilityApplier() {
+ return mVisibilityApplier;
+ }
+
@GuardedBy("ImfLock.class")
void setEnabledSessionLocked(SessionState session) {
if (mEnabledSession != session) {
@@ -4847,7 +4712,9 @@
// This is undocumented so far, but IMM#showInputMethodPicker() has been
// implemented so that auxiliary subtypes will be excluded when the soft
// keyboard is invisible.
- showAuxSubtypes = mInputShown;
+ synchronized (ImfLock.class) {
+ showAuxSubtypes = isInputShown();
+ }
break;
case InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES:
showAuxSubtypes = true;
@@ -4876,7 +4743,7 @@
synchronized (ImfLock.class) {
try {
if (mEnabledSession != null && mEnabledSession.mSession != null
- && !mShowRequested) {
+ && !isShowRequestedForCurrentWindow()) {
mEnabledSession.mSession.removeImeSurface();
}
} catch (RemoteException e) {
@@ -5864,7 +5731,6 @@
method = getCurMethodLocked();
p.println(" mCurMethod=" + getCurMethodLocked());
p.println(" mEnabledSession=" + mEnabledSession);
- p.println(" mShowRequested=" + mShowRequested + " mInputShown=" + mInputShown);
mVisibilityStateComputer.dump(pw);
p.println(" mInFullscreenMode=" + mInFullscreenMode);
p.println(" mSystemReady=" + mSystemReady + " mInteractive=" + mIsInteractive);
diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
index c90554d..8182fe9 100644
--- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java
+++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
@@ -70,11 +70,13 @@
private final BluetoothAdapter mBluetoothAdapter;
private final BluetoothRoutesUpdatedListener mListener;
private final AudioManager mAudioManager;
- private final Map<String, BluetoothEventReceiver> mEventReceiverMap = new HashMap<>();
- private final IntentFilter mIntentFilter = new IntentFilter();
- private final BroadcastReceiver mBroadcastReceiver = new BluetoothBroadcastReceiver();
private final BluetoothProfileListener mProfileListener = new BluetoothProfileListener();
+ private final AdapterStateChangedReceiver mAdapterStateChangedReceiver =
+ new AdapterStateChangedReceiver();
+ private final DeviceStateChangedReceiver mDeviceStateChangedReceiver =
+ new DeviceStateChangedReceiver();
+
private BluetoothA2dp mA2dpProfile;
private BluetoothHearingAid mHearingAidProfile;
private BluetoothLeAudio mLeAudioProfile;
@@ -105,32 +107,45 @@
buildBluetoothRoutes();
}
+ /**
+ * Registers listener to bluetooth status changes as the provided user.
+ *
+ * The registered receiver listens to {@link BluetoothA2dp#ACTION_ACTIVE_DEVICE_CHANGED} and
+ * {@link BluetoothA2dp#ACTION_CONNECTION_STATE_CHANGED } events for {@link BluetoothProfile#A2DP},
+ * {@link BluetoothProfile#HEARING_AID}, and {@link BluetoothProfile#LE_AUDIO} bluetooth profiles.
+ *
+ * @param user {@code UserHandle} as which receiver is registered
+ */
void start(UserHandle user) {
mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP);
mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEARING_AID);
mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.LE_AUDIO);
- // Bluetooth on/off broadcasts
- addEventReceiver(BluetoothAdapter.ACTION_STATE_CHANGED, new AdapterStateChangedReceiver());
+ IntentFilter adapterStateChangedIntentFilter = new IntentFilter();
- DeviceStateChangedReceiver deviceStateChangedReceiver = new DeviceStateChangedReceiver();
- addEventReceiver(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED, deviceStateChangedReceiver);
- addEventReceiver(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED, deviceStateChangedReceiver);
- addEventReceiver(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,
- deviceStateChangedReceiver);
- addEventReceiver(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED,
- deviceStateChangedReceiver);
- addEventReceiver(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED,
- deviceStateChangedReceiver);
- addEventReceiver(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED,
- deviceStateChangedReceiver);
+ adapterStateChangedIntentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+ mContext.registerReceiverAsUser(mAdapterStateChangedReceiver, user,
+ adapterStateChangedIntentFilter, null, null);
- mContext.registerReceiverAsUser(mBroadcastReceiver, user,
- mIntentFilter, null, null);
+ IntentFilter deviceStateChangedIntentFilter = new IntentFilter();
+
+ deviceStateChangedIntentFilter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
+ deviceStateChangedIntentFilter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+ deviceStateChangedIntentFilter.addAction(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
+ deviceStateChangedIntentFilter.addAction(
+ BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
+ deviceStateChangedIntentFilter.addAction(
+ BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
+ deviceStateChangedIntentFilter.addAction(
+ BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED);
+
+ mContext.registerReceiverAsUser(mDeviceStateChangedReceiver, user,
+ deviceStateChangedIntentFilter, null, null);
}
void stop() {
- mContext.unregisterReceiver(mBroadcastReceiver);
+ mContext.unregisterReceiver(mAdapterStateChangedReceiver);
+ mContext.unregisterReceiver(mDeviceStateChangedReceiver);
}
/**
@@ -167,11 +182,6 @@
}
}
- private void addEventReceiver(String action, BluetoothEventReceiver eventReceiver) {
- mEventReceiverMap.put(action, eventReceiver);
- mIntentFilter.addAction(action);
- }
-
private void buildBluetoothRoutes() {
mBluetoothRoutes.clear();
Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices();
@@ -495,26 +505,9 @@
}
}
- private class BluetoothBroadcastReceiver extends BroadcastReceiver {
+ private class AdapterStateChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, android.bluetooth.BluetoothDevice.class);
-
- BluetoothEventReceiver receiver = mEventReceiverMap.get(action);
- if (receiver != null) {
- receiver.onReceive(context, intent, device);
- }
- }
- }
-
- private interface BluetoothEventReceiver {
- void onReceive(Context context, Intent intent, BluetoothDevice device);
- }
-
- private class AdapterStateChangedReceiver implements BluetoothEventReceiver {
- @Override
- public void onReceive(Context context, Intent intent, BluetoothDevice device) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
if (state == BluetoothAdapter.STATE_OFF
|| state == BluetoothAdapter.STATE_TURNING_OFF) {
@@ -529,9 +522,12 @@
}
}
- private class DeviceStateChangedReceiver implements BluetoothEventReceiver {
+ private class DeviceStateChangedReceiver extends BroadcastReceiver {
@Override
- public void onReceive(Context context, Intent intent, BluetoothDevice device) {
+ public void onReceive(Context context, Intent intent) {
+ BluetoothDevice device = intent.getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE, android.bluetooth.BluetoothDevice.class);
+
switch (intent.getAction()) {
case BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED:
clearActiveRoutesWithType(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
diff --git a/services/core/java/com/android/server/notification/NotificationShellCmd.java b/services/core/java/com/android/server/notification/NotificationShellCmd.java
index 628a322..dc0cf4e 100644
--- a/services/core/java/com/android/server/notification/NotificationShellCmd.java
+++ b/services/core/java/com/android/server/notification/NotificationShellCmd.java
@@ -540,16 +540,16 @@
if ("broadcast".equals(intentKind)) {
pi = PendingIntent.getBroadcastAsUser(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
- | PendingIntent.FLAG_MUTABLE_UNAUDITED,
+ | PendingIntent.FLAG_IMMUTABLE,
UserHandle.CURRENT);
} else if ("service".equals(intentKind)) {
pi = PendingIntent.getService(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
- | PendingIntent.FLAG_MUTABLE_UNAUDITED);
+ | PendingIntent.FLAG_IMMUTABLE);
} else {
pi = PendingIntent.getActivityAsUser(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
- | PendingIntent.FLAG_MUTABLE_UNAUDITED, null,
+ | PendingIntent.FLAG_IMMUTABLE, null,
UserHandle.CURRENT);
}
builder.setContentIntent(pi);
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
index 71593e1..5e62b56 100644
--- a/services/core/java/com/android/server/pm/ApexManager.java
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -138,7 +138,7 @@
this.activeApexChanged = activeApexChanged;
}
- private ActiveApexInfo(ApexInfo apexInfo) {
+ public ActiveApexInfo(ApexInfo apexInfo) {
this(
apexInfo.moduleName,
new File(Environment.getApexDirectory() + File.separator
diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
index 9785d47..f85d6af 100644
--- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java
+++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
@@ -405,11 +405,15 @@
new TimingsTraceAndSlog(TAG, Trace.TRACE_TAG_PACKAGE_MANAGER);
tr.traceBegin("jobExecution");
boolean completed = false;
+ boolean fatalError = false;
try {
completed = runIdleOptimization(
pm, pkgs, params.getJobId() == JOB_POST_BOOT_UPDATE);
} catch (LegacyDexoptDisabledException e) {
Slog.wtf(TAG, e);
+ } catch (RuntimeException e) {
+ fatalError = true;
+ throw e;
} finally { // Those cleanup should be done always.
tr.traceEnd();
Slog.i(TAG,
@@ -422,12 +426,10 @@
if (completed) {
markPostBootUpdateCompleted(params);
}
- // Reschedule when cancelled
- job.jobFinished(params, !completed);
- } else {
- // Periodic job
- job.jobFinished(params, false /* reschedule */);
}
+ // Reschedule when cancelled. No need to reschedule when failed with
+ // fatal error because it's likely to fail again.
+ job.jobFinished(params, !completed && !fatalError);
markDexOptCompleted();
}
}));
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index db3a343..69436da 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -22,6 +22,7 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IBackgroundInstallControlService;
+import android.content.pm.InstallSourceInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
@@ -183,10 +184,12 @@
return;
}
- String installerPackageName = null;
+ String installerPackageName;
+ String initiatingPackageName;
try {
- installerPackageName = mPackageManager
- .getInstallSourceInfo(packageName).getInstallingPackageName();
+ final InstallSourceInfo installInfo = mPackageManager.getInstallSourceInfo(packageName);
+ installerPackageName = installInfo.getInstallingPackageName();
+ initiatingPackageName = installInfo.getInitiatingPackageName();
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, "Package's installer not found " + packageName);
return;
@@ -196,7 +199,8 @@
final long installTimestamp = System.currentTimeMillis()
- (SystemClock.uptimeMillis() - appInfo.createTimestamp);
- if (wasForegroundInstallation(installerPackageName, userId, installTimestamp)) {
+ if (installedByAdb(initiatingPackageName)
+ || wasForegroundInstallation(installerPackageName, userId, installTimestamp)) {
return;
}
@@ -205,6 +209,12 @@
writeBackgroundInstalledPackagesToDisk();
}
+ // ADB sets installerPackageName to null, this creates a loophole to bypass BIC which will be
+ // addressed with b/265203007
+ private boolean installedByAdb(String initiatingPackageName) {
+ return initiatingPackageName == null;
+ }
+
private boolean wasForegroundInstallation(String installerPackageName,
int userId, long installTimestamp) {
TreeSet<BackgroundInstallControlService.ForegroundTimeFrame> foregroundTimeFrames =
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index cf447a7..9da319d 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -65,8 +65,8 @@
import com.android.server.art.ArtManagerLocal;
import com.android.server.art.DexUseManagerLocal;
import com.android.server.art.model.ArtFlags;
-import com.android.server.art.model.OptimizeParams;
-import com.android.server.art.model.OptimizeResult;
+import com.android.server.art.model.DexoptParams;
+import com.android.server.art.model.DexoptResult;
import com.android.server.pm.Installer.InstallerException;
import com.android.server.pm.Installer.LegacyDexoptDisabledException;
import com.android.server.pm.PackageDexOptimizer.DexOptResult;
@@ -502,7 +502,7 @@
* necessary to fall back to the legacy code paths.
*/
private Optional<Integer> performDexOptWithArtService(DexoptOptions options,
- /*@OptimizeFlags*/ int extraFlags) {
+ /*@DexoptFlags*/ int extraFlags) {
ArtManagerLocal artManager = getArtManagerLocal();
if (artManager == null) {
return Optional.empty();
@@ -522,14 +522,14 @@
return Optional.of(PackageDexOptimizer.DEX_OPT_SKIPPED);
}
- OptimizeParams params = options.convertToOptimizeParams(extraFlags);
+ DexoptParams params = options.convertToDexoptParams(extraFlags);
if (params == null) {
return Optional.empty();
}
- OptimizeResult result;
+ DexoptResult result;
try {
- result = artManager.optimizePackage(snapshot, options.getPackageName(), params);
+ result = artManager.dexoptPackage(snapshot, options.getPackageName(), params);
} catch (UnsupportedOperationException e) {
reportArtManagerFallback(options.getPackageName(), e.toString());
return Optional.empty();
@@ -954,22 +954,21 @@
}
}
- private static class OptimizePackageDoneHandler
- implements ArtManagerLocal.OptimizePackageDoneCallback {
+ private static class DexoptDoneHandler implements ArtManagerLocal.DexoptDoneCallback {
@NonNull private final PackageManagerService mPm;
- OptimizePackageDoneHandler(@NonNull PackageManagerService pm) { mPm = pm; }
+ DexoptDoneHandler(@NonNull PackageManagerService pm) { mPm = pm; }
/**
- * Called after every package optimization operation done by {@link ArtManagerLocal}.
+ * Called after every package dexopt operation done by {@link ArtManagerLocal}.
*/
@Override
- public void onOptimizePackageDone(@NonNull OptimizeResult result) {
- for (OptimizeResult.PackageOptimizeResult pkgRes : result.getPackageOptimizeResults()) {
+ public void onDexoptDone(@NonNull DexoptResult result) {
+ for (DexoptResult.PackageDexoptResult pkgRes : result.getPackageDexoptResults()) {
CompilerStats.PackageStats stats =
mPm.getOrCreateCompilerPackageStats(pkgRes.getPackageName());
- for (OptimizeResult.DexContainerFileOptimizeResult dexRes :
- pkgRes.getDexContainerFileOptimizeResults()) {
+ for (DexoptResult.DexContainerFileDexoptResult dexRes :
+ pkgRes.getDexContainerFileDexoptResults()) {
stats.setCompileTime(
dexRes.getDexContainerFile(), dexRes.getDex2oatWallTimeMillis());
}
@@ -994,13 +993,13 @@
ArtManagerLocal artManager = new ArtManagerLocal(systemContext);
// There doesn't appear to be any checks that @NonNull is heeded, so use requireNonNull
// below to ensure we don't store away a null that we'll fail on later.
- artManager.addOptimizePackageDoneCallback(false /* onlyIncludeUpdates */,
- Runnable::run, new OptimizePackageDoneHandler(Objects.requireNonNull(pm)));
+ artManager.addDexoptDoneCallback(false /* onlyIncludeUpdates */,
+ Runnable::run, new DexoptDoneHandler(Objects.requireNonNull(pm)));
LocalManagerRegistry.addManager(ArtManagerLocal.class, artManager);
}
/**
- * Returns {@link ArtManagerLocal} if ART Service should be used for package optimization.
+ * Returns {@link ArtManagerLocal} if ART Service should be used for package dexopt.
*/
private static @Nullable ArtManagerLocal getArtManagerLocal() {
if (!useArtService()) {
@@ -1014,25 +1013,25 @@
}
/**
- * Converts an ART Service {@link OptimizeResult} to {@link DexOptResult}.
+ * Converts an ART Service {@link DexoptResult} to {@link DexOptResult}.
*
* For interfacing {@link ArtManagerLocal} with legacy dex optimization code in PackageManager.
*/
@DexOptResult
- private static int convertToDexOptResult(OptimizeResult result) {
- /*@OptimizeStatus*/ int status = result.getFinalStatus();
+ private static int convertToDexOptResult(DexoptResult result) {
+ /*@DexoptResultStatus*/ int status = result.getFinalStatus();
switch (status) {
- case OptimizeResult.OPTIMIZE_SKIPPED:
+ case DexoptResult.DEXOPT_SKIPPED:
return PackageDexOptimizer.DEX_OPT_SKIPPED;
- case OptimizeResult.OPTIMIZE_FAILED:
+ case DexoptResult.DEXOPT_FAILED:
return PackageDexOptimizer.DEX_OPT_FAILED;
- case OptimizeResult.OPTIMIZE_PERFORMED:
+ case DexoptResult.DEXOPT_PERFORMED:
return PackageDexOptimizer.DEX_OPT_PERFORMED;
- case OptimizeResult.OPTIMIZE_CANCELLED:
+ case DexoptResult.DEXOPT_CANCELLED:
return PackageDexOptimizer.DEX_OPT_CANCELLED;
default:
- throw new IllegalArgumentException("OptimizeResult for "
- + result.getPackageOptimizeResults().get(0).getPackageName()
+ throw new IllegalArgumentException("DexoptResult for "
+ + result.getPackageDexoptResults().get(0).getPackageName()
+ " has unsupported status " + status);
}
}
diff --git a/services/core/java/com/android/server/pm/DumpHelper.java b/services/core/java/com/android/server/pm/DumpHelper.java
index 3385a09..fcaaa90 100644
--- a/services/core/java/com/android/server/pm/DumpHelper.java
+++ b/services/core/java/com/android/server/pm/DumpHelper.java
@@ -111,6 +111,8 @@
dumpState.setOptionEnabled(DumpState.OPTION_DUMP_ALL_COMPONENTS);
} else if ("-f".equals(opt)) {
dumpState.setOptionEnabled(DumpState.OPTION_SHOW_FILTERS);
+ } else if ("--include-apex".equals(opt)) {
+ dumpState.setOptionEnabled(DumpState.OPTION_INCLUDE_APEX);
} else if ("--proto".equals(opt)) {
dumpProto(snapshot, fd);
return;
diff --git a/services/core/java/com/android/server/pm/DumpState.java b/services/core/java/com/android/server/pm/DumpState.java
index 6225753..0bdce21 100644
--- a/services/core/java/com/android/server/pm/DumpState.java
+++ b/services/core/java/com/android/server/pm/DumpState.java
@@ -51,6 +51,7 @@
public static final int OPTION_SHOW_FILTERS = 1 << 0;
public static final int OPTION_DUMP_ALL_COMPONENTS = 1 << 1;
public static final int OPTION_SKIP_PERMISSIONS = 1 << 2;
+ public static final int OPTION_INCLUDE_APEX = 1 << 3;
private int mTypes;
diff --git a/services/core/java/com/android/server/pm/InitAppsHelper.java b/services/core/java/com/android/server/pm/InitAppsHelper.java
index 6825dd7..5c4447e 100644
--- a/services/core/java/com/android/server/pm/InitAppsHelper.java
+++ b/services/core/java/com/android/server/pm/InitAppsHelper.java
@@ -21,11 +21,9 @@
import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_DURATION__EVENT__OTA_PACKAGE_MANAGER_DATA_APP_AVG_SCAN_TIME;
import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_DURATION__EVENT__OTA_PACKAGE_MANAGER_SYSTEM_APP_AVG_SCAN_TIME;
import static com.android.server.pm.PackageManagerService.SCAN_AS_APK_IN_APEX;
-import static com.android.server.pm.PackageManagerService.SCAN_AS_FACTORY;
import static com.android.server.pm.PackageManagerService.SCAN_AS_PRIVILEGED;
import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM;
import static com.android.server.pm.PackageManagerService.SCAN_BOOTING;
-import static com.android.server.pm.PackageManagerService.SCAN_DROP_CACHE;
import static com.android.server.pm.PackageManagerService.SCAN_FIRST_BOOT_OR_UPGRADE;
import static com.android.server.pm.PackageManagerService.SCAN_INITIAL;
import static com.android.server.pm.PackageManagerService.SCAN_NO_DEX;
@@ -147,14 +145,7 @@
sp.getFolder().getAbsolutePath())
|| apexInfo.preInstalledApexPath.getAbsolutePath().startsWith(
sp.getFolder().getAbsolutePath() + File.separator)) {
- int additionalScanFlag = SCAN_AS_APK_IN_APEX;
- if (apexInfo.isFactory) {
- additionalScanFlag |= SCAN_AS_FACTORY;
- }
- if (apexInfo.activeApexChanged) {
- additionalScanFlag |= SCAN_DROP_CACHE;
- }
- return new ScanPartition(apexInfo.apexDirectory, sp, additionalScanFlag);
+ return new ScanPartition(apexInfo.apexDirectory, sp, apexInfo);
}
}
return null;
@@ -266,7 +257,7 @@
}
scanDirTracedLI(mPm.getAppInstallDir(), 0,
- mScanFlags | SCAN_REQUIRE_KNOWN, packageParser, mExecutorService);
+ mScanFlags | SCAN_REQUIRE_KNOWN, packageParser, mExecutorService, null);
List<Runnable> unfinishedTasks = mExecutorService.shutdownNow();
if (!unfinishedTasks.isEmpty()) {
@@ -335,12 +326,12 @@
}
scanDirTracedLI(partition.getOverlayFolder(),
mSystemParseFlags, mSystemScanFlags | partition.scanFlag,
- packageParser, executorService);
+ packageParser, executorService, partition.apexInfo);
}
scanDirTracedLI(frameworkDir,
mSystemParseFlags, mSystemScanFlags | SCAN_NO_DEX | SCAN_AS_PRIVILEGED,
- packageParser, executorService);
+ packageParser, executorService, null);
if (!mPm.mPackages.containsKey("android")) {
throw new IllegalStateException(
"Failed to load frameworks package; check log for warnings");
@@ -352,11 +343,11 @@
scanDirTracedLI(partition.getPrivAppFolder(),
mSystemParseFlags,
mSystemScanFlags | SCAN_AS_PRIVILEGED | partition.scanFlag,
- packageParser, executorService);
+ packageParser, executorService, partition.apexInfo);
}
scanDirTracedLI(partition.getAppFolder(),
mSystemParseFlags, mSystemScanFlags | partition.scanFlag,
- packageParser, executorService);
+ packageParser, executorService, partition.apexInfo);
}
}
@@ -373,7 +364,8 @@
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
private void scanDirTracedLI(File scanDir, int parseFlags, int scanFlags,
- PackageParser2 packageParser, ExecutorService executorService) {
+ PackageParser2 packageParser, ExecutorService executorService,
+ @Nullable ApexManager.ActiveApexInfo apexInfo) {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanDir [" + scanDir.getAbsolutePath() + "]");
try {
if ((scanFlags & SCAN_AS_APK_IN_APEX) != 0) {
@@ -381,7 +373,7 @@
parseFlags |= PARSE_APK_IN_APEX;
}
mInstallPackageHelper.installPackagesFromDir(scanDir, parseFlags,
- scanFlags, packageParser, executorService);
+ scanFlags, packageParser, executorService, apexInfo);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
diff --git a/services/core/java/com/android/server/pm/InstallArgs.java b/services/core/java/com/android/server/pm/InstallArgs.java
index ced547c..9cf9122 100644
--- a/services/core/java/com/android/server/pm/InstallArgs.java
+++ b/services/core/java/com/android/server/pm/InstallArgs.java
@@ -59,7 +59,7 @@
final boolean mForceQueryableOverride;
final int mDataLoaderType;
final int mPackageSource;
- final boolean mKeepApplicationEnabledSetting;
+ final boolean mApplicationEnabledSettingPersistent;
// The list of instruction sets supported by this app. This is currently
// only used during the rmdex() phase to clean up resources. We can get rid of this
@@ -74,7 +74,7 @@
int autoRevokePermissionsMode, String traceMethod, int traceCookie,
SigningDetails signingDetails, int installReason, int installScenario,
boolean forceQueryableOverride, int dataLoaderType, int packageSource,
- boolean keepApplicationEnabledSetting) {
+ boolean applicationEnabledSettingPersistent) {
mOriginInfo = originInfo;
mMoveInfo = moveInfo;
mInstallFlags = installFlags;
@@ -95,7 +95,7 @@
mForceQueryableOverride = forceQueryableOverride;
mDataLoaderType = dataLoaderType;
mPackageSource = packageSource;
- mKeepApplicationEnabledSetting = keepApplicationEnabledSetting;
+ mApplicationEnabledSettingPersistent = applicationEnabledSettingPersistent;
}
/**
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index ac4da2e..f0f23cd 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -367,10 +367,11 @@
if ((scanFlags & SCAN_AS_APK_IN_APEX) != 0) {
boolean isFactory = (scanFlags & SCAN_AS_FACTORY) != 0;
- pkgSetting.getPkgState().setApkInApex(true);
pkgSetting.getPkgState().setApkInUpdatedApex(!isFactory);
}
+ pkgSetting.getPkgState().setApexModuleName(request.getApexModuleName());
+
// TODO(toddke): Consider a method specifically for modifying the Package object
// post scan; or, moving this stuff out of the Package object since it has nothing
// to do with the package on disk.
@@ -1146,7 +1147,7 @@
if (onExternal) {
Slog.i(TAG, "Static shared libs can only be installed on internal storage.");
throw new PrepareFailure(INSTALL_FAILED_INVALID_INSTALL_LOCATION,
- "Packages declaring static-shared libs cannot be updated");
+ "Static shared libs can only be installed on internal storage.");
}
}
@@ -1716,6 +1717,7 @@
+ ", old=" + oldPackage);
}
request.setReturnCode(PackageManager.INSTALL_SUCCEEDED);
+ request.setApexModuleName(oldPackageState.getApexModuleName());
targetParseFlags = systemParseFlags;
targetScanFlags = systemScanFlags;
} else { // non system replace
@@ -2127,7 +2129,7 @@
}
// Enable system package for requested users
if (installedForUsers != null
- && !installRequest.isKeepApplicationEnabledSetting()) {
+ && !installRequest.isApplicationEnabledSettingPersistent()) {
for (int origUserId : installedForUsers) {
if (userId == UserHandle.USER_ALL || userId == origUserId) {
ps.setEnabled(COMPONENT_ENABLED_STATE_DEFAULT,
@@ -2180,7 +2182,7 @@
// be installed and enabled. The caller, however, can explicitly specify to
// keep the existing enabled state.
ps.setInstalled(true, userId);
- if (!installRequest.isKeepApplicationEnabledSetting()) {
+ if (!installRequest.isApplicationEnabledSettingPersistent()) {
ps.setEnabled(COMPONENT_ENABLED_STATE_DEFAULT, userId,
installerPackageName);
}
@@ -2189,7 +2191,7 @@
// Thus, updating the settings to install the app for all users.
for (int currentUserId : allUsers) {
ps.setInstalled(true, currentUserId);
- if (!installRequest.isKeepApplicationEnabledSetting()) {
+ if (!installRequest.isApplicationEnabledSettingPersistent()) {
ps.setEnabled(COMPONENT_ENABLED_STATE_DEFAULT, currentUserId,
installerPackageName);
}
@@ -2291,7 +2293,7 @@
}
}
installRequest.setName(pkgName);
- installRequest.setUid(pkg.getUid());
+ installRequest.setAppId(pkg.getUid());
installRequest.setPkg(pkg);
installRequest.setReturnCode(PackageManager.INSTALL_SUCCEEDED);
//to update install status
@@ -2776,7 +2778,7 @@
}
Bundle extras = new Bundle();
- extras.putInt(Intent.EXTRA_UID, request.getUid());
+ extras.putInt(Intent.EXTRA_UID, request.getAppId());
if (update) {
extras.putBoolean(Intent.EXTRA_REPLACING, true);
}
@@ -2799,7 +2801,7 @@
// Send PACKAGE_ADDED broadcast for users that see the package for the first time
// sendPackageAddedForNewUsers also deals with system apps
- int appId = UserHandle.getAppId(request.getUid());
+ int appId = UserHandle.getAppId(request.getAppId());
boolean isSystem = request.isInstallSystem();
mPm.sendPackageAddedForNewUsers(mPm.snapshotComputer(), packageName,
isSystem || virtualPreload, virtualPreload /*startReceiver*/, appId,
@@ -2944,9 +2946,9 @@
}
if (allNewUsers && !update) {
- mPm.notifyPackageAdded(packageName, request.getUid());
+ mPm.notifyPackageAdded(packageName, request.getAppId());
} else {
- mPm.notifyPackageChanged(packageName, request.getUid());
+ mPm.notifyPackageChanged(packageName, request.getAppId());
}
// Log current value of "unknown sources" setting
@@ -3172,7 +3174,7 @@
final RemovePackageHelper removePackageHelper = new RemovePackageHelper(mPm);
removePackageHelper.removePackage(stubPkg, true /*chatty*/);
try {
- return scanSystemPackageTracedLI(scanFile, parseFlags, scanFlags);
+ return scanSystemPackageTracedLI(scanFile, parseFlags, scanFlags, null);
} catch (PackageManagerException e) {
Slog.w(TAG, "Failed to install compressed system package:" + stubPkg.getPackageName(),
e);
@@ -3304,7 +3306,7 @@
| ParsingPackageUtils.PARSE_IS_SYSTEM_DIR;
@PackageManagerService.ScanFlags int scanFlags = mPm.getSystemPackageScanFlags(codePath);
final AndroidPackage pkg = scanSystemPackageTracedLI(
- codePath, parseFlags, scanFlags);
+ codePath, parseFlags, scanFlags, null);
synchronized (mPm.mLock) {
PackageSetting pkgSetting = mPm.mSettings.getPackageLPr(pkg.getPackageName());
@@ -3484,7 +3486,7 @@
try {
final File codePath = new File(pkg.getPath());
synchronized (mPm.mInstallLock) {
- scanSystemPackageTracedLI(codePath, 0, scanFlags);
+ scanSystemPackageTracedLI(codePath, 0, scanFlags, null);
}
} catch (PackageManagerException e) {
Slog.e(TAG, "Failed to parse updated, ex-system package: "
@@ -3563,7 +3565,8 @@
if (throwable == null) {
try {
- addForInitLI(parseResult.parsedPackage, newParseFlags, newScanFlags, null);
+ addForInitLI(parseResult.parsedPackage, newParseFlags, newScanFlags, null,
+ new ApexManager.ActiveApexInfo(ai));
AndroidPackage pkg = parseResult.parsedPackage.hideAsFinal();
if (ai.isFactory && !ai.isActive) {
disableSystemPackageLPw(pkg);
@@ -3585,8 +3588,8 @@
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
public void installPackagesFromDir(File scanDir, int parseFlags,
- int scanFlags, PackageParser2 packageParser,
- ExecutorService executorService) {
+ int scanFlags, PackageParser2 packageParser, ExecutorService executorService,
+ @Nullable ApexManager.ActiveApexInfo apexInfo) {
final File[] files = scanDir.listFiles();
if (ArrayUtils.isEmpty(files)) {
Log.d(TAG, "No files in app dir " + scanDir);
@@ -3634,7 +3637,7 @@
}
try {
addForInitLI(parseResult.parsedPackage, parseFlags, scanFlags,
- new UserHandle(UserHandle.USER_SYSTEM));
+ new UserHandle(UserHandle.USER_SYSTEM), apexInfo);
} catch (PackageManagerException e) {
errorCode = e.error;
errorMsg = "Failed to scan " + parseResult.scanFile + ": " + e.getMessage();
@@ -3697,7 +3700,7 @@
try {
synchronized (mPm.mInstallLock) {
final AndroidPackage newPkg = scanSystemPackageTracedLI(
- scanFile, reparseFlags, rescanFlags);
+ scanFile, reparseFlags, rescanFlags, null);
// We rescanned a stub, add it to the list of stubbed system packages
if (newPkg.isStub()) {
stubSystemApps.add(packageName);
@@ -3716,10 +3719,11 @@
*/
@GuardedBy("mPm.mInstallLock")
public AndroidPackage scanSystemPackageTracedLI(File scanFile, final int parseFlags,
- int scanFlags) throws PackageManagerException {
+ int scanFlags, @Nullable ApexManager.ActiveApexInfo apexInfo)
+ throws PackageManagerException {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanPackage [" + scanFile.toString() + "]");
try {
- return scanSystemPackageLI(scanFile, parseFlags, scanFlags);
+ return scanSystemPackageLI(scanFile, parseFlags, scanFlags, apexInfo);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
@@ -3730,7 +3734,8 @@
* Returns {@code null} in case of errors and the error code is stored in mLastScanError
*/
@GuardedBy("mPm.mInstallLock")
- private AndroidPackage scanSystemPackageLI(File scanFile, int parseFlags, int scanFlags)
+ private AndroidPackage scanSystemPackageLI(File scanFile, int parseFlags, int scanFlags,
+ @Nullable ApexManager.ActiveApexInfo apexInfo)
throws PackageManagerException {
if (DEBUG_INSTALL) Slog.d(TAG, "Parsing: " + scanFile);
@@ -3748,7 +3753,7 @@
}
return addForInitLI(parsedPackage, parseFlags, scanFlags,
- new UserHandle(UserHandle.USER_SYSTEM));
+ new UserHandle(UserHandle.USER_SYSTEM), apexInfo);
}
/**
@@ -3768,7 +3773,26 @@
private AndroidPackage addForInitLI(ParsedPackage parsedPackage,
@ParsingPackageUtils.ParseFlags int parseFlags,
@PackageManagerService.ScanFlags int scanFlags,
- @Nullable UserHandle user) throws PackageManagerException {
+ @Nullable UserHandle user, @Nullable ApexManager.ActiveApexInfo activeApexInfo)
+ throws PackageManagerException {
+ PackageSetting disabledPkgSetting;
+ synchronized (mPm.mLock) {
+ disabledPkgSetting =
+ mPm.mSettings.getDisabledSystemPkgLPr(parsedPackage.getPackageName());
+ if (activeApexInfo != null && disabledPkgSetting != null) {
+ // When a disabled system package is scanned, its final PackageSetting is actually
+ // skipped and not added to any data structures, instead relying on the disabled
+ // setting read from the persisted Settings XML file. This persistence does not
+ // include the APEX module name, so here, re-set it from the active APEX info.
+ //
+ // This also has the (beneficial) side effect where if a package disappears from an
+ // APEX, leaving only a /data copy, it will lose its apexModuleName.
+ //
+ // This must be done before scanSystemPackageLI as that will throw in the case of a
+ // system -> data package.
+ disabledPkgSetting.setApexModuleName(activeApexInfo.apexModuleName);
+ }
+ }
final Pair<ScanResult, Boolean> scanResultPair = scanSystemPackageLI(
parsedPackage, parseFlags, scanFlags, user);
@@ -3777,6 +3801,24 @@
final InstallRequest installRequest = new InstallRequest(
parsedPackage, parseFlags, scanFlags, user, scanResult);
+ String existingApexModuleName = null;
+ synchronized (mPm.mLock) {
+ var existingPkgSetting = mPm.mSettings.getPackageLPr(parsedPackage.getPackageName());
+ if (existingPkgSetting != null) {
+ existingApexModuleName = existingPkgSetting.getApexModuleName();
+ }
+ }
+
+ if (activeApexInfo != null) {
+ installRequest.setApexModuleName(activeApexInfo.apexModuleName);
+ } else {
+ if (disabledPkgSetting != null) {
+ installRequest.setApexModuleName(disabledPkgSetting.getApexModuleName());
+ } else if (existingApexModuleName != null) {
+ installRequest.setApexModuleName(existingApexModuleName);
+ }
+ }
+
synchronized (mPm.mLock) {
boolean appIdCreated = false;
try {
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index c6cdc4c..a9c5773 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -44,6 +44,7 @@
import com.android.server.pm.parsing.pkg.ParsedPackage;
import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageState;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
@@ -81,7 +82,7 @@
/** Package Installed Info */
@Nullable
private String mName;
- private int mUid = INVALID_UID;
+ private int mAppId = INVALID_UID;
// The set of users that originally had this package installed.
@Nullable
private int[] mOrigUsers;
@@ -107,6 +108,12 @@
@Nullable
private ApexInfo mApexInfo;
+ /**
+ * For tracking {@link PackageState#getApexModuleName()}.
+ */
+ @Nullable
+ private String mApexModuleName;
+
@Nullable
private ScanResult mScanResult;
@@ -129,7 +136,7 @@
params.mTraceMethod, params.mTraceCookie, params.mSigningDetails,
params.mInstallReason, params.mInstallScenario, params.mForceQueryableOverride,
params.mDataLoaderType, params.mPackageSource,
- params.mKeepApplicationEnabledSetting);
+ params.mApplicationEnabledSettingPersistent);
mPackageMetrics = new PackageMetrics(this);
mIsInstallInherit = params.mIsInherit;
mSessionId = params.mSessionId;
@@ -158,7 +165,7 @@
mUserId = user.getIdentifier();
} else {
// APEX
- mUserId = INVALID_UID;
+ mUserId = UserHandle.USER_SYSTEM;
}
mInstallArgs = null;
mParsedPackage = parsedPackage;
@@ -348,6 +355,11 @@
}
@Nullable
+ public String getApexModuleName() {
+ return mApexModuleName;
+ }
+
+ @Nullable
public String getSourceInstallerPackageName() {
return mInstallArgs.mInstallSource.mInstallerPackageName;
}
@@ -367,8 +379,8 @@
return mOrigUsers;
}
- public int getUid() {
- return mUid;
+ public int getAppId() {
+ return mAppId;
}
@Nullable
@@ -499,8 +511,8 @@
return mScanResult.mChangedAbiCodePath;
}
- public boolean isKeepApplicationEnabledSetting() {
- return mInstallArgs == null ? false : mInstallArgs.mKeepApplicationEnabledSetting;
+ public boolean isApplicationEnabledSettingPersistent() {
+ return mInstallArgs == null ? false : mInstallArgs.mApplicationEnabledSettingPersistent;
}
public boolean isForceQueryableOverride() {
@@ -644,12 +656,16 @@
mApexInfo = apexInfo;
}
+ public void setApexModuleName(@Nullable String apexModuleName) {
+ mApexModuleName = apexModuleName;
+ }
+
public void setPkg(AndroidPackage pkg) {
mPkg = pkg;
}
- public void setUid(int uid) {
- mUid = uid;
+ public void setAppId(int appId) {
+ mAppId = appId;
}
public void setNewUsers(int[] newUsers) {
@@ -773,10 +789,10 @@
}
}
- public void onInstallCompleted(int userId) {
+ public void onInstallCompleted() {
if (getReturnCode() == INSTALL_SUCCEEDED) {
if (mPackageMetrics != null) {
- mPackageMetrics.onInstallSucceed(userId);
+ mPackageMetrics.onInstallSucceed();
}
}
}
diff --git a/services/core/java/com/android/server/pm/InstallingSession.java b/services/core/java/com/android/server/pm/InstallingSession.java
index eb3b29c..7b759e3 100644
--- a/services/core/java/com/android/server/pm/InstallingSession.java
+++ b/services/core/java/com/android/server/pm/InstallingSession.java
@@ -97,7 +97,7 @@
final boolean mIsInherit;
final int mSessionId;
final int mRequireUserAction;
- final boolean mKeepApplicationEnabledSetting;
+ final boolean mApplicationEnabledSettingPersistent;
// For move install
InstallingSession(OriginInfo originInfo, MoveInfo moveInfo, IPackageInstallObserver2 observer,
@@ -130,7 +130,7 @@
mIsInherit = false;
mSessionId = -1;
mRequireUserAction = USER_ACTION_UNSPECIFIED;
- mKeepApplicationEnabledSetting = false;
+ mApplicationEnabledSettingPersistent = false;
}
InstallingSession(int sessionId, File stagedDir, IPackageInstallObserver2 observer,
@@ -164,7 +164,7 @@
mIsInherit = sessionParams.mode == MODE_INHERIT_EXISTING;
mSessionId = sessionId;
mRequireUserAction = sessionParams.requireUserAction;
- mKeepApplicationEnabledSetting = sessionParams.keepApplicationEnabledSetting;
+ mApplicationEnabledSettingPersistent = sessionParams.applicationEnabledSettingPersistent;
}
@Override
@@ -535,7 +535,7 @@
mInstallPackageHelper.installPackagesTraced(installRequests);
for (InstallRequest request : installRequests) {
- request.onInstallCompleted(mUser.getIdentifier());
+ request.onInstallCompleted();
doPostInstall(request);
}
}
@@ -609,6 +609,7 @@
// processApkInstallRequests() fails. Need a way to keep info stored in apexd
// and PMS in sync in the face of install failures.
request.setApexInfo(apexInfo);
+ request.setApexModuleName(apexInfo.moduleName);
mPm.mHandler.post(() -> processApkInstallRequests(true, requests));
return;
}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 350f5ef..47e18f1 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -278,8 +278,8 @@
private static final String ATTR_SIGNATURE = "signature";
private static final String ATTR_CHECKSUM_KIND = "checksumKind";
private static final String ATTR_CHECKSUM_VALUE = "checksumValue";
- private static final String ATTR_KEEP_APPLICATION_ENABLED_SETTING =
- "keepApplicationEnabledSetting";
+ private static final String ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT =
+ "applicationEnabledSettingPersistent";
private static final String PROPERTY_NAME_INHERIT_NATIVE = "pi.inherit_native_on_dont_kill";
private static final int[] EMPTY_CHILD_SESSION_ARRAY = EmptyArray.INT;
@@ -1163,7 +1163,7 @@
info.requireUserAction = params.requireUserAction;
info.installerUid = mInstallerUid;
info.packageSource = params.packageSource;
- info.keepApplicationEnabledSetting = params.keepApplicationEnabledSetting;
+ info.applicationEnabledSettingPersistent = params.applicationEnabledSettingPersistent;
info.pendingUserActionReason = userActionRequirementToReason(mUserActionRequirement);
}
return info;
@@ -4553,8 +4553,8 @@
}
@Override
- public boolean isKeepApplicationEnabledSetting() {
- return params.keepApplicationEnabledSetting;
+ public boolean isApplicationEnabledSettingPersistent() {
+ return params.applicationEnabledSettingPersistent;
}
@Override
@@ -4945,8 +4945,8 @@
writeStringAttribute(out, ATTR_ABI_OVERRIDE, params.abiOverride);
writeStringAttribute(out, ATTR_VOLUME_UUID, params.volumeUuid);
out.attributeInt(null, ATTR_INSTALL_REASON, params.installReason);
- writeBooleanAttribute(out, ATTR_KEEP_APPLICATION_ENABLED_SETTING,
- params.keepApplicationEnabledSetting);
+ writeBooleanAttribute(out, ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT,
+ params.applicationEnabledSettingPersistent);
final boolean isDataLoader = params.dataLoaderParams != null;
writeBooleanAttribute(out, ATTR_IS_DATALOADER, isDataLoader);
@@ -5110,8 +5110,8 @@
params.volumeUuid = readStringAttribute(in, ATTR_VOLUME_UUID);
params.installReason = in.getAttributeInt(null, ATTR_INSTALL_REASON);
params.packageSource = in.getAttributeInt(null, ATTR_PACKAGE_SOURCE);
- params.keepApplicationEnabledSetting = in.getAttributeBoolean(null,
- ATTR_KEEP_APPLICATION_ENABLED_SETTING, false);
+ params.applicationEnabledSettingPersistent = in.getAttributeBoolean(null,
+ ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT, false);
if (in.getAttributeBoolean(null, ATTR_IS_DATALOADER, false)) {
params.dataLoaderParams = new DataLoaderParams(
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 92bbb7e..9cc0334 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -716,7 +716,7 @@
* The list of all system partitions that may contain packages in ascending order of
* specificity (the more generic, the earlier in the list a partition appears).
*/
- @VisibleForTesting(visibility = Visibility.PRIVATE)
+ @VisibleForTesting(visibility = Visibility.PACKAGE)
public static final List<ScanPartition> SYSTEM_PARTITIONS = Collections.unmodifiableList(
PackagePartitions.getOrderedPartitions(ScanPartition::new));
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 849cbeb..12841a4 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -3291,7 +3291,7 @@
sessionParams.installFlags |= PackageManager.INSTALL_DISABLE_VERIFICATION;
break;
case "--skip-enable":
- sessionParams.setKeepApplicationEnabledSetting();
+ sessionParams.setApplicationEnabledSettingPersistent();
break;
case "--bypass-low-target-sdk-block":
sessionParams.installFlags |=
diff --git a/services/core/java/com/android/server/pm/PackageMetrics.java b/services/core/java/com/android/server/pm/PackageMetrics.java
index 8252a9fa..d4c1256 100644
--- a/services/core/java/com/android/server/pm/PackageMetrics.java
+++ b/services/core/java/com/android/server/pm/PackageMetrics.java
@@ -19,9 +19,11 @@
import static android.os.Process.INVALID_UID;
import android.annotation.IntDef;
+import android.app.ActivityManager;
import android.app.admin.SecurityLog;
import android.content.pm.PackageManager;
import android.content.pm.parsing.ApkLiteParseUtils;
+import android.os.UserHandle;
import android.util.Pair;
import android.util.SparseArray;
@@ -68,8 +70,8 @@
mInstallRequest = installRequest;
}
- public void onInstallSucceed(int userId) {
- reportInstallationToSecurityLog(userId);
+ public void onInstallSucceed() {
+ reportInstallationToSecurityLog(mInstallRequest.getUserId());
reportInstallationStats(true /* success */);
}
@@ -110,10 +112,11 @@
}
}
+
FrameworkStatsLog.write(FrameworkStatsLog.PACKAGE_INSTALLATION_SESSION_REPORTED,
mInstallRequest.getSessionId() /* session_id */,
packageName /* package_name */,
- mInstallRequest.getUid() /* uid */,
+ getUid(mInstallRequest.getAppId(), mInstallRequest.getUserId()) /* uid */,
newUsers /* user_ids */,
userManagerInternal.getUserTypesForStatsd(newUsers) /* user_types */,
originalUsers /* original_user_ids */,
@@ -140,6 +143,13 @@
);
}
+ private static int getUid(int appId, int userId) {
+ if (userId == UserHandle.USER_ALL) {
+ userId = ActivityManager.getCurrentUser();
+ }
+ return UserHandle.getUid(userId, appId);
+ }
+
private long getApksSize(File apkDir) {
// TODO(b/249294752): also count apk sizes for failed installs
final AtomicLong apksSize = new AtomicLong();
@@ -218,9 +228,9 @@
final int[] originalUsers = info.mOrigUsers;
final int[] originalUserTypes = userManagerInternal.getUserTypesForStatsd(originalUsers);
FrameworkStatsLog.write(FrameworkStatsLog.PACKAGE_UNINSTALLATION_REPORTED,
- info.mUid, removedUsers, removedUserTypes, originalUsers, originalUserTypes,
- deleteFlags, PackageManager.DELETE_SUCCEEDED, info.mIsRemovedPackageSystemUpdate,
- !info.mRemovedForAllUsers);
+ getUid(info.mUid, userId), removedUsers, removedUserTypes, originalUsers,
+ originalUserTypes, deleteFlags, PackageManager.DELETE_SUCCEEDED,
+ info.mIsRemovedPackageSystemUpdate, !info.mRemovedForAllUsers);
final String packageName = info.mRemovedPackage;
final long versionCode = info.mRemovedPackageVersionCode;
reportUninstallationToSecurityLog(packageName, versionCode, userId);
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 6562de96..53fdfaa 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -1262,6 +1262,12 @@
return pkgState.isApkInUpdatedApex();
}
+ @Nullable
+ @Override
+ public String getApexModuleName() {
+ return pkgState.getApexModuleName();
+ }
+
public PackageSetting setDomainSetId(@NonNull UUID domainSetId) {
mDomainSetId = domainSetId;
onChanged();
@@ -1317,6 +1323,11 @@
return this;
}
+ public PackageSetting setApexModuleName(@Nullable String apexModuleName) {
+ pkgState.setApexModuleName(apexModuleName);
+ return this;
+ }
+
@NonNull
@Override
public PackageStateUnserialized getTransientState() {
diff --git a/services/core/java/com/android/server/pm/ScanPartition.java b/services/core/java/com/android/server/pm/ScanPartition.java
index e1d2b3b..9ee6035 100644
--- a/services/core/java/com/android/server/pm/ScanPartition.java
+++ b/services/core/java/com/android/server/pm/ScanPartition.java
@@ -16,13 +16,17 @@
package com.android.server.pm;
+import static com.android.server.pm.PackageManagerService.SCAN_AS_APK_IN_APEX;
+import static com.android.server.pm.PackageManagerService.SCAN_AS_FACTORY;
import static com.android.server.pm.PackageManagerService.SCAN_AS_ODM;
import static com.android.server.pm.PackageManagerService.SCAN_AS_OEM;
import static com.android.server.pm.PackageManagerService.SCAN_AS_PRODUCT;
import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM_EXT;
import static com.android.server.pm.PackageManagerService.SCAN_AS_VENDOR;
+import static com.android.server.pm.PackageManagerService.SCAN_DROP_CACHE;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.pm.PackagePartitions;
import com.android.internal.annotations.VisibleForTesting;
@@ -32,14 +36,18 @@
/**
* List of partitions to be scanned during system boot
*/
-@VisibleForTesting
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class ScanPartition extends PackagePartitions.SystemPartition {
@PackageManagerService.ScanFlags
public final int scanFlag;
+ @Nullable
+ public final ApexManager.ActiveApexInfo apexInfo;
+
public ScanPartition(@NonNull PackagePartitions.SystemPartition partition) {
super(partition);
scanFlag = scanFlagForPartition(partition);
+ apexInfo = null;
}
/**
@@ -48,9 +56,21 @@
* partition along with any specified additional scan flags.
*/
public ScanPartition(@NonNull File folder, @NonNull ScanPartition original,
- @PackageManagerService.ScanFlags int additionalScanFlag) {
+ @Nullable ApexManager.ActiveApexInfo apexInfo) {
super(folder, original);
- this.scanFlag = original.scanFlag | additionalScanFlag;
+ var scanFlags = original.scanFlag;
+ this.apexInfo = apexInfo;
+ if (apexInfo != null) {
+ scanFlags |= SCAN_AS_APK_IN_APEX;
+ if (apexInfo.isFactory) {
+ scanFlags |= SCAN_AS_FACTORY;
+ }
+ if (apexInfo.activeApexChanged) {
+ scanFlags |= SCAN_DROP_CACHE;
+ }
+ }
+ //noinspection WrongConstant
+ this.scanFlag = scanFlags;
}
private static int scanFlagForPartition(PackagePartitions.SystemPartition partition) {
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 97fb0c2..aedf782 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -5052,6 +5052,7 @@
pw.print(prefix); pw.print(" privatePkgFlags="); printFlags(pw, ps.getPrivateFlags(),
PRIVATE_FLAG_DUMP_SPEC);
pw.println();
+ pw.print(prefix); pw.print(" apexModuleName="); pw.println(ps.getApexModuleName());
if (pkg != null && pkg.getOverlayTarget() != null) {
pw.print(prefix); pw.print(" overlayTarget="); pw.println(pkg.getOverlayTarget());
@@ -5263,7 +5264,8 @@
&& !packageName.equals(ps.getPackageName())) {
continue;
}
- if (ps.getPkg() != null && ps.getPkg().isApex()) {
+ if (ps.getPkg() != null && ps.getPkg().isApex()
+ && !dumpState.isOptionEnabled(DumpState.OPTION_INCLUDE_APEX)) {
// Filter APEX packages which will be dumped in the APEX section
continue;
}
@@ -5319,7 +5321,8 @@
&& !packageName.equals(ps.getPackageName())) {
continue;
}
- if (ps.getPkg() != null && ps.getPkg().isApex()) {
+ if (ps.getPkg() != null && ps.getPkg().isApex()
+ && !dumpState.isOptionEnabled(DumpState.OPTION_INCLUDE_APEX)) {
// Filter APEX packages which will be dumped in the APEX section
continue;
}
diff --git a/services/core/java/com/android/server/pm/StorageEventHelper.java b/services/core/java/com/android/server/pm/StorageEventHelper.java
index 4f7c2bd..23156d1 100644
--- a/services/core/java/com/android/server/pm/StorageEventHelper.java
+++ b/services/core/java/com/android/server/pm/StorageEventHelper.java
@@ -158,7 +158,7 @@
final AndroidPackage pkg;
try {
pkg = installPackageHelper.scanSystemPackageTracedLI(
- ps.getPath(), parseFlags, SCAN_INITIAL);
+ ps.getPath(), parseFlags, SCAN_INITIAL, null);
loaded.add(pkg);
} catch (PackageManagerException e) {
diff --git a/services/core/java/com/android/server/pm/dex/DexoptOptions.java b/services/core/java/com/android/server/pm/dex/DexoptOptions.java
index 411c19f..d3fba7c 100644
--- a/services/core/java/com/android/server/pm/dex/DexoptOptions.java
+++ b/services/core/java/com/android/server/pm/dex/DexoptOptions.java
@@ -24,7 +24,7 @@
import com.android.server.art.ReasonMapping;
import com.android.server.art.model.ArtFlags;
-import com.android.server.art.model.OptimizeParams;
+import com.android.server.art.model.DexoptParams;
import com.android.server.pm.DexOptHelper;
import com.android.server.pm.PackageManagerService;
@@ -201,22 +201,22 @@
}
/**
- * Returns an {@link OptimizeParams} instance corresponding to this object, for use with
+ * Returns an {@link DexoptParams} instance corresponding to this object, for use with
* {@link com.android.server.art.ArtManagerLocal}.
*
- * @param extraFlags extra {@link ArtFlags#OptimizeFlags} to set in the returned
- * {@code OptimizeParams} beyond those converted from this object
+ * @param extraFlags extra {@link ArtFlags#DexoptFlags} to set in the returned
+ * {@code DexoptParams} beyond those converted from this object
* @return null if the settings cannot be accurately represented, and hence the old
* PackageManager/installd code paths need to be used.
*/
- public @Nullable OptimizeParams convertToOptimizeParams(/*@OptimizeFlags*/ int extraFlags) {
+ public @Nullable DexoptParams convertToDexoptParams(/*@DexoptFlags*/ int extraFlags) {
if (mSplitName != null) {
DexOptHelper.reportArtManagerFallback(
mPackageName, "Request to optimize only split " + mSplitName);
return null;
}
- /*@OptimizeFlags*/ int flags = extraFlags;
+ /*@DexoptFlags*/ int flags = extraFlags;
if ((mFlags & DEXOPT_CHECK_FOR_PROFILES_UPDATES) == 0
&& isProfileGuidedCompilerFilter(mCompilerFilter)) {
// ART Service doesn't support bypassing the profile update check when profiles are
@@ -322,7 +322,7 @@
"Invalid compilation reason " + mCompilationReason);
}
- return new OptimizeParams.Builder(reason, flags)
+ return new DexoptParams.Builder(reason, flags)
.setCompilerFilter(mCompilerFilter)
.setPriorityClass(priority)
.build();
diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java
index 5fdead0..a12c9d0 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageState.java
@@ -417,4 +417,11 @@
* @hide
*/
boolean isVendor();
+
+ /**
+ * The name of the APEX module containing this package, if it is an APEX or APK-in-APEX.
+ * @hide
+ */
+ @Nullable
+ String getApexModuleName();
}
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
index 8dee8ee..bc6dab4 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
@@ -154,6 +154,8 @@
private final SigningInfo mSigningInfo;
@NonNull
private final SparseArray<PackageUserState> mUserStates;
+ @Nullable
+ private final String mApexModuleName;
private PackageStateImpl(@NonNull PackageState pkgState, @Nullable AndroidPackage pkg) {
mAndroidPackage = pkg;
@@ -206,6 +208,8 @@
mUserStates.put(userStates.keyAt(index),
UserStateImpl.copy(userStates.valueAt(index)));
}
+
+ mApexModuleName = pkgState.getApexModuleName();
}
@NonNull
@@ -714,6 +718,11 @@
}
@DataClass.Generated.Member
+ public @Nullable String getApexModuleName() {
+ return mApexModuleName;
+ }
+
+ @DataClass.Generated.Member
public @NonNull PackageStateImpl setBooleans( int value) {
mBooleans = value;
return this;
@@ -723,7 +732,7 @@
time = 1671671043929L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java",
- inputSignatures = "private int mBooleans\nprivate final @android.annotation.Nullable com.android.server.pm.pkg.AndroidPackage mAndroidPackage\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mVolumeUuid\nprivate final int mAppId\nprivate final int mCategoryOverride\nprivate final @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate final @android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy int mHiddenApiEnforcementPolicy\nprivate final long mLastModifiedTime\nprivate final long mLastUpdateTime\nprivate final long mLongVersionCode\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mMimeGroups\nprivate final @android.annotation.NonNull java.io.File mPath\nprivate final @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSeInfo\nprivate final boolean mHasSharedUser\nprivate final int mSharedUserAppId\nprivate final @android.annotation.NonNull java.lang.String[] mUsesSdkLibraries\nprivate final @android.annotation.NonNull long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.NonNull java.lang.String[] mUsesStaticLibraries\nprivate final @android.annotation.NonNull long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibrary> mUsesLibraries\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesLibraryFiles\nprivate final @android.annotation.NonNull long[] mLastPackageUsageTime\nprivate final @android.annotation.NonNull android.content.pm.SigningInfo mSigningInfo\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserState> mUserStates\npublic static com.android.server.pm.pkg.PackageState copy(com.android.server.pm.pkg.PackageStateInternal)\nprivate void setBoolean(int,boolean)\nprivate boolean getBoolean(int)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @java.lang.Override boolean isExternalStorage()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isOdm()\npublic @java.lang.Override boolean isOem()\npublic @java.lang.Override boolean isPrivileged()\npublic @java.lang.Override boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic @java.lang.Override boolean isSystem()\npublic @java.lang.Override boolean isSystemExt()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @java.lang.Override boolean isVendor()\npublic @java.lang.Override long getVersionCode()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override boolean isApex()\nclass PackageStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageState]\nprivate static final int SYSTEM\nprivate static final int EXTERNAL_STORAGE\nprivate static final int PRIVILEGED\nprivate static final int OEM\nprivate static final int VENDOR\nprivate static final int PRODUCT\nprivate static final int SYSTEM_EXT\nprivate static final int REQUIRED_FOR_SYSTEM_USER\nprivate static final int ODM\nprivate static final int FORCE_QUERYABLE_OVERRIDE\nprivate static final int HIDDEN_UNTIL_INSTALLED\nprivate static final int INSTALL_PERMISSIONS_FIXED\nprivate static final int UPDATE_AVAILABLE\nprivate static final int UPDATED_SYSTEM_APP\nprivate static final int APK_IN_UPDATED_APEX\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
+ inputSignatures = "private int mBooleans\nprivate final @android.annotation.Nullable com.android.server.pm.pkg.AndroidPackage mAndroidPackage\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mVolumeUuid\nprivate final int mAppId\nprivate final int mCategoryOverride\nprivate final @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate final @android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy int mHiddenApiEnforcementPolicy\nprivate final long mLastModifiedTime\nprivate final long mLastUpdateTime\nprivate final long mLongVersionCode\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mMimeGroups\nprivate final @android.annotation.NonNull java.io.File mPath\nprivate final @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSeInfo\nprivate final boolean mHasSharedUser\nprivate final int mSharedUserAppId\nprivate final @android.annotation.NonNull java.lang.String[] mUsesSdkLibraries\nprivate final @android.annotation.NonNull long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.NonNull java.lang.String[] mUsesStaticLibraries\nprivate final @android.annotation.NonNull long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibrary> mUsesLibraries\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesLibraryFiles\nprivate final @android.annotation.NonNull long[] mLastPackageUsageTime\nprivate final @android.annotation.NonNull android.content.pm.SigningInfo mSigningInfo\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserState> mUserStates\nprivate final @android.annotation.Nullable java.lang.String mApexModuleName\npublic static com.android.server.pm.pkg.PackageState copy(com.android.server.pm.pkg.PackageStateInternal)\nprivate void setBoolean(int,boolean)\nprivate boolean getBoolean(int)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @java.lang.Override boolean isExternalStorage()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isOdm()\npublic @java.lang.Override boolean isOem()\npublic @java.lang.Override boolean isPrivileged()\npublic @java.lang.Override boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic @java.lang.Override boolean isSystem()\npublic @java.lang.Override boolean isSystemExt()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @java.lang.Override boolean isVendor()\npublic @java.lang.Override long getVersionCode()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override boolean isApex()\nclass PackageStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageState]\nprivate static final int SYSTEM\nprivate static final int EXTERNAL_STORAGE\nprivate static final int PRIVILEGED\nprivate static final int OEM\nprivate static final int VENDOR\nprivate static final int PRODUCT\nprivate static final int SYSTEM_EXT\nprivate static final int REQUIRED_FOR_SYSTEM_USER\nprivate static final int ODM\nprivate static final int FORCE_QUERYABLE_OVERRIDE\nprivate static final int HIDDEN_UNTIL_INSTALLED\nprivate static final int INSTALL_PERMISSIONS_FIXED\nprivate static final int UPDATE_AVAILABLE\nprivate static final int UPDATED_SYSTEM_APP\nprivate static final int APK_IN_UPDATED_APEX\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
@Deprecated
private void __metadata() {}
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java b/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java
index 57fbfe9..19c0886 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java
@@ -54,7 +54,6 @@
private List<String> usesLibraryFiles = emptyList();
private boolean updatedSystemApp;
- private boolean apkInApex;
private boolean apkInUpdatedApex;
@NonNull
@@ -70,6 +69,9 @@
@NonNull
private final PackageSetting mPackageSetting;
+ @Nullable
+ private String mApexModuleName;
+
public PackageStateUnserialized(@NonNull PackageSetting packageSetting) {
mPackageSetting = packageSetting;
}
@@ -138,11 +140,11 @@
}
this.updatedSystemApp = other.updatedSystemApp;
- this.apkInApex = other.apkInApex;
this.apkInUpdatedApex = other.apkInUpdatedApex;
this.lastPackageUsageTimeInMills = other.lastPackageUsageTimeInMills;
this.overrideSeInfo = other.overrideSeInfo;
this.seInfo = other.seInfo;
+ this.mApexModuleName = other.mApexModuleName;
mPackageSetting.onChanged();
}
@@ -187,12 +189,6 @@
return this;
}
- public PackageStateUnserialized setApkInApex(boolean value) {
- apkInApex = value;
- mPackageSetting.onChanged();
- return this;
- }
-
public PackageStateUnserialized setApkInUpdatedApex(boolean value) {
apkInUpdatedApex = value;
mPackageSetting.onChanged();
@@ -218,6 +214,13 @@
return this;
}
+ @NonNull
+ public PackageStateUnserialized setApexModuleName(@NonNull String value) {
+ mApexModuleName = value;
+ mPackageSetting.onChanged();
+ return this;
+ }
+
// Code below generated by codegen v1.0.23.
@@ -254,11 +257,6 @@
}
@DataClass.Generated.Member
- public boolean isApkInApex() {
- return apkInApex;
- }
-
- @DataClass.Generated.Member
public boolean isApkInUpdatedApex() {
return apkInUpdatedApex;
}
@@ -292,11 +290,16 @@
return mPackageSetting;
}
+ @DataClass.Generated.Member
+ public @Nullable String getApexModuleName() {
+ return mApexModuleName;
+ }
+
@DataClass.Generated(
- time = 1666291743725L,
+ time = 1671483772254L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java",
- inputSignatures = "private boolean hiddenUntilInstalled\nprivate @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibraryWrapper> usesLibraryInfos\nprivate @android.annotation.NonNull java.util.List<java.lang.String> usesLibraryFiles\nprivate boolean updatedSystemApp\nprivate boolean apkInApex\nprivate boolean apkInUpdatedApex\nprivate volatile @android.annotation.NonNull long[] lastPackageUsageTimeInMills\nprivate @android.annotation.Nullable java.lang.String overrideSeInfo\nprivate @android.annotation.NonNull java.lang.String seInfo\nprivate final @android.annotation.NonNull com.android.server.pm.PackageSetting mPackageSetting\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized addUsesLibraryInfo(com.android.server.pm.pkg.SharedLibraryWrapper)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized addUsesLibraryFile(java.lang.String)\nprivate long[] lazyInitLastPackageUsageTimeInMills()\npublic com.android.server.pm.pkg.PackageStateUnserialized setLastPackageUsageTimeInMills(int,long)\npublic long getLatestPackageUseTimeInMills()\npublic long getLatestForegroundPackageUseTimeInMills()\npublic void updateFrom(com.android.server.pm.pkg.PackageStateUnserialized)\npublic @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> getNonNativeUsesLibraryInfos()\npublic com.android.server.pm.pkg.PackageStateUnserialized setHiddenUntilInstalled(boolean)\npublic com.android.server.pm.pkg.PackageStateUnserialized setUsesLibraryInfos(java.util.List<android.content.pm.SharedLibraryInfo>)\npublic com.android.server.pm.pkg.PackageStateUnserialized setUsesLibraryFiles(java.util.List<java.lang.String>)\npublic com.android.server.pm.pkg.PackageStateUnserialized setUpdatedSystemApp(boolean)\npublic com.android.server.pm.pkg.PackageStateUnserialized setApkInApex(boolean)\npublic com.android.server.pm.pkg.PackageStateUnserialized setApkInUpdatedApex(boolean)\npublic com.android.server.pm.pkg.PackageStateUnserialized setLastPackageUsageTimeInMills(long)\npublic com.android.server.pm.pkg.PackageStateUnserialized setOverrideSeInfo(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized setSeInfo(java.lang.String)\nclass PackageStateUnserialized extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genSetters=true, genConstructor=false, genBuilder=false)")
+ inputSignatures = "private boolean hiddenUntilInstalled\nprivate @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibraryWrapper> usesLibraryInfos\nprivate @android.annotation.NonNull java.util.List<java.lang.String> usesLibraryFiles\nprivate boolean updatedSystemApp\nprivate boolean apkInUpdatedApex\nprivate volatile @android.annotation.NonNull long[] lastPackageUsageTimeInMills\nprivate @android.annotation.Nullable java.lang.String overrideSeInfo\nprivate @android.annotation.NonNull java.lang.String seInfo\nprivate final @android.annotation.NonNull com.android.server.pm.PackageSetting mPackageSetting\nprivate @android.annotation.Nullable java.lang.String mApexModuleName\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized addUsesLibraryInfo(com.android.server.pm.pkg.SharedLibraryWrapper)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized addUsesLibraryFile(java.lang.String)\nprivate long[] lazyInitLastPackageUsageTimeInMills()\npublic com.android.server.pm.pkg.PackageStateUnserialized setLastPackageUsageTimeInMills(int,long)\npublic long getLatestPackageUseTimeInMills()\npublic long getLatestForegroundPackageUseTimeInMills()\npublic void updateFrom(com.android.server.pm.pkg.PackageStateUnserialized)\npublic @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> getNonNativeUsesLibraryInfos()\npublic com.android.server.pm.pkg.PackageStateUnserialized setHiddenUntilInstalled(boolean)\npublic com.android.server.pm.pkg.PackageStateUnserialized setUsesLibraryInfos(java.util.List<android.content.pm.SharedLibraryInfo>)\npublic com.android.server.pm.pkg.PackageStateUnserialized setUsesLibraryFiles(java.util.List<java.lang.String>)\npublic com.android.server.pm.pkg.PackageStateUnserialized setUpdatedSystemApp(boolean)\npublic com.android.server.pm.pkg.PackageStateUnserialized setApkInUpdatedApex(boolean)\npublic com.android.server.pm.pkg.PackageStateUnserialized setLastPackageUsageTimeInMills(long)\npublic com.android.server.pm.pkg.PackageStateUnserialized setOverrideSeInfo(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized setSeInfo(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized setApexModuleName(java.lang.String)\nclass PackageStateUnserialized extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genSetters=true, genConstructor=false, genBuilder=false)")
@Deprecated
private void __metadata() {}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 65acdc1..a099e72 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -661,7 +661,7 @@
dispatchMediaKeyRepeatWithWakeLock((KeyEvent)msg.obj);
break;
case MSG_DISPATCH_SHOW_RECENTS:
- showRecentApps(false);
+ showRecents();
break;
case MSG_DISPATCH_SHOW_GLOBAL_ACTIONS:
showGlobalActionsInternal();
@@ -2910,7 +2910,7 @@
break;
case KeyEvent.KEYCODE_RECENT_APPS:
if (down && repeatCount == 0) {
- showRecentApps(false /* triggeredFromAltTab */);
+ showRecents();
}
return key_consumed;
case KeyEvent.KEYCODE_APP_SWITCH:
@@ -3094,22 +3094,23 @@
}
break;
case KeyEvent.KEYCODE_TAB:
- if (down && event.isMetaPressed()) {
- if (!keyguardOn && isUserSetupComplete()) {
- showRecentApps(false);
- return key_consumed;
- }
- } else if (down && repeatCount == 0) {
- // Display task switcher for ALT-TAB.
- if (mRecentAppsHeldModifiers == 0 && !keyguardOn && isUserSetupComplete()) {
- final int shiftlessModifiers =
- event.getModifiers() & ~KeyEvent.META_SHIFT_MASK;
- if (KeyEvent.metaStateHasModifiers(
- shiftlessModifiers, KeyEvent.META_ALT_ON)) {
- mRecentAppsHeldModifiers = shiftlessModifiers;
- showRecentApps(true);
+ if (down) {
+ if (event.isMetaPressed()) {
+ if (!keyguardOn && isUserSetupComplete()) {
+ showRecents();
return key_consumed;
}
+ } else {
+ // Display task switcher for ALT-TAB.
+ if (mRecentAppsHeldModifiers == 0 && !keyguardOn && isUserSetupComplete()) {
+ final int modifiers = event.getModifiers();
+ if (KeyEvent.metaStateHasModifiers(modifiers, KeyEvent.META_ALT_ON)) {
+ mRecentAppsHeldModifiers = modifiers;
+ showRecentsFromAltTab(KeyEvent.metaStateHasModifiers(modifiers,
+ KeyEvent.META_SHIFT_ON));
+ return key_consumed;
+ }
+ }
}
}
break;
@@ -3646,11 +3647,19 @@
mHandler.obtainMessage(MSG_DISPATCH_SHOW_RECENTS).sendToTarget();
}
- private void showRecentApps(boolean triggeredFromAltTab) {
+ private void showRecents() {
mPreloadedRecentApps = false; // preloading no longer needs to be canceled
StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
if (statusbar != null) {
- statusbar.showRecentApps(triggeredFromAltTab);
+ statusbar.showRecentApps(false /* triggeredFromAltTab */, false /* forward */);
+ }
+ }
+
+ private void showRecentsFromAltTab(boolean forward) {
+ mPreloadedRecentApps = false; // preloading no longer needs to be canceled
+ StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
+ if (statusbar != null) {
+ statusbar.showRecentApps(true /* triggeredFromAltTab */, forward);
}
}
@@ -4299,9 +4308,6 @@
case KeyEvent.KEYCODE_DEMO_APP_2:
case KeyEvent.KEYCODE_DEMO_APP_3:
case KeyEvent.KEYCODE_DEMO_APP_4: {
- // TODO(b/254604589): Dispatch KeyEvent to System UI.
- sendSystemKeyToStatusBarAsync(keyCode);
-
// Just drop if keys are not intercepted for direct key.
result &= ~ACTION_PASS_TO_USER;
break;
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 326d709..ed6a46f 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -737,6 +737,7 @@
}
TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
tm.notifyUserActivity();
+ mInputManagerInternal.notifyUserActivity();
mPolicy.userActivity(displayGroupId, event);
mFaceDownDetector.userActivity(event);
mScreenUndimDetector.userActivity(displayGroupId);
diff --git a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
index 60dbbdd..3fcb08a 100644
--- a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
+++ b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
@@ -710,6 +710,11 @@
if (gnssChargeUC != EnergyConsumerSnapshot.UNAVAILABLE) {
mStats.updateGnssEnergyConsumerStatsLocked(gnssChargeUC, elapsedRealtime);
}
+
+ final long cameraChargeUC = energyConsumerDeltas.cameraChargeUC;
+ if (cameraChargeUC != EnergyConsumerSnapshot.UNAVAILABLE) {
+ mStats.updateCameraEnergyConsumerStatsLocked(cameraChargeUC, elapsedRealtime);
+ }
}
// Inform mStats about each applicable custom energy bucket.
if (energyConsumerDeltas != null
@@ -904,6 +909,9 @@
case EnergyConsumerType.WIFI:
buckets[EnergyConsumerStats.POWER_BUCKET_WIFI] = true;
break;
+ case EnergyConsumerType.CAMERA:
+ buckets[EnergyConsumerStats.POWER_BUCKET_CAMERA] = true;
+ break;
}
}
return buckets;
@@ -955,6 +963,9 @@
if ((flags & UPDATE_WIFI) != 0) {
addEnergyConsumerIdLocked(energyConsumerIds, EnergyConsumerType.WIFI);
}
+ if ((flags & UPDATE_CAMERA) != 0) {
+ addEnergyConsumerIdLocked(energyConsumerIds, EnergyConsumerType.CAMERA);
+ }
if (energyConsumerIds.size() == 0) {
return null;
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 c559436..d622fd7 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -178,7 +178,7 @@
// TODO: remove "tcp" from network methods, since we measure total stats.
// Current on-disk Parcel version. Must be updated when the format of the parcelable changes
- public static final int VERSION = 210;
+ public static final int VERSION = 211;
// The maximum number of names wakelocks we will keep track of
// per uid; once the limit is reached, we batch the remaining wakelocks
@@ -649,10 +649,12 @@
int UPDATE_BT = 0x08;
int UPDATE_RPM = 0x10;
int UPDATE_DISPLAY = 0x20;
- int RESET = 0x40;
+ int UPDATE_CAMERA = 0x40;
+ int RESET = 0x80;
int UPDATE_ALL =
- UPDATE_CPU | UPDATE_WIFI | UPDATE_RADIO | UPDATE_BT | UPDATE_RPM | UPDATE_DISPLAY;
+ UPDATE_CPU | UPDATE_WIFI | UPDATE_RADIO | UPDATE_BT | UPDATE_RPM | UPDATE_DISPLAY
+ | UPDATE_CAMERA;
int UPDATE_ON_PROC_STATE_CHANGE = UPDATE_WIFI | UPDATE_RADIO | UPDATE_BT;
@@ -665,6 +667,7 @@
UPDATE_BT,
UPDATE_RPM,
UPDATE_DISPLAY,
+ UPDATE_CAMERA,
UPDATE_ALL,
})
@Retention(RetentionPolicy.SOURCE)
@@ -6244,6 +6247,8 @@
}
getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
.noteCameraTurnedOnLocked(elapsedRealtimeMs);
+
+ scheduleSyncExternalStatsLocked("camera-on", ExternalStatsSync.UPDATE_CAMERA);
}
@GuardedBy("this")
@@ -6259,6 +6264,8 @@
}
getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
.noteCameraTurnedOffLocked(elapsedRealtimeMs);
+
+ scheduleSyncExternalStatsLocked("camera-off", ExternalStatsSync.UPDATE_CAMERA);
}
@GuardedBy("this")
@@ -6273,6 +6280,8 @@
uid.noteResetCameraLocked(elapsedRealtimeMs);
}
}
+
+ scheduleSyncExternalStatsLocked("camera-reset", ExternalStatsSync.UPDATE_CAMERA);
}
@GuardedBy("this")
@@ -7414,6 +7423,12 @@
return getPowerBucketConsumptionUC(EnergyConsumerStats.POWER_BUCKET_WIFI);
}
+ @GuardedBy("this")
+ @Override
+ public long getCameraEnergyConsumptionUC() {
+ return getPowerBucketConsumptionUC(EnergyConsumerStats.POWER_BUCKET_CAMERA);
+ }
+
/**
* Returns the consumption (in microcoulombs) that the given standard power bucket consumed.
* Will return {@link #POWER_DATA_UNAVAILABLE} if data is unavailable
@@ -8427,6 +8442,12 @@
processState);
}
+ @GuardedBy("mBsi")
+ @Override
+ public long getCameraEnergyConsumptionUC() {
+ return getEnergyConsumptionUC(EnergyConsumerStats.POWER_BUCKET_CAMERA);
+ }
+
/**
* Gets the minimum of the uid's foreground activity time and its PROCESS_STATE_TOP time
* since last marked. Also sets the mark time for both these timers.
@@ -8477,6 +8498,20 @@
return gnssTimeUs;
}
+ /**
+ * Gets the uid's time spent using the camera since last marked. Also sets the mark time for
+ * the camera timer.
+ */
+ private long markCameraTimeUs(long elapsedRealtimeMs) {
+ final StopwatchTimer timer = mCameraTurnedOnTimer;
+ if (timer == null) {
+ return 0;
+ }
+ final long cameraTimeUs = timer.getTimeSinceMarkLocked(elapsedRealtimeMs * 1000);
+ timer.setMark(elapsedRealtimeMs);
+ return cameraTimeUs;
+ }
+
public StopwatchTimer createAudioTurnedOnTimerLocked() {
if (mAudioTurnedOnTimer == null) {
mAudioTurnedOnTimer = new StopwatchTimer(mBsi.mClock, Uid.this, AUDIO_TURNED_ON,
@@ -12902,6 +12937,53 @@
}
/**
+ * Accumulate camera charge consumption and distribute it to the correct state and the apps.
+ *
+ * @param chargeUC amount of charge (microcoulombs) used by the camera since this was last
+ * called.
+ */
+ @GuardedBy("this")
+ public void updateCameraEnergyConsumerStatsLocked(long chargeUC, long elapsedRealtimeMs) {
+ if (DEBUG_ENERGY) Slog.d(TAG, "Updating camera stats: " + chargeUC);
+ if (mGlobalEnergyConsumerStats == null) {
+ return;
+ }
+
+ if (!mOnBatteryInternal || chargeUC <= 0) {
+ // There's nothing further to update.
+ return;
+ }
+
+ if (mIgnoreNextExternalStats) {
+ // Although under ordinary resets we won't get here, and typically a new sync will
+ // happen right after the reset, strictly speaking we need to set all mark times to now.
+ final int uidStatsSize = mUidStats.size();
+ for (int i = 0; i < uidStatsSize; i++) {
+ final Uid uid = mUidStats.valueAt(i);
+ uid.markCameraTimeUs(elapsedRealtimeMs);
+ }
+ return;
+ }
+
+ mGlobalEnergyConsumerStats.updateStandardBucket(
+ EnergyConsumerStats.POWER_BUCKET_CAMERA, chargeUC);
+
+ // Collect the per uid time since mark so that we can normalize power.
+ final SparseDoubleArray cameraTimeUsArray = new SparseDoubleArray();
+
+ // Note: Iterating over all UIDs may be suboptimal.
+ final int uidStatsSize = mUidStats.size();
+ for (int i = 0; i < uidStatsSize; i++) {
+ final Uid uid = mUidStats.valueAt(i);
+ final long cameraTimeUs = uid.markCameraTimeUs(elapsedRealtimeMs);
+ if (cameraTimeUs == 0) continue;
+ cameraTimeUsArray.put(uid.getUid(), (double) cameraTimeUs);
+ }
+ distributeEnergyToUidsLocked(EnergyConsumerStats.POWER_BUCKET_CAMERA, chargeUC,
+ cameraTimeUsArray, 0, elapsedRealtimeMs);
+ }
+
+ /**
* Accumulate Custom power bucket charge, globally and for each app.
*
* @param totalChargeUC charge (microcoulombs) used for this bucket since this was last called.
diff --git a/services/core/java/com/android/server/power/stats/CameraPowerCalculator.java b/services/core/java/com/android/server/power/stats/CameraPowerCalculator.java
index 16892034..89991bf 100644
--- a/services/core/java/com/android/server/power/stats/CameraPowerCalculator.java
+++ b/services/core/java/com/android/server/power/stats/CameraPowerCalculator.java
@@ -48,27 +48,44 @@
long rawRealtimeUs, long rawUptimeUs, BatteryUsageStatsQuery query) {
super.calculate(builder, batteryStats, rawRealtimeUs, rawUptimeUs, query);
- final long durationMs = batteryStats.getCameraOnTime(rawRealtimeUs,
- BatteryStats.STATS_SINCE_CHARGED) / 1000;
- final double powerMah = mPowerEstimator.calculatePower(durationMs);
+ long consumptionUc = batteryStats.getCameraEnergyConsumptionUC();
+ int powerModel = getPowerModel(consumptionUc, query);
+ long durationMs =
+ batteryStats.getCameraOnTime(
+ rawRealtimeUs, BatteryStats.STATS_SINCE_CHARGED) / 1000;
+ double powerMah;
+ if (powerModel == BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION) {
+ powerMah = uCtoMah(consumptionUc);
+ } else {
+ powerMah = mPowerEstimator.calculatePower(durationMs);
+ }
+
builder.getAggregateBatteryConsumerBuilder(
- BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA, durationMs)
- .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA, powerMah);
+ .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA, powerMah, powerModel);
builder.getAggregateBatteryConsumerBuilder(
- BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS)
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS)
.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA, durationMs)
- .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA, powerMah);
+ .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA, powerMah, powerModel);
}
@Override
protected void calculateApp(UidBatteryConsumer.Builder app, BatteryStats.Uid u,
long rawRealtimeUs, long rawUptimeUs, BatteryUsageStatsQuery query) {
- final long durationMs =
+ long consumptionUc = app.getBatteryStatsUid().getCameraEnergyConsumptionUC();
+ int powerModel = getPowerModel(consumptionUc, query);
+ long durationMs =
mPowerEstimator.calculateDuration(u.getCameraTurnedOnTimer(), rawRealtimeUs,
BatteryStats.STATS_SINCE_CHARGED);
- final double powerMah = mPowerEstimator.calculatePower(durationMs);
+ double powerMah;
+ if (powerModel == BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION) {
+ powerMah = uCtoMah(consumptionUc);
+ } else {
+ powerMah = mPowerEstimator.calculatePower(durationMs);
+ }
+
app.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA, durationMs)
- .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA, powerMah);
+ .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA, powerMah, powerModel);
}
}
diff --git a/services/core/java/com/android/server/power/stats/EnergyConsumerSnapshot.java b/services/core/java/com/android/server/power/stats/EnergyConsumerSnapshot.java
index 18595ca..939a08b 100644
--- a/services/core/java/com/android/server/power/stats/EnergyConsumerSnapshot.java
+++ b/services/core/java/com/android/server/power/stats/EnergyConsumerSnapshot.java
@@ -123,6 +123,9 @@
/** The chargeUC for {@link EnergyConsumerType#WIFI}. */
public long wifiChargeUC = UNAVAILABLE;
+ /** The chargeUC for {@link EnergyConsumerType#CAMERA}. */
+ public long cameraChargeUC = UNAVAILABLE;
+
/** Map of {@link EnergyConsumerType#OTHER} ordinals to their total chargeUC. */
public @Nullable long[] otherTotalChargeUC = null;
@@ -256,6 +259,10 @@
output.wifiChargeUC = deltaChargeUC;
break;
+ case EnergyConsumerType.CAMERA:
+ output.cameraChargeUC = deltaChargeUC;
+ break;
+
case EnergyConsumerType.OTHER:
if (output.otherTotalChargeUC == null) {
output.otherTotalChargeUC = new long[mNumOtherOrdinals];
@@ -458,6 +465,9 @@
case EnergyConsumerType.WIFI:
chargeUC[i] = delta.wifiChargeUC;
break;
+ case EnergyConsumerType.CAMERA:
+ chargeUC[i] = delta.cameraChargeUC;
+ break;
case EnergyConsumerType.OTHER:
if (delta.otherTotalChargeUC != null) {
chargeUC[i] = delta.otherTotalChargeUC[energyConsumer.ordinal];
diff --git a/services/core/java/com/android/server/resources/ResourcesManagerService.java b/services/core/java/com/android/server/resources/ResourcesManagerService.java
index eec3a02..94aa518 100644
--- a/services/core/java/com/android/server/resources/ResourcesManagerService.java
+++ b/services/core/java/com/android/server/resources/ResourcesManagerService.java
@@ -74,8 +74,8 @@
@Override
protected void dump(@NonNull FileDescriptor fd,
@NonNull PrintWriter pw, @Nullable String[] args) {
- try {
- mActivityManagerService.dumpAllResources(ParcelFileDescriptor.dup(fd), pw);
+ try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(fd)) {
+ mActivityManagerService.dumpAllResources(pfd, pw);
} catch (Exception e) {
pw.println("Exception while trying to dump all resources: " + e.getMessage());
e.printStackTrace(pw);
diff --git a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java
index 7d8336a..a75d110 100644
--- a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java
+++ b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java
@@ -62,13 +62,12 @@
private int dumpResources() throws RemoteException {
String processId = getNextArgRequired();
- try {
+ try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(getOutFileDescriptor())) {
ConditionVariable lock = new ConditionVariable();
RemoteCallback
finishCallback = new RemoteCallback(result -> lock.open(), null);
- if (!mInterface.dumpResources(processId,
- ParcelFileDescriptor.dup(getOutFileDescriptor()), finishCallback)) {
+ if (!mInterface.dumpResources(processId, pfd, finishCallback)) {
getErrPrintWriter().println("RESOURCES DUMP FAILED on process " + processId);
return -1;
}
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 392fda9..0fd6d9b 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -39,7 +39,7 @@
void cancelPreloadRecentApps();
- void showRecentApps(boolean triggeredFromAltTab);
+ void showRecentApps(boolean triggeredFromAltTab, boolean forward);
void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey);
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 8d71d9c..97ca8df 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -454,10 +454,10 @@
}
@Override
- public void showRecentApps(boolean triggeredFromAltTab) {
+ public void showRecentApps(boolean triggeredFromAltTab, boolean forward) {
if (mBar != null) {
try {
- mBar.showRecentApps(triggeredFromAltTab);
+ mBar.showRecentApps(triggeredFromAltTab, forward);
} catch (RemoteException ex) {}
}
}
diff --git a/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java b/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java
index b0d301e..0809297 100644
--- a/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java
+++ b/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java
@@ -358,12 +358,52 @@
@NonNull
private final LocalLog mLocalDebugLog = new LocalLog(30, false /* useLocalTimestamps */);
+ /**
+ * The usual interval between refresh attempts. Always used after a successful request.
+ *
+ * <p>The value also determines whether a network time result is considered fresh.
+ * Refreshes only take place from this class when the latest time result is considered too
+ * old.
+ */
private final int mNormalPollingIntervalMillis;
+
+ /**
+ * A shortened interval between refresh attempts used after a failure to refresh.
+ * Always shorter than {@link #mNormalPollingIntervalMillis} and only used when {@link
+ * #mTryAgainTimesMax} != 0.
+ *
+ * <p>This value is also the lower bound for the interval allowed between successive
+ * refreshes when the latest time result is missing or too old, e.g. a refresh may not be
+ * triggered when network connectivity is restored if the last attempt was too recent.
+ */
private final int mShortPollingIntervalMillis;
+
+ /**
+ * The number of times {@link #mShortPollingIntervalMillis} can be used after successive
+ * failures before switching back to using {@link #mNormalPollingIntervalMillis} once before
+ * repeating. When this value is negative, the refresh algorithm will continue to use {@link
+ * #mShortPollingIntervalMillis} until a successful refresh.
+ */
private final int mTryAgainTimesMax;
+
private final NtpTrustedTime mNtpTrustedTime;
/**
+ * Records the time of the last refresh attempt (successful or otherwise) by this service.
+ * This is used when scheduling the next refresh attempt. In cases where {@link
+ * #refreshIfRequiredAndReschedule} is called too frequently, this will prevent each call
+ * resulting in a network request. See also {@link #mShortPollingIntervalMillis}.
+ *
+ * <p>Time servers are a shared resource and so Android should avoid loading them.
+ * Generally, a refresh attempt will succeed and the service won't need to make further
+ * requests and this field will not limit requests.
+ */
+ // This field is only updated and accessed by the mHandler thread (except dump()).
+ @GuardedBy("this")
+ @ElapsedRealtimeLong
+ private Long mLastRefreshAttemptElapsedRealtimeMillis;
+
+ /**
* Keeps track of successive time refresh failures have occurred. This is reset to zero when
* time refresh is successful or if the number exceeds (a non-negative) {@link
* #mTryAgainTimesMax}.
@@ -378,6 +418,11 @@
int normalPollingIntervalMillis, int shortPollingIntervalMillis,
int tryAgainTimesMax, @NonNull NtpTrustedTime ntpTrustedTime) {
mElapsedRealtimeMillisSupplier = Objects.requireNonNull(elapsedRealtimeMillisSupplier);
+ if (shortPollingIntervalMillis > normalPollingIntervalMillis) {
+ throw new IllegalArgumentException(String.format(
+ "shortPollingIntervalMillis (%s) > normalPollingIntervalMillis (%s)",
+ shortPollingIntervalMillis, normalPollingIntervalMillis));
+ }
mNormalPollingIntervalMillis = normalPollingIntervalMillis;
mShortPollingIntervalMillis = shortPollingIntervalMillis;
mTryAgainTimesMax = tryAgainTimesMax;
@@ -387,81 +432,139 @@
@Override
public boolean forceRefreshForTests(
@NonNull Network network, @NonNull RefreshCallbacks refreshCallbacks) {
- boolean success = mNtpTrustedTime.forceRefresh(network);
- logToDebugAndDumpsys("forceRefreshForTests: success=" + success);
+ boolean refreshSuccessful = tryRefresh(network);
+ logToDebugAndDumpsys("forceRefreshForTests: refreshSuccessful=" + refreshSuccessful);
- if (success) {
+ if (refreshSuccessful) {
makeNetworkTimeSuggestion(mNtpTrustedTime.getCachedTimeResult(),
"EngineImpl.forceRefreshForTests()", refreshCallbacks);
}
- return success;
+ return refreshSuccessful;
}
@Override
public void refreshIfRequiredAndReschedule(
@NonNull Network network, @NonNull String reason,
@NonNull RefreshCallbacks refreshCallbacks) {
- long currentElapsedRealtimeMillis = mElapsedRealtimeMillisSupplier.get();
-
- final int maxNetworkTimeAgeMillis = mNormalPollingIntervalMillis;
- // Force an NTP fix when outdated
+ // Attempt to refresh the network time if there is no latest time result, or if the
+ // latest time result is considered too old.
NtpTrustedTime.TimeResult initialTimeResult = mNtpTrustedTime.getCachedTimeResult();
- if (calculateTimeResultAgeMillis(initialTimeResult, currentElapsedRealtimeMillis)
- >= maxNetworkTimeAgeMillis) {
- if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh using network=" + network);
- boolean successful = mNtpTrustedTime.forceRefresh(network);
- if (successful) {
- synchronized (this) {
- mTryAgainCounter = 0;
- }
- } else {
- String logMsg = "forceRefresh() returned false:"
- + " initialTimeResult=" + initialTimeResult
- + ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis;
- logToDebugAndDumpsys(logMsg);
- }
+ boolean shouldAttemptRefresh;
+ synchronized (this) {
+ long currentElapsedRealtimeMillis = mElapsedRealtimeMillisSupplier.get();
+
+ // calculateTimeResultAgeMillis() safely handles a null initialTimeResult.
+ long timeResultAgeMillis = calculateTimeResultAgeMillis(
+ initialTimeResult, currentElapsedRealtimeMillis);
+ shouldAttemptRefresh =
+ timeResultAgeMillis >= mNormalPollingIntervalMillis
+ && isRefreshAllowed(currentElapsedRealtimeMillis);
+ }
+
+ boolean refreshSuccessful = false;
+ if (shouldAttemptRefresh) {
+ // This is a blocking call. Deliberately invoked without holding the "this" monitor
+ // to avoid blocking logic that wants to use the "this" monitor.
+ refreshSuccessful = tryRefresh(network);
}
synchronized (this) {
- long nextPollDelayMillis;
- NtpTrustedTime.TimeResult latestTimeResult = mNtpTrustedTime.getCachedTimeResult();
- if (calculateTimeResultAgeMillis(latestTimeResult, currentElapsedRealtimeMillis)
- < maxNetworkTimeAgeMillis) {
- // Obtained fresh fix; schedule next normal update
- nextPollDelayMillis = mNormalPollingIntervalMillis
- - latestTimeResult.getAgeMillis(currentElapsedRealtimeMillis);
-
- makeNetworkTimeSuggestion(latestTimeResult, reason, refreshCallbacks);
- } else {
- // No fresh fix; schedule retry
- mTryAgainCounter++;
- if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) {
- nextPollDelayMillis = mShortPollingIntervalMillis;
- } else {
- // Try much later
+ // Manage mTryAgainCounter.
+ if (shouldAttemptRefresh) {
+ if (refreshSuccessful) {
+ // Reset failure tracking.
mTryAgainCounter = 0;
-
- nextPollDelayMillis = mNormalPollingIntervalMillis;
+ } else {
+ if (mTryAgainTimesMax < 0) {
+ // When mTryAgainTimesMax is negative there's no enforced maximum and
+ // short intervals should be used until a successful refresh. Setting
+ // mTryAgainCounter to 1 is sufficient for the interval calculations
+ // below. There's no need to increment.
+ mTryAgainCounter = 1;
+ } else {
+ mTryAgainCounter++;
+ if (mTryAgainCounter > mTryAgainTimesMax) {
+ mTryAgainCounter = 0;
+ }
+ }
}
}
- long nextRefreshElapsedRealtimeMillis =
- currentElapsedRealtimeMillis + nextPollDelayMillis;
+
+ // currentElapsedRealtimeMillis is used to evaluate ages and refresh scheduling
+ // below. Capturing this after a possible successful refresh ensures that latest
+ // time result ages will be >= 0.
+ long currentElapsedRealtimeMillis = mElapsedRealtimeMillisSupplier.get();
+
+ // This section of code deliberately doesn't assume it is the only component using
+ // mNtpTrustedTime to obtain NTP times: another component in the same process could
+ // be gathering NTP signals (which then won't have been suggested to the time
+ // detector).
+ // TODO(b/222295093): Make this class the sole owner of mNtpTrustedTime and
+ // simplify / reduce duplicate suggestions.
+ NtpTrustedTime.TimeResult latestTimeResult = mNtpTrustedTime.getCachedTimeResult();
+ long latestTimeResultAgeMillis = calculateTimeResultAgeMillis(
+ latestTimeResult, currentElapsedRealtimeMillis);
+
+ // Suggest the latest time result to the time detector if it is fresh regardless of
+ // whether refresh happened above.
+ if (latestTimeResultAgeMillis < mNormalPollingIntervalMillis) {
+ // We assume the time detector service will detect duplicate suggestions and not
+ // do more work than it has to, so no need to avoid making duplicate
+ // suggestions.
+ makeNetworkTimeSuggestion(latestTimeResult, reason, refreshCallbacks);
+ }
+
+ // (Re)schedule the next refresh based on the latest state.
+ // Determine which refresh delay to use by using the current value of
+ // mTryAgainCounter. The refresh delay is applied to a different point in time
+ // depending on whether the latest available time result (if any) is still
+ // considered fresh to ensure the delay acts correctly.
+ long refreshDelayMillis = mTryAgainCounter > 0
+ ? mShortPollingIntervalMillis : mNormalPollingIntervalMillis;
+ long nextRefreshElapsedRealtimeMillis;
+ if (latestTimeResultAgeMillis < mNormalPollingIntervalMillis) {
+ // The latest time result is fresh, use it to determine when next to refresh.
+ nextRefreshElapsedRealtimeMillis =
+ latestTimeResult.getElapsedRealtimeMillis() + refreshDelayMillis;
+ } else if (mLastRefreshAttemptElapsedRealtimeMillis != null) {
+ // The latest time result is missing or old and still needs to be refreshed.
+ // mLastRefreshAttemptElapsedRealtimeMillis, which should always be set by this
+ // point because there's no fresh time result, should be very close to
+ // currentElapsedRealtimeMillis unless the refresh was not allowed.
+ nextRefreshElapsedRealtimeMillis =
+ mLastRefreshAttemptElapsedRealtimeMillis + refreshDelayMillis;
+ } else {
+ // This should not happen: mLastRefreshAttemptElapsedRealtimeMillis should
+ // always be non-null by this point.
+ logToDebugAndDumpsys(
+ "mLastRefreshAttemptElapsedRealtimeMillis unexpectedly missing."
+ + " Scheduling using currentElapsedRealtimeMillis");
+ nextRefreshElapsedRealtimeMillis =
+ currentElapsedRealtimeMillis + refreshDelayMillis;
+ }
refreshCallbacks.scheduleNextRefresh(nextRefreshElapsedRealtimeMillis);
logToDebugAndDumpsys("refreshIfRequiredAndReschedule:"
+ " network=" + network
+ ", reason=" + reason
- + ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis
+ ", initialTimeResult=" + initialTimeResult
+ + ", shouldAttemptRefresh=" + shouldAttemptRefresh
+ + ", refreshSuccessful=" + refreshSuccessful
+ + ", currentElapsedRealtimeMillis="
+ + formatElapsedRealtimeMillis(currentElapsedRealtimeMillis)
+ ", latestTimeResult=" + latestTimeResult
+ ", mTryAgainCounter=" + mTryAgainCounter
- + ", nextPollDelayMillis=" + nextPollDelayMillis
+ + ", refreshDelayMillis=" + refreshDelayMillis
+ ", nextRefreshElapsedRealtimeMillis="
- + Duration.ofMillis(nextRefreshElapsedRealtimeMillis)
- + " (" + nextRefreshElapsedRealtimeMillis + ")");
+ + formatElapsedRealtimeMillis(nextRefreshElapsedRealtimeMillis));
}
}
+ private static String formatElapsedRealtimeMillis(
+ @ElapsedRealtimeLong long elapsedRealtimeMillis) {
+ return Duration.ofMillis(elapsedRealtimeMillis) + " (" + elapsedRealtimeMillis + ")";
+ }
+
private static long calculateTimeResultAgeMillis(
@Nullable TimeResult timeResult,
@ElapsedRealtimeLong long currentElapsedRealtimeMillis) {
@@ -469,6 +572,26 @@
: timeResult.getAgeMillis(currentElapsedRealtimeMillis);
}
+ @GuardedBy("this")
+ private boolean isRefreshAllowed(@ElapsedRealtimeLong long currentElapsedRealtimeMillis) {
+ if (mLastRefreshAttemptElapsedRealtimeMillis == null) {
+ return true;
+ }
+ // Use the second meaning of mShortPollingIntervalMillis: to determine the minimum time
+ // allowed after an unsuccessful refresh before another can be attempted.
+ long nextRefreshAllowedElapsedRealtimeMillis =
+ mLastRefreshAttemptElapsedRealtimeMillis + mShortPollingIntervalMillis;
+ return currentElapsedRealtimeMillis >= nextRefreshAllowedElapsedRealtimeMillis;
+ }
+
+ private boolean tryRefresh(@NonNull Network network) {
+ long currentElapsedRealtimeMillis = mElapsedRealtimeMillisSupplier.get();
+ synchronized (this) {
+ mLastRefreshAttemptElapsedRealtimeMillis = currentElapsedRealtimeMillis;
+ }
+ return mNtpTrustedTime.forceRefresh(network);
+ }
+
/** Suggests the time to the time detector. It may choose use it to set the system clock. */
private void makeNetworkTimeSuggestion(@NonNull TimeResult ntpResult,
@NonNull String debugInfo, @NonNull RefreshCallbacks refreshCallbacks) {
@@ -489,6 +612,10 @@
ipw.println("mTryAgainTimesMax=" + mTryAgainTimesMax);
synchronized (this) {
+ String lastRefreshAttemptValue = mLastRefreshAttemptElapsedRealtimeMillis == null
+ ? "null"
+ : formatElapsedRealtimeMillis(mLastRefreshAttemptElapsedRealtimeMillis);
+ ipw.println("mLastRefreshAttemptElapsedRealtimeMillis=" + lastRefreshAttemptValue);
ipw.println("mTryAgainCounter=" + mTryAgainCounter);
}
ipw.println();
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index ebee995..d4f2f2d 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -24,11 +24,13 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.graphics.Rect;
+import android.media.PlaybackParams;
import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.AdResponse;
@@ -85,6 +87,11 @@
public class TvInteractiveAppManagerService extends SystemService {
private static final boolean DEBUG = false;
private static final String TAG = "TvInteractiveAppManagerService";
+
+ private static final String METADATA_CLASS_NAME =
+ "android.media.tv.interactive.AppLinkInfo.ClassName";
+ private static final String METADATA_URI =
+ "android.media.tv.interactive.AppLinkInfo.Uri";
// A global lock.
private final Object mLock = new Object();
private final Context mContext;
@@ -101,6 +108,8 @@
// TODO: remove mGetServiceListCalled if onBootPhrase work correctly
@GuardedBy("mLock")
private boolean mGetServiceListCalled = false;
+ @GuardedBy("mLock")
+ private boolean mGetAppLinkInfoListCalled = false;
private final UserManager mUserManager;
@@ -120,6 +129,41 @@
}
@GuardedBy("mLock")
+ private void buildAppLinkInfoLocked(int userId) {
+ UserState userState = getOrCreateUserStateLocked(userId);
+ if (DEBUG) {
+ Slogf.d(TAG, "buildAppLinkInfoLocked");
+ }
+ PackageManager pm = mContext.getPackageManager();
+ List<ApplicationInfo> appInfos = pm.getInstalledApplicationsAsUser(
+ PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA), userId);
+ List<AppLinkInfo> appLinkInfos = new ArrayList<>();
+ for (ApplicationInfo appInfo : appInfos) {
+ AppLinkInfo info = buildAppLinkInfoLocked(appInfo);
+ if (info != null) {
+ appLinkInfos.add(info);
+ }
+ }
+ // sort the list by package name
+ Collections.sort(appLinkInfos, Comparator.comparing(AppLinkInfo::getComponentName));
+ userState.mAppLinkInfoList.clear();
+ userState.mAppLinkInfoList.addAll(appLinkInfos);
+ }
+
+ @GuardedBy("mLock")
+ private AppLinkInfo buildAppLinkInfoLocked(ApplicationInfo appInfo) {
+ if (appInfo.metaData == null || appInfo.packageName == null) {
+ return null;
+ }
+ String className = appInfo.metaData.getString(METADATA_CLASS_NAME, null);
+ String uri = appInfo.metaData.getString(METADATA_URI, null);
+ if (className == null || uri == null) {
+ return null;
+ }
+ return new AppLinkInfo(appInfo.packageName, className, uri);
+ }
+
+ @GuardedBy("mLock")
private void buildTvInteractiveAppServiceListLocked(int userId, String[] updatedPackages) {
UserState userState = getOrCreateUserStateLocked(userId);
userState.mPackageSet.clear();
@@ -310,6 +354,7 @@
} else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
synchronized (mLock) {
buildTvInteractiveAppServiceListLocked(mCurrentUserId, null);
+ buildAppLinkInfoLocked(mCurrentUserId);
}
}
}
@@ -321,6 +366,7 @@
synchronized (mLock) {
if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
buildTvInteractiveAppServiceListLocked(userId, packages);
+ buildAppLinkInfoLocked(userId);
}
}
}
@@ -427,6 +473,7 @@
mCurrentUserId = userId;
buildTvInteractiveAppServiceListLocked(userId, null);
+ buildAppLinkInfoLocked(userId);
}
}
@@ -512,6 +559,7 @@
private void startProfileLocked(int userId) {
mRunningProfiles.add(userId);
buildTvInteractiveAppServiceListLocked(userId, null);
+ buildAppLinkInfoLocked(userId);
}
@GuardedBy("mLock")
@@ -667,6 +715,26 @@
}
@Override
+ public List<AppLinkInfo> getAppLinkInfoList(int userId) {
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, "getAppLinkInfoList");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ if (!mGetAppLinkInfoListCalled) {
+ buildAppLinkInfoLocked(userId);
+ mGetAppLinkInfoListCalled = true;
+ }
+ UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+ List<AppLinkInfo> appLinkInfos = new ArrayList<>(userState.mAppLinkInfoList);
+ return appLinkInfos;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void registerAppLinkInfo(String tiasId, AppLinkInfo appLinkInfo, int userId) {
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "registerAppLinkInfo: " + appLinkInfo);
@@ -1268,6 +1336,31 @@
}
@Override
+ public void sendCurrentVideoBounds(IBinder sessionToken, Rect bounds, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "sendCurrentVideoBounds(bounds=%s)", bounds.toString());
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "sendCurrentVideoBounds");
+ SessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).sendCurrentVideoBounds(bounds);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in sendCurrentVideoBounds", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void sendCurrentChannelUri(IBinder sessionToken, Uri channelUri, int userId) {
if (DEBUG) {
Slogf.d(TAG, "sendCurrentChannelUri(channelUri=%s)", channelUri.toString());
@@ -1496,6 +1589,118 @@
}
@Override
+ public void notifyTimeShiftPlaybackParams(
+ IBinder sessionToken, PlaybackParams params, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "notifyTimeShiftPlaybackParams(params=%s)", params);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(
+ Binder.getCallingPid(), callingUid, userId, "notifyTimeShiftPlaybackParams");
+ SessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState =
+ getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+ getSessionLocked(sessionState).notifyTimeShiftPlaybackParams(params);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in notifyTimeShiftPlaybackParams", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void notifyTimeShiftStatusChanged(
+ IBinder sessionToken, String inputId, int status, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "notifyTimeShiftStatusChanged(inputId=%s, status=%d)",
+ inputId, status);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(
+ Binder.getCallingPid(), callingUid, userId, "notifyTimeShiftStatusChanged");
+ SessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState =
+ getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+ getSessionLocked(sessionState).notifyTimeShiftStatusChanged(
+ inputId, status);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in notifyTimeShiftStatusChanged", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void notifyTimeShiftStartPositionChanged(
+ IBinder sessionToken, String inputId, long timeMs, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "notifyTimeShiftStartPositionChanged(inputId=%s, timeMs=%d)",
+ inputId, timeMs);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(
+ Binder.getCallingPid(), callingUid, userId,
+ "notifyTimeShiftStartPositionChanged");
+ SessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState =
+ getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+ getSessionLocked(sessionState).notifyTimeShiftStartPositionChanged(
+ inputId, timeMs);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in notifyTimeShiftStartPositionChanged", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void notifyTimeShiftCurrentPositionChanged(
+ IBinder sessionToken, String inputId, long timeMs, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "notifyTimeShiftCurrentPositionChanged(inputId=%s, timeMs=%d)",
+ inputId, timeMs);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(
+ Binder.getCallingPid(), callingUid, userId,
+ "notifyTimeShiftCurrentPositionChanged");
+ SessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState =
+ getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+ getSessionLocked(sessionState).notifyTimeShiftCurrentPositionChanged(
+ inputId, timeMs);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in notifyTimeShiftCurrentPositionChanged", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void setSurface(IBinder sessionToken, Surface surface, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
@@ -1883,6 +2088,8 @@
// A set of all TV Interactive App service packages.
private final Set<String> mPackageSet = new HashSet<>();
+ // A list of all app link infos.
+ private final List<AppLinkInfo> mAppLinkInfoList = new ArrayList<>();
// A list of callbacks.
private final RemoteCallbackList<ITvInteractiveAppManagerCallback> mCallbacks =
@@ -2255,6 +2462,27 @@
}
@Override
+ public void onTimeShiftCommandRequest(
+ @TvInteractiveAppService.TimeShiftCommandType String cmdType,
+ Bundle parameters) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onTimeShiftCommandRequest (cmdType=" + cmdType
+ + ", parameters=" + parameters.toString() + ")");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onTimeShiftCommandRequest(
+ cmdType, parameters, mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onTimeShiftCommandRequest", e);
+ }
+ }
+ }
+
+ @Override
public void onSetVideoBounds(Rect rect) {
synchronized (mLock) {
if (DEBUG) {
@@ -2272,6 +2500,23 @@
}
@Override
+ public void onRequestCurrentVideoBounds() {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestCurrentVideoBounds");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestCurrentVideoBounds(mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestCurrentVideoBounds", e);
+ }
+ }
+ }
+
+ @Override
public void onRequestCurrentChannelUri() {
synchronized (mLock) {
if (DEBUG) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java
index 79de282..25ce280 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperData.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java
@@ -18,10 +18,10 @@
import static android.app.WallpaperManager.FLAG_LOCK;
-import static com.android.server.wallpaper.WallpaperManagerService.WALLPAPER;
-import static com.android.server.wallpaper.WallpaperManagerService.WALLPAPER_CROP;
-import static com.android.server.wallpaper.WallpaperManagerService.WALLPAPER_LOCK_CROP;
-import static com.android.server.wallpaper.WallpaperManagerService.WALLPAPER_LOCK_ORIG;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_LOCK_CROP;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_LOCK_ORIG;
import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir;
import android.app.IWallpaperManagerCallback;
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 3d59b7b..6edfebf 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -32,7 +32,14 @@
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static com.android.server.wallpaper.WallpaperUtils.RECORD_FILE;
+import static com.android.server.wallpaper.WallpaperUtils.RECORD_LOCK_FILE;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_INFO;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_LOCK_ORIG;
import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir;
+import static com.android.server.wallpaper.WallpaperUtils.makeWallpaperIdLocked;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -199,20 +206,6 @@
*/
private static final long MIN_WALLPAPER_CRASH_TIME = 10000;
private static final int MAX_WALLPAPER_COMPONENT_LOG_LENGTH = 128;
- static final String WALLPAPER = "wallpaper_orig";
- static final String WALLPAPER_CROP = "wallpaper";
- static final String WALLPAPER_LOCK_ORIG = "wallpaper_lock_orig";
- static final String WALLPAPER_LOCK_CROP = "wallpaper_lock";
- static final String WALLPAPER_INFO = "wallpaper_info.xml";
- private static final String RECORD_FILE = "decode_record";
- private static final String RECORD_LOCK_FILE = "decode_lock_record";
-
- // All the various per-user state files we need to be aware of
- private static final String[] sPerUserFiles = new String[] {
- WALLPAPER, WALLPAPER_CROP,
- WALLPAPER_LOCK_ORIG, WALLPAPER_LOCK_CROP,
- WALLPAPER_INFO
- };
/**
* Observes the wallpaper for changes and notifies all IWallpaperServiceCallbacks
@@ -883,18 +876,15 @@
*/
private final SparseArray<SparseArray<RemoteCallbackList<IWallpaperManagerCallback>>>
mColorsChangedListeners;
+ // The currently bound home or home+lock wallpaper
protected WallpaperData mLastWallpaper;
+ // The currently bound lock screen only wallpaper, or null if none
+ protected WallpaperData mLastLockWallpaper;
private IWallpaperManagerCallback mKeyguardListener;
private boolean mWaitingForUnlock;
private boolean mShuttingDown;
/**
- * ID of the current wallpaper, changed every time anything sets a wallpaper.
- * This is used for external detection of wallpaper update activity.
- */
- private int mWallpaperId;
-
- /**
* Name of the component used to display bitmap wallpapers from either the gallery or
* built-in wallpapers.
*/
@@ -976,13 +966,6 @@
}
}
- int makeWallpaperIdLocked() {
- do {
- ++mWallpaperId;
- } while (mWallpaperId == 0);
- return mWallpaperId;
- }
-
private boolean supportsMultiDisplay(WallpaperConnection connection) {
if (connection != null) {
return connection.mInfo == null // This is image wallpaper
@@ -1852,11 +1835,9 @@
final TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
t.traceBegin("Wallpaper_selinux_restorecon-" + userId);
try {
- final File wallpaperDir = getWallpaperDir(userId);
- for (String filename : sPerUserFiles) {
- File f = new File(wallpaperDir, filename);
- if (f.exists()) {
- SELinux.restorecon(f);
+ for (File file: WallpaperUtils.getWallpaperFiles(userId)) {
+ if (file.exists()) {
+ SELinux.restorecon(file);
}
}
} finally {
@@ -1872,12 +1853,9 @@
void onRemoveUser(int userId) {
if (userId < 1) return;
- final File wallpaperDir = getWallpaperDir(userId);
synchronized (mLock) {
stopObserversLocked(userId);
- for (String filename : sPerUserFiles) {
- new File(wallpaperDir, filename).delete();
- }
+ WallpaperUtils.getWallpaperFiles(userId).forEach(File::delete);
mUserRestorecon.delete(userId);
}
}
@@ -2874,8 +2852,8 @@
setWallpaperComponent(name, UserHandle.getCallingUserId(), FLAG_SYSTEM);
}
- private void setWallpaperComponent(ComponentName name, @SetWallpaperFlags int which,
- int userId) {
+ @VisibleForTesting
+ void setWallpaperComponent(ComponentName name, @SetWallpaperFlags int which, int userId) {
userId = ActivityManager.handleIncomingUser(getCallingPid(), getCallingUid(), userId,
false /* all */, true /* full */, "changing live wallpaper", null /* pkg */);
checkPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT);
@@ -3096,14 +3074,19 @@
Slog.w(TAG, msg);
return false;
}
- if (wallpaper.userId == mCurrentUserId && mLastWallpaper != null
+ if (mEnableSeparateLockScreenEngine) {
+ maybeDetachLastWallpapers(wallpaper);
+ } else if (wallpaper.userId == mCurrentUserId && mLastWallpaper != null
&& !wallpaper.equals(mFallbackWallpaper)) {
detachWallpaperLocked(mLastWallpaper);
}
wallpaper.wallpaperComponent = componentName;
wallpaper.connection = newConn;
newConn.mReply = reply;
- if (wallpaper.userId == mCurrentUserId && !wallpaper.equals(mFallbackWallpaper)) {
+ if (mEnableSeparateLockScreenEngine) {
+ updateCurrentWallpapers(wallpaper);
+ } else if (wallpaper.userId == mCurrentUserId && !wallpaper.equals(
+ mFallbackWallpaper)) {
mLastWallpaper = wallpaper;
}
updateFallbackConnection();
@@ -3120,6 +3103,40 @@
return true;
}
+ // Updates tracking of the currently bound wallpapers. Assumes mEnableSeparateLockScreenEngine
+ // is true.
+ private void updateCurrentWallpapers(WallpaperData newWallpaper) {
+ if (newWallpaper.userId == mCurrentUserId && !newWallpaper.equals(mFallbackWallpaper)) {
+ if (newWallpaper.mWhich == (FLAG_SYSTEM | FLAG_LOCK)) {
+ mLastWallpaper = newWallpaper;
+ mLastLockWallpaper = null;
+ } else if (newWallpaper.mWhich == FLAG_SYSTEM) {
+ mLastWallpaper = newWallpaper;
+ } else if (newWallpaper.mWhich == FLAG_LOCK) {
+ mLastLockWallpaper = newWallpaper;
+ }
+ }
+ }
+
+ // Detaches previously bound wallpapers if no longer in use. Assumes
+ // mEnableSeparateLockScreenEngine is true.
+ private void maybeDetachLastWallpapers(WallpaperData newWallpaper) {
+ if (newWallpaper.userId != mCurrentUserId || newWallpaper.equals(mFallbackWallpaper)) {
+ return;
+ }
+ boolean homeUpdated = (newWallpaper.mWhich & FLAG_SYSTEM) != 0;
+ boolean lockUpdated = (newWallpaper.mWhich & FLAG_LOCK) != 0;
+ // This is the case where a home+lock wallpaper was changed to home-only, and the old
+ // home+lock became (static) or will become (live) lock-only.
+ boolean lockNeedsHomeWallpaper = mLastLockWallpaper == null && !lockUpdated;
+ if (mLastWallpaper != null && homeUpdated && !lockNeedsHomeWallpaper) {
+ detachWallpaperLocked(mLastWallpaper);
+ }
+ if (mLastLockWallpaper != null && lockUpdated) {
+ detachWallpaperLocked(mLastLockWallpaper);
+ }
+ }
+
private void detachWallpaperLocked(WallpaperData wallpaper) {
if (wallpaper.connection != null) {
if (wallpaper.connection.mReply != null) {
@@ -3150,7 +3167,12 @@
wallpaper.connection.mTryToRebindRunnable);
wallpaper.connection = null;
- if (wallpaper == mLastWallpaper) mLastWallpaper = null;
+ if (wallpaper == mLastWallpaper) {
+ mLastWallpaper = null;
+ }
+ if (wallpaper == mLastLockWallpaper) {
+ mLastLockWallpaper = null;
+ }
}
}
@@ -3624,8 +3646,8 @@
final int id = parser.getAttributeInt(null, "id", -1);
if (id != -1) {
wallpaper.wallpaperId = id;
- if (id > mWallpaperId) {
- mWallpaperId = id;
+ if (id > WallpaperUtils.getCurrentWallpaperId()) {
+ WallpaperUtils.setCurrentWallpaperId(id);
}
} else {
wallpaper.wallpaperId = makeWallpaperIdLocked();
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperUtils.java b/services/core/java/com/android/server/wallpaper/WallpaperUtils.java
index a9b8092..d0311e3 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperUtils.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperUtils.java
@@ -19,10 +19,68 @@
import android.os.Environment;
import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
class WallpaperUtils {
+ static final String WALLPAPER = "wallpaper_orig";
+ static final String WALLPAPER_CROP = "wallpaper";
+ static final String WALLPAPER_LOCK_ORIG = "wallpaper_lock_orig";
+ static final String WALLPAPER_LOCK_CROP = "wallpaper_lock";
+ static final String WALLPAPER_INFO = "wallpaper_info.xml";
+ static final String RECORD_FILE = "decode_record";
+ static final String RECORD_LOCK_FILE = "decode_lock_record";
+
+ // All the various per-user state files we need to be aware of
+ private static final String[] sPerUserFiles = new String[] {
+ WALLPAPER, WALLPAPER_CROP,
+ WALLPAPER_LOCK_ORIG, WALLPAPER_LOCK_CROP,
+ WALLPAPER_INFO
+ };
+
+ /**
+ * ID of the current wallpaper, incremented every time anything sets a wallpaper.
+ * This is used for external detection of wallpaper update activity.
+ */
+ private static int sWallpaperId;
+
static File getWallpaperDir(int userId) {
return Environment.getUserSystemDirectory(userId);
}
+
+ /**
+ * generate a new wallpaper id
+ * should be called with the {@link WallpaperManagerService} lock held
+ */
+ static int makeWallpaperIdLocked() {
+ do {
+ ++sWallpaperId;
+ } while (sWallpaperId == 0);
+ return sWallpaperId;
+ }
+
+ /**
+ * returns the id of the current wallpaper (the last one that has been set)
+ */
+ static int getCurrentWallpaperId() {
+ return sWallpaperId;
+ }
+
+ /**
+ * sets the id of the current wallpaper
+ * used when a wallpaper with higher id than current is loaded from settings
+ */
+ static void setCurrentWallpaperId(int id) {
+ sWallpaperId = id;
+ }
+
+ static List<File> getWallpaperFiles(int userId) {
+ File wallpaperDir = getWallpaperDir(userId);
+ List<File> result = new ArrayList<File>();
+ for (int i = 0; i < sPerUserFiles.length; i++) {
+ result.add(new File(wallpaperDir, sPerUserFiles[i]));
+ }
+ return result;
+ }
}
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index e2ab216..65127e4 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -891,8 +891,7 @@
// to show the border. We will do so when the pending message is handled.
if (!mHandler.hasMessages(
MyHandler.MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED)) {
- setMagnifiedRegionBorderShown(
- isMagnifying() || isForceShowingMagnifiableBounds(), true);
+ setMagnifiedRegionBorderShown(isForceShowingMagnifiableBounds(), true);
}
}
@@ -1057,7 +1056,7 @@
// rotation or folding/unfolding the device. In the rotation case, the screenshot
// used for rotation already has the border. After the rotation is complete
// we will show the border.
- if (isMagnifying() || isForceShowingMagnifiableBounds()) {
+ if (isForceShowingMagnifiableBounds()) {
setMagnifiedRegionBorderShown(false, false);
final long delay = (long) (mLongAnimationDuration
* mService.getWindowAnimationScaleLocked());
@@ -1398,8 +1397,7 @@
case MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED : {
synchronized (mService.mGlobalLock) {
- if (mMagnifedViewport.isMagnifying()
- || isForceShowingMagnifiableBounds()) {
+ if (isForceShowingMagnifiableBounds()) {
mMagnifedViewport.setMagnifiedRegionBorderShown(true, true);
mService.scheduleAnimationLocked();
}
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 4428be7..d4895ed 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -46,6 +46,7 @@
import static com.android.server.wm.ActivityRecord.State.PAUSING;
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.STOPPING;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ALL;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
@@ -240,11 +241,21 @@
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "activityStopped");
r = ActivityRecord.isInRootTaskLocked(token);
if (r != null) {
+ if (!r.isState(STOPPING, RESTARTING_PROCESS)
+ && mTaskSupervisor.hasScheduledRestartTimeouts(r)) {
+ // Recover the restarting state which was replaced by other lifecycle changes.
+ r.setState(RESTARTING_PROCESS, "continue-restart");
+ }
if (r.attachedToProcess() && r.isState(RESTARTING_PROCESS)) {
// The activity was requested to restart from
// {@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 45ae3d8..6abd3d7 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -9357,6 +9357,15 @@
configChangeFlags = 0;
return;
}
+ if (!preserveWindow) {
+ // If the activity is the IME input target, ensure storing the last IME shown state
+ // before relaunching it for restoring the IME visibility once its new window focused.
+ final InputTarget imeInputTarget = mDisplayContent.getImeInputTarget();
+ mLastImeShown = imeInputTarget != null && imeInputTarget.getWindowState() != null
+ && imeInputTarget.getWindowState().mActivityRecord == this
+ && mDisplayContent.mInputMethodWindow != null
+ && mDisplayContent.mInputMethodWindow.isVisible();
+ }
// Do not waiting for translucent activity if it is going to relaunch.
final Task rootTask = getRootTask();
if (rootTask != null && rootTask.mTranslucentActivityWaiting == this) {
diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
index b40aa3c..1944b3f 100644
--- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
+++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
@@ -284,7 +284,7 @@
IntentSender target = createIntentSenderForOriginalIntent(mCallingUid,
FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT);
- mIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(mUserId, target);
+ mIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(mUserId, target, mRInfo);
mCallingPid = mRealCallingPid;
mCallingUid = mRealCallingUid;
mResolvedType = null;
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 407ffd0..919bab8 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -2297,6 +2297,10 @@
mHandler.sendEmptyMessageDelayed(SLEEP_TIMEOUT_MSG, SLEEP_TIMEOUT);
}
+ boolean hasScheduledRestartTimeouts(ActivityRecord r) {
+ return mHandler.hasMessages(RESTART_ACTIVITY_PROCESS_TIMEOUT_MSG, r);
+ }
+
void removeRestartTimeouts(ActivityRecord r) {
mHandler.removeMessages(RESTART_ACTIVITY_PROCESS_TIMEOUT_MSG, r);
}
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index d395f12..db88f0f 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -36,7 +36,6 @@
import android.animation.ArgbEvaluator;
import android.content.Context;
import android.graphics.Color;
-import android.graphics.GraphicBuffer;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
@@ -251,9 +250,6 @@
screenshotBuffer.getColorSpace());
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- GraphicBuffer buffer = GraphicBuffer.createFromHardwareBuffer(
- screenshotBuffer.getHardwareBuffer());
-
t.setLayer(mScreenshotLayer, SCREEN_FREEZE_LAYER_BASE);
t.reparent(mBackColorSurface, displayContent.getSurfaceControl());
// If hdr layers are on-screen, e.g. picture-in-picture mode, the screenshot of
@@ -263,10 +259,11 @@
t.setLayer(mBackColorSurface, -1);
t.setColor(mBackColorSurface, new float[]{mStartLuma, mStartLuma, mStartLuma});
t.setAlpha(mBackColorSurface, 1);
- t.setBuffer(mScreenshotLayer, buffer);
+ t.setBuffer(mScreenshotLayer, hardwareBuffer);
t.setColorSpace(mScreenshotLayer, screenshotBuffer.getColorSpace());
t.show(mScreenshotLayer);
t.show(mBackColorSurface);
+ hardwareBuffer.close();
if (mRoundedCornerOverlay != null) {
for (SurfaceControl sc : mRoundedCornerOverlay) {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 5e081d5..e538584 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2406,7 +2406,7 @@
if (isDisplayRotation) {
// This isn't cheap, so only do it for display rotations.
changeInfo.mSnapshotLuma = TransitionAnimation.getBorderLuma(
- screenshotBuffer.getHardwareBuffer(), screenshotBuffer.getColorSpace());
+ buffer, screenshotBuffer.getColorSpace());
}
SurfaceControl.Transaction t = wc.mWmService.mTransactionFactory.get();
@@ -2418,6 +2418,7 @@
t.setLayer(snapshotSurface, Integer.MAX_VALUE);
t.apply();
t.close();
+ buffer.close();
// Detach the screenshot on the sync transaction (the screenshot is just meant to
// freeze the window until the sync transaction is applied (with all its other
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 4e7613b..0243a98 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9177,6 +9177,7 @@
boolean shouldRestoreImeVisibility(IBinder imeTargetWindowToken) {
final Task imeTargetWindowTask;
+ boolean hadRequestedShowIme = false;
synchronized (mGlobalLock) {
final WindowState imeTargetWindow = mWindowMap.get(imeTargetWindowToken);
if (imeTargetWindow == null) {
@@ -9186,11 +9187,14 @@
if (imeTargetWindowTask == null) {
return false;
}
+ if (imeTargetWindow.mActivityRecord != null) {
+ hadRequestedShowIme = imeTargetWindow.mActivityRecord.mLastImeShown;
+ }
}
final TaskSnapshot snapshot = getTaskSnapshot(imeTargetWindowTask.mTaskId,
imeTargetWindowTask.mUserId, false /* isLowResolution */,
false /* restoreFromDisk */);
- return snapshot != null && snapshot.hasImeSurface();
+ return snapshot != null && snapshot.hasImeSurface() || hadRequestedShowIme;
}
@Override
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index dd70fca..0a5e0b7 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -21,6 +21,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS;
import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT;
@@ -1167,34 +1168,50 @@
}
case OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: {
final IBinder secondaryFragmentToken = operation.getSecondaryFragmentToken();
- final TaskFragment secondaryTaskFragment = secondaryFragmentToken != null
- ? mLaunchTaskFragments.get(secondaryFragmentToken)
- : null;
- taskFragment.setAdjacentTaskFragment(secondaryTaskFragment);
- effects |= TRANSACT_EFFECTS_LIFECYCLE;
-
- // Clear the focused app if the focused app is no longer visible after reset the
- // adjacent TaskFragments.
- if (secondaryTaskFragment == null
- && taskFragment.getDisplayContent().mFocusedApp != null
- && taskFragment.hasChild(taskFragment.getDisplayContent().mFocusedApp)
- && !taskFragment.shouldBeVisible(null /* starting */)) {
- taskFragment.getDisplayContent().setFocusedApp(null);
+ final TaskFragment secondaryTaskFragment =
+ mLaunchTaskFragments.get(secondaryFragmentToken);
+ if (secondaryTaskFragment == null) {
+ final Throwable exception = new IllegalArgumentException(
+ "SecondaryFragmentToken must be set for setAdjacentTaskFragments.");
+ sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment,
+ opType, exception);
+ break;
+ }
+ if (taskFragment.getAdjacentTaskFragment() != secondaryTaskFragment) {
+ // Only have lifecycle effect if the adjacent changed.
+ taskFragment.setAdjacentTaskFragment(secondaryTaskFragment);
+ effects |= TRANSACT_EFFECTS_LIFECYCLE;
}
final Bundle bundle = hop.getLaunchOptions();
final WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams =
- bundle != null ? new WindowContainerTransaction.TaskFragmentAdjacentParams(
- bundle) : null;
- if (adjacentParams == null) {
+ bundle != null
+ ? new WindowContainerTransaction.TaskFragmentAdjacentParams(bundle)
+ : null;
+ taskFragment.setDelayLastActivityRemoval(adjacentParams != null
+ && adjacentParams.shouldDelayPrimaryLastActivityRemoval());
+ secondaryTaskFragment.setDelayLastActivityRemoval(adjacentParams != null
+ && adjacentParams.shouldDelaySecondaryLastActivityRemoval());
+ break;
+ }
+ case OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS: {
+ final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
+ if (adjacentTaskFragment == null) {
break;
}
+ taskFragment.resetAdjacentTaskFragment();
+ effects |= TRANSACT_EFFECTS_LIFECYCLE;
- taskFragment.setDelayLastActivityRemoval(
- adjacentParams.shouldDelayPrimaryLastActivityRemoval());
- if (secondaryTaskFragment != null) {
- secondaryTaskFragment.setDelayLastActivityRemoval(
- adjacentParams.shouldDelaySecondaryLastActivityRemoval());
+ // Clear the focused app if the focused app is no longer visible after reset the
+ // adjacent TaskFragments.
+ final ActivityRecord focusedApp = taskFragment.getDisplayContent().mFocusedApp;
+ final TaskFragment focusedTaskFragment = focusedApp != null
+ ? focusedApp.getTaskFragment()
+ : null;
+ if ((focusedTaskFragment == taskFragment
+ || focusedTaskFragment == adjacentTaskFragment)
+ && !focusedTaskFragment.shouldBeVisible(null /* starting */)) {
+ focusedTaskFragment.getDisplayContent().setFocusedApp(null /* newFocus */);
}
break;
}
@@ -1528,6 +1545,9 @@
throw new IllegalArgumentException("setAdjacentRootsHierarchyOp: Not created by"
+ " organizer root1=" + root1 + " root2=" + root2);
}
+ if (root1.getAdjacentTaskFragment() == root2) {
+ return TRANSACT_EFFECTS_NONE;
+ }
root1.setAdjacentTaskFragment(root2);
return TRANSACT_EFFECTS_LIFECYCLE;
}
@@ -1538,7 +1558,9 @@
throw new IllegalArgumentException("clearAdjacentRootsHierarchyOp: Not created by"
+ " organizer root=" + root);
}
-
+ if (root.getAdjacentTaskFragment() == null) {
+ return TRANSACT_EFFECTS_NONE;
+ }
root.resetAdjacentTaskFragment();
return TRANSACT_EFFECTS_LIFECYCLE;
}
diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
index 595d03d..be60946 100644
--- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
@@ -39,15 +39,17 @@
implements ProviderSession.ProviderInternalCallback<Void> {
private static final String TAG = "GetRequestSession";
- public ClearRequestSession(Context context, int userId,
+ public ClearRequestSession(Context context, int userId, int callingUid,
IClearCredentialStateCallback callback, ClearCredentialStateRequest request,
CallingAppInfo callingAppInfo) {
- super(context, userId, request, callback, RequestInfo.TYPE_UNDEFINED, callingAppInfo);
+ super(context, userId, callingUid, request, callback, RequestInfo.TYPE_UNDEFINED,
+ callingAppInfo);
}
/**
* Creates a new provider session, and adds it list of providers that are contributing to
* this session.
+ *
* @return the provider session created within this request session, for the given provider
* info.
*/
@@ -111,8 +113,10 @@
Log.i(TAG, "respondToClientWithResponseAndFinish");
try {
mClientCallback.onSuccess();
+ logApiCalled(RequestType.CLEAR_CREDENTIALS, /* isSuccessful */ true);
} catch (RemoteException e) {
Log.i(TAG, "Issue while propagating the response to the client");
+ logApiCalled(RequestType.CLEAR_CREDENTIALS, /* isSuccessful */ false);
}
finishSession();
}
@@ -124,10 +128,12 @@
} catch (RemoteException e) {
e.printStackTrace();
}
+ logApiCalled(RequestType.CLEAR_CREDENTIALS, /* isSuccessful */ false);
finishSession();
}
+
private void processResponses() {
- for (ProviderSession session: mProviders.values()) {
+ for (ProviderSession session : mProviders.values()) {
if (session.isProviderResponseSet()) {
// If even one provider responded successfully, send back the response
// TODO: Aggregate other exceptions
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
index 82c2358..acfa491 100644
--- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -44,11 +44,12 @@
implements ProviderSession.ProviderInternalCallback<CreateCredentialResponse> {
private static final String TAG = "CreateRequestSession";
- CreateRequestSession(@NonNull Context context, int userId,
+ CreateRequestSession(@NonNull Context context, int userId, int callingUid,
CreateCredentialRequest request,
ICreateCredentialCallback callback,
CallingAppInfo callingAppInfo) {
- super(context, userId, request, callback, RequestInfo.TYPE_CREATE, callingAppInfo);
+ super(context, userId, callingUid, request, callback, RequestInfo.TYPE_CREATE,
+ callingAppInfo);
}
/**
@@ -63,7 +64,7 @@
RemoteCredentialService remoteCredentialService) {
ProviderCreateSession providerCreateSession = ProviderCreateSession
.createNewSession(mContext, mUserId, providerInfo,
- this, remoteCredentialService);
+ this, remoteCredentialService);
if (providerCreateSession != null) {
Log.i(TAG, "In startProviderSession - provider session created and being added");
mProviders.put(providerCreateSession.getComponentName().flattenToString(),
@@ -81,8 +82,9 @@
mClientAppInfo.getPackageName()),
providerDataList));
} catch (RemoteException e) {
- Log.i(TAG, "Issue with invoking pending intent: " + e.getMessage());
- // TODO: Propagate failure
+ respondToClientWithErrorAndFinish(
+ CreateCredentialException.TYPE_UNKNOWN,
+ "Unable to invoke selector");
}
}
@@ -106,8 +108,7 @@
@Override
public void onUiCancellation() {
- // TODO("Replace with properly defined error type")
- respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_NO_CREDENTIAL,
+ respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_USER_CANCELED,
"User cancelled the selector");
}
@@ -115,8 +116,10 @@
Log.i(TAG, "respondToClientWithResponseAndFinish");
try {
mClientCallback.onResponse(response);
+ logApiCalled(RequestType.CREATE_CREDENTIALS, /* isSuccessful */ true);
} catch (RemoteException e) {
Log.i(TAG, "Issue while responding to client: " + e.getMessage());
+ logApiCalled(RequestType.CREATE_CREDENTIALS, /* isSuccessful */ false);
}
finishSession();
}
@@ -128,6 +131,7 @@
} catch (RemoteException e) {
Log.i(TAG, "Issue while responding to client: " + e.getMessage());
}
+ logApiCalled(RequestType.CREATE_CREDENTIALS, /* isSuccessful */ false);
finishSession();
}
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index aefd300..f76cf49 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -269,11 +269,13 @@
ICancellationSignal cancelTransport = CancellationSignal.createTransport();
int userId = UserHandle.getCallingUserId();
+ int callingUid = Binder.getCallingUid();
// New request session, scoped for this request only.
final GetRequestSession session =
new GetRequestSession(
getContext(),
userId,
+ callingUid,
callback,
request,
constructCallingAppInfo(callingPackage, userId));
@@ -319,10 +321,12 @@
// New request session, scoped for this request only.
int userId = UserHandle.getCallingUserId();
+ int callingUid = Binder.getCallingUid();
final CreateRequestSession session =
new CreateRequestSession(
getContext(),
userId,
+ callingUid,
request,
callback,
constructCallingAppInfo(callingPackage, userId));
@@ -434,10 +438,12 @@
// New request session, scoped for this request only.
int userId = UserHandle.getCallingUserId();
+ int callingUid = Binder.getCallingUid();
final ClearRequestSession session =
new ClearRequestSession(
getContext(),
userId,
+ callingUid,
callback,
request,
constructCallingAppInfo(callingPackage, userId));
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index c7fa72c..f7c5905 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -41,10 +41,10 @@
implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> {
private static final String TAG = "GetRequestSession";
- public GetRequestSession(Context context, int userId,
+ public GetRequestSession(Context context, int userId, int callingUid,
IGetCredentialCallback callback, GetCredentialRequest request,
CallingAppInfo callingAppInfo) {
- super(context, userId, request, callback, RequestInfo.TYPE_GET, callingAppInfo);
+ super(context, userId, callingUid, request, callback, RequestInfo.TYPE_GET, callingAppInfo);
}
/**
@@ -76,8 +76,8 @@
mRequestId, mClientRequest, mClientAppInfo.getPackageName()),
providerDataList));
} catch (RemoteException e) {
- Log.i(TAG, "Issue with invoking pending intent: " + e.getMessage());
- // TODO: Propagate failure
+ respondToClientWithErrorAndFinish(
+ GetCredentialException.TYPE_UNKNOWN, "Unable to instantiate selector");
}
}
@@ -104,8 +104,10 @@
private void respondToClientWithResponseAndFinish(GetCredentialResponse response) {
try {
mClientCallback.onResponse(response);
+ logApiCalled(RequestType.GET_CREDENTIALS, /* isSuccessful */ true);
} catch (RemoteException e) {
Log.i(TAG, "Issue while responding to client with a response : " + e.getMessage());
+ logApiCalled(RequestType.GET_CREDENTIALS, /* isSuccessful */ false);
}
finishSession();
}
@@ -117,13 +119,13 @@
Log.i(TAG, "Issue while responding to client with error : " + e.getMessage());
}
+ logApiCalled(RequestType.GET_CREDENTIALS, /* isSuccessful */ false);
finishSession();
}
@Override
public void onUiCancellation() {
- // TODO("Replace with user cancelled error type when ready")
- respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL,
+ respondToClientWithErrorAndFinish(GetCredentialException.TYPE_USER_CANCELED,
"User cancelled the selector");
}
diff --git a/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java b/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java
index 8796314..c2b346f 100644
--- a/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java
+++ b/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java
@@ -39,6 +39,12 @@
return pendingIntentResponse.getResultCode() == Activity.RESULT_OK;
}
+ /** Returns true if the pending intent was cancelled by the user. */
+ public static boolean isCancelledResponse(
+ ProviderPendingIntentResponse pendingIntentResponse) {
+ return pendingIntentResponse.getResultCode() == Activity.RESULT_CANCELED;
+ }
+
/** Extracts the {@link CredentialsResponseContent} object added to the result data. */
public static CredentialsResponseContent extractResponseContent(Intent resultData) {
if (resultData == null) {
diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
index 27eaa0b..7a24a22 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
@@ -263,9 +263,9 @@
Log.i(TAG, "Pending intent contains provider exception");
return exception;
}
+ } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) {
+ return new CreateCredentialException(CreateCredentialException.TYPE_USER_CANCELED);
} else {
- Log.i(TAG, "Pending intent result code not Activity.RESULT_OK");
- // TODO("Update with unknown exception when ready")
return new CreateCredentialException(CreateCredentialException.TYPE_NO_CREDENTIAL);
}
return null;
@@ -273,12 +273,11 @@
/**
* When an invalid state occurs, e.g. entry mismatch or no response from provider,
- * we send back a TYPE_NO_CREDENTIAL error as to the developer, it is the same as not
- * getting any credentials back.
+ * we send back a TYPE_UNKNOWN error as to the developer.
*/
private void invokeCallbackOnInternalInvalidState() {
mCallbacks.onFinalErrorReceived(mComponentName,
- CreateCredentialException.TYPE_NO_CREDENTIAL,
+ CreateCredentialException.TYPE_UNKNOWN,
null);
}
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index de93af4..95f2313 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -375,6 +375,11 @@
private void onAuthenticationEntrySelected(
@Nullable ProviderPendingIntentResponse providerPendingIntentResponse) {
//TODO: Other provider intent statuses
+ if (providerPendingIntentResponse == null) {
+ Log.i(TAG, "providerPendingIntentResponse is null");
+ onUpdateEmptyResponse();
+ }
+
GetCredentialException exception = maybeGetPendingIntentException(
providerPendingIntentResponse);
if (exception != null) {
@@ -393,7 +398,7 @@
}
Log.i(TAG, "No error or respond found in pending intent response");
- invokeCallbackOnInternalInvalidState();
+ onUpdateEmptyResponse();
}
private void onActionEntrySelected(ProviderPendingIntentResponse
@@ -415,12 +420,16 @@
}
}
+ private void onUpdateEmptyResponse() {
+ updateStatusAndInvokeCallback(Status.NO_CREDENTIALS);
+ }
+
@Nullable
private GetCredentialException maybeGetPendingIntentException(
ProviderPendingIntentResponse pendingIntentResponse) {
if (pendingIntentResponse == null) {
Log.i(TAG, "pendingIntentResponse is null");
- return new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL);
+ return null;
}
if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) {
GetCredentialException exception = PendingIntentResultHandler
@@ -429,8 +438,9 @@
Log.i(TAG, "Pending intent contains provider exception");
return exception;
}
+ } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) {
+ return new GetCredentialException(GetCredentialException.TYPE_USER_CANCELED);
} else {
- Log.i(TAG, "Pending intent result code not Activity.RESULT_OK");
return new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL);
}
return null;
@@ -438,12 +448,10 @@
/**
* When an invalid state occurs, e.g. entry mismatch or no response from provider,
- * we send back a TYPE_NO_CREDENTIAL error as to the developer, it is the same as not
- * getting any credentials back.
+ * we send back a TYPE_UNKNOWN error as to the developer.
*/
private void invokeCallbackOnInternalInvalidState() {
mCallbacks.onFinalErrorReceived(mComponentName,
- GetCredentialException.TYPE_NO_CREDENTIAL,
- null);
+ GetCredentialException.TYPE_UNKNOWN, null);
}
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java
index 7036dfb..678c752 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -133,7 +133,7 @@
PENDING_INTENT_INVOKED,
CREDENTIAL_RECEIVED_FROM_SELECTION,
SAVE_ENTRIES_RECEIVED, CANCELED,
- COMPLETE
+ NO_CREDENTIALS, COMPLETE
}
/** Converts exception to a provider session status. */
diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java
index 0c3c34e..8e44f0f 100644
--- a/services/credentials/java/com/android/server/credentials/RequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -16,6 +16,13 @@
package com.android.server.credentials;
+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_UNKNOWN;
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_STATUS__API_STATUS_FAILURE;
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_STATUS__API_STATUS_SUCCESS;
+
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.content.Context;
@@ -29,6 +36,8 @@
import android.service.credentials.CredentialProviderInfo;
import android.util.Log;
+import com.android.internal.util.FrameworkStatsLog;
+
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@@ -37,28 +46,53 @@
* Base class of a request session, that listens to UI events. This class must be extended
* every time a new response type is expected from the providers.
*/
-abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialManagerUiCallback{
+abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialManagerUiCallback {
private static final String TAG = "RequestSession";
+ // Metrics constants
+ private static final int METRICS_API_NAME_UNKNOWN =
+ CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_UNKNOWN;
+ private static final int METRICS_API_NAME_GET_CREDENTIAL =
+ CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_GET_CREDENTIAL;
+ private static final int METRICS_API_NAME_CREATE_CREDENTIAL =
+ CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_CREATE_CREDENTIAL;
+ private static final int METRICS_API_NAME_CLEAR_CREDENTIAL =
+ CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_CLEAR_CREDENTIAL;
+ private static final int METRICS_API_STATUS_SUCCESS =
+ CREDENTIAL_MANAGER_API_CALLED__API_STATUS__API_STATUS_SUCCESS;
+ private static final int METRICS_API_STATUS_FAILURE =
+ CREDENTIAL_MANAGER_API_CALLED__API_STATUS__API_STATUS_FAILURE;
+
// TODO: Revise access levels of attributes
- @NonNull protected final T mClientRequest;
- @NonNull protected final U mClientCallback;
- @NonNull protected final IBinder mRequestId;
- @NonNull protected final Context mContext;
- @NonNull protected final CredentialManagerUi mCredentialManagerUi;
- @NonNull protected final String mRequestType;
- @NonNull protected final Handler mHandler;
- @UserIdInt protected final int mUserId;
- @NonNull protected final CallingAppInfo mClientAppInfo;
+ @NonNull
+ protected final T mClientRequest;
+ @NonNull
+ protected final U mClientCallback;
+ @NonNull
+ protected final IBinder mRequestId;
+ @NonNull
+ protected final Context mContext;
+ @NonNull
+ protected final CredentialManagerUi mCredentialManagerUi;
+ @NonNull
+ protected final String mRequestType;
+ @NonNull
+ protected final Handler mHandler;
+ @UserIdInt
+ protected final int mUserId;
+ private final int mCallingUid;
+ @NonNull
+ protected final CallingAppInfo mClientAppInfo;
protected final Map<String, ProviderSession> mProviders = new HashMap<>();
protected RequestSession(@NonNull Context context,
- @UserIdInt int userId, @NonNull T clientRequest, U clientCallback,
+ @UserIdInt int userId, int callingUid, @NonNull T clientRequest, U clientCallback,
@NonNull String requestType,
CallingAppInfo callingAppInfo) {
mContext = context;
mUserId = userId;
+ mCallingUid = callingUid;
mClientRequest = clientRequest;
mClientCallback = clientCallback;
mRequestType = requestType;
@@ -117,6 +151,33 @@
return false;
}
+ // TODO: move these definitions to a separate logging focused class.
+ enum RequestType {
+ GET_CREDENTIALS,
+ CREATE_CREDENTIALS,
+ CLEAR_CREDENTIALS,
+ }
+
+ private static int getApiNameFromRequestType(RequestType requestType) {
+ switch (requestType) {
+ case GET_CREDENTIALS:
+ return METRICS_API_NAME_GET_CREDENTIAL;
+ case CREATE_CREDENTIALS:
+ return METRICS_API_NAME_CREATE_CREDENTIAL;
+ case CLEAR_CREDENTIALS:
+ return METRICS_API_NAME_CLEAR_CREDENTIAL;
+ default:
+ return METRICS_API_NAME_UNKNOWN;
+ }
+ }
+
+ protected void logApiCalled(RequestType requestType, boolean isSuccessful) {
+ FrameworkStatsLog.write(FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED,
+ /* api_name */getApiNameFromRequestType(requestType), /* caller_uid */
+ mCallingUid, /* api_status */
+ isSuccessful ? METRICS_API_STATUS_SUCCESS : METRICS_API_STATUS_FAILURE);
+ }
+
/**
* Returns true if at least one provider is ready for UI invocation, and no
* provider is pending a response.
diff --git a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
index f549797..e416718 100644
--- a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
@@ -211,6 +211,12 @@
}
}
+ internal fun onSystemReady() {
+ mutateState {
+ with(policy) { onSystemReady() }
+ }
+ }
+
private val PackageManagerLocal.allPackageStates:
Pair<Map<String, PackageState>, Map<String, PackageState>>
get() = withUnfilteredSnapshot().use { it.packageStates to it.disabledSystemPackageStates }
diff --git a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
index e0f94c7..07a5e72 100644
--- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
@@ -255,6 +255,13 @@
}
}
+ fun MutateStateScope.onSystemReady() {
+ newState.systemState.isSystemReady = true
+ forEachSchemePolicy {
+ with(it) { onSystemReady() }
+ }
+ }
+
fun BinaryXmlPullParser.parseSystemState(state: AccessState) {
forEachTag {
when (tagName) {
@@ -362,6 +369,8 @@
open fun MutateStateScope.onPackageUninstalled(packageName: String, appId: Int, userId: Int) {}
+ open fun MutateStateScope.onSystemReady() {}
+
open fun BinaryXmlPullParser.parseSystemState(state: AccessState) {}
open fun BinaryXmlSerializer.serializeSystemState(state: AccessState) {}
diff --git a/services/permission/java/com/android/server/permission/access/AccessState.kt b/services/permission/java/com/android/server/permission/access/AccessState.kt
index 9616193..5532311 100644
--- a/services/permission/java/com/android/server/permission/access/AccessState.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessState.kt
@@ -50,6 +50,8 @@
var privilegedPermissionAllowlistPackages: IndexedListSet<String>,
var permissionAllowlist: PermissionAllowlist,
var implicitToSourcePermissions: IndexedMap<String, IndexedListSet<String>>,
+ var isSystemReady: Boolean,
+ // TODO: Get and watch the state for deviceAndProfileOwners
// Mapping from user ID to package name.
var deviceAndProfileOwners: IntMap<String>,
val permissionGroups: IndexedMap<String, PermissionGroupInfo>,
@@ -67,6 +69,7 @@
IndexedListSet(),
PermissionAllowlist(),
IndexedMap(),
+ false,
IntMap(),
IndexedMap(),
IndexedMap(),
@@ -85,6 +88,7 @@
privilegedPermissionAllowlistPackages,
permissionAllowlist,
implicitToSourcePermissions,
+ isSystemReady,
deviceAndProfileOwners,
permissionGroups.copy { it },
permissionTrees.copy { it },
diff --git a/services/permission/java/com/android/server/permission/access/permission/Permission.kt b/services/permission/java/com/android/server/permission/access/permission/Permission.kt
index 7bfca12..714480c 100644
--- a/services/permission/java/com/android/server/permission/access/permission/Permission.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/Permission.kt
@@ -91,6 +91,9 @@
inline val isKnownSigner: Boolean
get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_KNOWN_SIGNER)
+ inline val isModule: Boolean
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_MODULE)
+
inline val isOem: Boolean
get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_OEM)
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index 903fad3..c7e9371 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -1747,7 +1747,7 @@
override fun writeLegacyPermissionStateTEMP() {}
override fun onSystemReady() {
- // TODO STOPSHIP privappPermissionsViolationsfix check
+ service.onSystemReady()
permissionControllerManager = PermissionControllerManager(
context, PermissionThread.getHandler()
)
diff --git a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
index d0833bd..694efbb 100644
--- a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
@@ -54,6 +54,8 @@
IndexedListSet<OnPermissionFlagsChangedListener>()
private val onPermissionFlagsChangedListenersLock = Any()
+ private val privilegedPermissionAllowlistViolations = IndexedSet<String>()
+
override val subjectScheme: String
get() = UidUri.SCHEME
@@ -734,7 +736,7 @@
} else {
newFlags = newFlags andInv PermissionFlags.LEGACY_GRANTED
val wasGrantedByImplicit = newFlags.hasBits(PermissionFlags.IMPLICIT_GRANTED)
- val isLeanBackNotificationsPermission = newState.systemState.isLeanback &&
+ val isLeanbackNotificationsPermission = newState.systemState.isLeanback &&
permissionName in NOTIFICATIONS_PERMISSIONS
val isImplicitPermission = anyPackageInAppId(appId) {
permissionName in it.androidPackage!!.implicitPermissions
@@ -748,7 +750,7 @@
}
!sourcePermission.isRuntime
} ?: false
- val shouldGrantByImplicit = isLeanBackNotificationsPermission ||
+ val shouldGrantByImplicit = isLeanbackNotificationsPermission ||
(isImplicitPermission && isAnySourcePermissionNonRuntime)
if (shouldGrantByImplicit) {
newFlags = newFlags or PermissionFlags.IMPLICIT_GRANTED
@@ -917,7 +919,21 @@
if (packageState.isUpdatedSystemApp) {
return true
}
- // TODO: Enforce the allowlist on boot
+ // Only enforce the privileged permission allowlist on boot
+ if (!newState.systemState.isSystemReady) {
+ // Apps that are in updated apex's do not need to be allowlisted
+ if (!packageState.isApkInUpdatedApex) {
+ Log.w(
+ LOG_TAG, "Privileged permission ${permission.name} for package" +
+ " ${packageState.packageName} (${packageState.path}) not in" +
+ " privileged permission allowlist"
+ )
+ if (RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_ENFORCE) {
+ privilegedPermissionAllowlistViolations += "${packageState.packageName}" +
+ " (${packageState.path}): ${permission.name}"
+ }
+ }
+ }
return !RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_ENFORCE
}
@@ -1106,6 +1122,12 @@
// Special permission for the recents app.
return true
}
+ // TODO(b/261913353): STOPSHIP: Add AndroidPackage.apexModuleName.
+ // This should be androidPackage.apexModuleName instead
+ if (permission.isModule && androidPackage.packageName != null) {
+ // Special permission granted for APKs inside APEX modules.
+ return true
+ }
return false
}
@@ -1155,6 +1177,13 @@
return uid == ownerUid
}
+ override fun MutateStateScope.onSystemReady() {
+ if (!privilegedPermissionAllowlistViolations.isEmpty()) {
+ throw IllegalStateException("Signature|privileged permissions not in privileged" +
+ " permission allowlist: $privilegedPermissionAllowlistViolations")
+ }
+ }
+
override fun BinaryXmlPullParser.parseSystemState(state: AccessState) {
with(persistence) { this@parseSystemState.parseSystemState(state) }
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
new file mode 100644
index 0000000..73d04c6
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 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 com.android.server.inputmethod;
+
+import static android.inputmethodservice.InputMethodService.IME_ACTIVE;
+
+import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_SOFT_INPUT;
+import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_SOFT_INPUT;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_NOT_ALWAYS;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_INVALID;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME_IMPLICIT;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.os.RemoteException;
+import android.view.inputmethod.InputMethodManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test the behavior of {@link DefaultImeVisibilityApplier} when performing or applying the IME
+ * visibility state.
+ *
+ * Build/Install/Run:
+ * atest FrameworksInputMethodSystemServerTests:DefaultImeVisibilityApplierTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTestBase {
+ private DefaultImeVisibilityApplier mVisibilityApplier;
+
+ @Before
+ public void setUp() throws RemoteException {
+ super.setUp();
+ mVisibilityApplier =
+ (DefaultImeVisibilityApplier) mInputMethodManagerService.getVisibilityApplier();
+ mInputMethodManagerService.mCurFocusedWindowClient = mock(
+ InputMethodManagerService.ClientState.class);
+ }
+
+ @Test
+ public void testPerformShowIme() throws Exception {
+ mVisibilityApplier.performShowIme(mWindowToken, null, null, SHOW_SOFT_INPUT);
+ verifyShowSoftInput(false, true, InputMethodManager.SHOW_IMPLICIT);
+ }
+
+ @Test
+ public void testPerformHideIme() throws Exception {
+ mVisibilityApplier.performHideIme(mWindowToken, null, null, HIDE_SOFT_INPUT);
+ verifyHideSoftInput(false, true);
+ }
+
+ @Test
+ public void testApplyImeVisibility_throwForInvalidState() {
+ mVisibilityApplier.applyImeVisibility(mWindowToken, null, STATE_INVALID);
+ assertThrows(IllegalArgumentException.class,
+ () -> mVisibilityApplier.applyImeVisibility(mWindowToken, null, STATE_INVALID));
+ }
+
+ @Test
+ public void testApplyImeVisibility_showIme() {
+ mVisibilityApplier.applyImeVisibility(mWindowToken, null, STATE_SHOW_IME);
+ verify(mMockWindowManagerInternal).showImePostLayout(eq(mWindowToken), any());
+ }
+
+ @Test
+ public void testApplyImeVisibility_hideIme() {
+ mVisibilityApplier.applyImeVisibility(mWindowToken, null, STATE_HIDE_IME);
+ verify(mMockWindowManagerInternal).hideIme(eq(mWindowToken), anyInt(), any());
+ }
+
+ @Test
+ public void testApplyImeVisibility_hideImeExplicit() throws Exception {
+ mInputMethodManagerService.mImeWindowVis = IME_ACTIVE;
+ mVisibilityApplier.applyImeVisibility(mWindowToken, null, STATE_HIDE_IME_EXPLICIT);
+ verifyHideSoftInput(true, true);
+ }
+
+ @Test
+ public void testApplyImeVisibility_hideNotAlways() throws Exception {
+ mInputMethodManagerService.mImeWindowVis = IME_ACTIVE;
+ mVisibilityApplier.applyImeVisibility(mWindowToken, null, STATE_HIDE_IME_NOT_ALWAYS);
+ verifyHideSoftInput(true, true);
+ }
+
+ @Test
+ public void testApplyImeVisibility_showImeImplicit() throws Exception {
+ mVisibilityApplier.applyImeVisibility(mWindowToken, null, STATE_SHOW_IME_IMPLICIT);
+ verifyShowSoftInput(true, true, InputMethodManager.SHOW_IMPLICIT);
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
new file mode 100644
index 0000000..8415fe1
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 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 com.android.server.inputmethod;
+
+import static android.accessibilityservice.AccessibilityService.SHOW_MODE_HIDDEN;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
+
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState;
+import static com.android.server.inputmethod.InputMethodManagerService.FALLBACK_DISPLAY_ID;
+import static com.android.server.inputmethod.InputMethodManagerService.ImeDisplayValidator;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.inputmethod.InputMethodManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.wm.WindowManagerInternal;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test the behavior of {@link ImeVisibilityStateComputer} and {@link ImeVisibilityApplier} when
+ * requesting the IME visibility.
+ *
+ * Build/Install/Run:
+ * atest FrameworksInputMethodSystemServerTests:ImeVisibilityStateComputerTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTestBase {
+ private ImeVisibilityStateComputer mComputer;
+ private int mImeDisplayPolicy = DISPLAY_IME_POLICY_LOCAL;
+
+ @Before
+ public void setUp() throws RemoteException {
+ super.setUp();
+ ImeVisibilityStateComputer.Injector injector = new ImeVisibilityStateComputer.Injector() {
+ @Override
+ public WindowManagerInternal getWmService() {
+ return mMockWindowManagerInternal;
+ }
+
+ @Override
+ public ImeDisplayValidator getImeValidator() {
+ return displayId -> mImeDisplayPolicy;
+ }
+ };
+ mComputer = new ImeVisibilityStateComputer(mInputMethodManagerService, injector);
+ }
+
+ @Test
+ public void testRequestImeVisibility_showImplicit() {
+ initImeTargetWindowState(mWindowToken);
+ boolean res = mComputer.onImeShowFlags(null, InputMethodManager.SHOW_IMPLICIT);
+ mComputer.requestImeVisibility(mWindowToken, res);
+
+ final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
+ assertThat(state).isNotNull();
+ assertThat(state.hasEdiorFocused()).isTrue();
+ assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED);
+ assertThat(state.isRequestedImeVisible()).isTrue();
+
+ assertThat(mComputer.mRequestedShowExplicitly).isFalse();
+ }
+
+ @Test
+ public void testRequestImeVisibility_showExplicit() {
+ initImeTargetWindowState(mWindowToken);
+ boolean res = mComputer.onImeShowFlags(null, 0 /* show explicit */);
+ mComputer.requestImeVisibility(mWindowToken, res);
+
+ final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
+ assertThat(state).isNotNull();
+ assertThat(state.hasEdiorFocused()).isTrue();
+ assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED);
+ assertThat(state.isRequestedImeVisible()).isTrue();
+
+ assertThat(mComputer.mRequestedShowExplicitly).isTrue();
+ }
+
+ @Test
+ public void testRequestImeVisibility_showImplicit_a11yNoImePolicy() {
+ // Precondition: set AccessibilityService#SHOW_MODE_HIDDEN policy
+ mComputer.getImePolicy().setA11yRequestNoSoftKeyboard(SHOW_MODE_HIDDEN);
+
+ initImeTargetWindowState(mWindowToken);
+ boolean res = mComputer.onImeShowFlags(null, InputMethodManager.SHOW_IMPLICIT);
+ mComputer.requestImeVisibility(mWindowToken, res);
+
+ final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
+ assertThat(state).isNotNull();
+ assertThat(state.hasEdiorFocused()).isTrue();
+ assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED);
+ assertThat(state.isRequestedImeVisible()).isFalse();
+
+ assertThat(mComputer.mRequestedShowExplicitly).isFalse();
+ }
+
+ @Test
+ public void testRequestImeVisibility_showImplicit_imeHiddenPolicy() {
+ // Precondition: set IME hidden display policy before calling showSoftInput
+ mComputer.getImePolicy().setImeHiddenByDisplayPolicy(true);
+
+ initImeTargetWindowState(mWindowToken);
+ boolean res = mComputer.onImeShowFlags(null, InputMethodManager.SHOW_IMPLICIT);
+ mComputer.requestImeVisibility(mWindowToken, res);
+
+ final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
+ assertThat(state).isNotNull();
+ assertThat(state.hasEdiorFocused()).isTrue();
+ assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED);
+ assertThat(state.isRequestedImeVisible()).isFalse();
+
+ assertThat(mComputer.mRequestedShowExplicitly).isFalse();
+ }
+
+ @Test
+ public void testRequestImeVisibility_hideNotAlways() {
+ // Precondition: ensure IME has shown before hiding request.
+ mComputer.setInputShown(true);
+
+ initImeTargetWindowState(mWindowToken);
+ assertThat(mComputer.canHideIme(null, InputMethodManager.HIDE_NOT_ALWAYS)).isTrue();
+ mComputer.requestImeVisibility(mWindowToken, false);
+
+ final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
+ assertThat(state).isNotNull();
+ assertThat(state.hasEdiorFocused()).isTrue();
+ assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED);
+ assertThat(state.isRequestedImeVisible()).isFalse();
+ }
+
+ @Test
+ public void testComputeImeDisplayId() {
+ final ImeTargetWindowState state = mComputer.getOrCreateWindowState(mWindowToken);
+
+ mImeDisplayPolicy = DISPLAY_IME_POLICY_LOCAL;
+ mComputer.computeImeDisplayId(state, DEFAULT_DISPLAY);
+ assertThat(mComputer.getImePolicy().isImeHiddenByDisplayPolicy()).isFalse();
+ assertThat(state.getImeDisplayId()).isEqualTo(DEFAULT_DISPLAY);
+
+ mComputer.computeImeDisplayId(state, 10 /* displayId */);
+ assertThat(mComputer.getImePolicy().isImeHiddenByDisplayPolicy()).isFalse();
+ assertThat(state.getImeDisplayId()).isEqualTo(10);
+
+ mImeDisplayPolicy = DISPLAY_IME_POLICY_HIDE;
+ mComputer.computeImeDisplayId(state, 10 /* displayId */);
+ assertThat(mComputer.getImePolicy().isImeHiddenByDisplayPolicy()).isTrue();
+ assertThat(state.getImeDisplayId()).isEqualTo(INVALID_DISPLAY);
+
+ mImeDisplayPolicy = DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
+ mComputer.computeImeDisplayId(state, 10 /* displayId */);
+ assertThat(mComputer.getImePolicy().isImeHiddenByDisplayPolicy()).isFalse();
+ assertThat(state.getImeDisplayId()).isEqualTo(FALLBACK_DISPLAY_ID);
+ }
+
+ private void initImeTargetWindowState(IBinder windowToken) {
+ final ImeTargetWindowState state = new ImeTargetWindowState(SOFT_INPUT_STATE_UNCHANGED,
+ 0, true, true, true);
+ mComputer.setWindowState(windowToken, state);
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index 640bde3..804bb49 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -26,6 +26,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -200,9 +201,8 @@
"TestServiceThread",
Process.THREAD_PRIORITY_FOREGROUND, /* allowIo */
false);
- mInputMethodManagerService =
- new InputMethodManagerService(
- mContext, mServiceThread, mMockInputMethodBindingController);
+ mInputMethodManagerService = new InputMethodManagerService(mContext, mServiceThread,
+ mMockInputMethodBindingController);
// Start a InputMethodManagerService.Lifecycle to publish and manage the lifecycle of
// InputMethodManagerService, which is closer to the real situation.
@@ -239,12 +239,17 @@
protected void verifyShowSoftInput(boolean setVisible, boolean showSoftInput)
throws RemoteException {
+ verifyShowSoftInput(setVisible, showSoftInput, anyInt());
+ }
+
+ protected void verifyShowSoftInput(boolean setVisible, boolean showSoftInput, int showFlags)
+ throws RemoteException {
synchronized (ImfLock.class) {
verify(mMockInputMethodBindingController, times(setVisible ? 1 : 0))
.setCurrentMethodVisible();
}
verify(mMockInputMethod, times(showSoftInput ? 1 : 0))
- .showSoftInput(any(), any(), anyInt(), any());
+ .showSoftInput(any(), any(), eq(showFlags), any());
}
protected void verifyHideSoftInput(boolean setNotVisible, boolean hideSoftInput)
diff --git a/services/tests/PackageManagerServiceTests/host/Android.bp b/services/tests/PackageManagerServiceTests/host/Android.bp
index 83677c2..47e7a37 100644
--- a/services/tests/PackageManagerServiceTests/host/Android.bp
+++ b/services/tests/PackageManagerServiceTests/host/Android.bp
@@ -30,11 +30,16 @@
"truth-prebuilt",
],
static_libs: [
+ "ApexInstallHelper",
"cts-host-utils",
"frameworks-base-hostutils",
"PackageManagerServiceHostTestsIntentVerifyUtils",
],
test_suites: ["general-tests"],
+ data: [
+ ":PackageManagerTestApex",
+ ":PackageManagerTestApexApp",
+ ],
java_resources: [
":PackageManagerTestOverlayActor",
":PackageManagerTestOverlay",
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/ApexUpdateTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/ApexUpdateTest.kt
new file mode 100644
index 0000000..44b4e30
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/ApexUpdateTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 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 com.android.server.pm.test
+
+import com.android.modules.testing.utils.ApexInstallHelper
+import com.android.tradefed.invoker.TestInformation
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
+import com.android.tradefed.testtype.junit4.BeforeClassWithInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DeviceJUnit4ClassRunner::class)
+class ApexUpdateTest : BaseHostJUnit4Test() {
+
+ companion object {
+ private const val APEX_NAME = "com.android.server.pm.test.apex"
+ private const val APK_IN_APEX_NAME = "$APEX_NAME.app"
+ private const val APK_FILE_NAME = "PackageManagerTestApexApp.apk"
+
+ private lateinit var apexInstallHelper: ApexInstallHelper
+
+ @JvmStatic
+ @BeforeClassWithInfo
+ fun initApexHelper(testInformation: TestInformation) {
+ apexInstallHelper = ApexInstallHelper(testInformation)
+ }
+
+ @JvmStatic
+ @AfterClass
+ fun revertChanges() {
+ apexInstallHelper.revertChanges()
+ }
+ }
+
+ @Before
+ @After
+ fun uninstallApp() {
+ device.uninstallPackage(APK_IN_APEX_NAME)
+ }
+
+ @Test
+ fun apexModuleName() {
+ // Install the test APEX and assert it's returned as the APEX module itself
+ // (null when not --include-apex)
+ apexInstallHelper.pushApexAndReboot("PackageManagerTestApex.apex")
+ assertModuleName(APEX_NAME).isNull()
+ assertModuleName(APEX_NAME, includeApex = true).isEqualTo(APEX_NAME)
+
+ // Check the APK-in-APEX, ensuring there is only 1 active package
+ assertModuleName(APK_IN_APEX_NAME).isEqualTo(APEX_NAME)
+ assertModuleName(APK_IN_APEX_NAME, hidden = true).isNull()
+
+ // Then install a /data update to the APK-in-APEX
+ device.installPackage(testInformation.getDependencyFile(APK_FILE_NAME, false), false)
+
+ // Verify same as above
+ assertModuleName(APEX_NAME, includeApex = true).isEqualTo(APEX_NAME)
+ assertModuleName(APK_IN_APEX_NAME).isEqualTo(APEX_NAME)
+
+ // But also check that the /data variant now has a hidden package
+ assertModuleName(APK_IN_APEX_NAME, hidden = true).isEqualTo(APEX_NAME)
+
+ // Reboot the device and check that values are preserved
+ device.reboot()
+ assertModuleName(APEX_NAME, includeApex = true).isEqualTo(APEX_NAME)
+ assertModuleName(APK_IN_APEX_NAME).isEqualTo(APEX_NAME)
+ assertModuleName(APK_IN_APEX_NAME, hidden = true).isEqualTo(APEX_NAME)
+
+ // Revert the install changes (delete system image APEX) and check that it's gone
+ apexInstallHelper.revertChanges()
+ assertModuleName(APEX_NAME, includeApex = true).isNull()
+
+ // Verify the module name is no longer associated with the APK-in-APEX,
+ // which is now just a regular /data APK with no hidden system variant.
+ // The assertion for the valid /data APK uses "null" because the value
+ // printed for normal packages is "apexModuleName=null". As opposed to
+ // a literal null indicating the package variant doesn't exist
+ assertModuleName(APK_IN_APEX_NAME).isEqualTo("null")
+ assertModuleName(APK_IN_APEX_NAME, hidden = true).isEqualTo(null)
+ }
+
+ private fun assertModuleName(
+ packageName: String,
+ hidden: Boolean = false,
+ includeApex: Boolean = false
+ ) = assertThat(
+ device.executeShellCommand(
+ "dumpsys package ${"--include-apex".takeIf { includeApex }} $packageName"
+ )
+ .lineSequence()
+ .map(String::trim)
+ .dropWhile { !it.startsWith(if (hidden) "Hidden system packages:" else "Packages:")}
+ .dropWhile { !it.startsWith("Package [$packageName]") }
+ .takeWhile { !it.startsWith("User 0:") }
+ .firstOrNull { it.startsWith("apexModuleName=") }
+ ?.removePrefix("apexModuleName=")
+ )
+}
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Apex/Android.bp b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/Android.bp
new file mode 100644
index 0000000..aef365e
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/Android.bp
@@ -0,0 +1,40 @@
+//
+// Copyright (C) 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+apex {
+ name: "PackageManagerTestApex",
+ apps: ["PackageManagerTestApexApp"],
+ androidManifest: "AndroidManifestApex.xml",
+ file_contexts: ":apex.test-file_contexts",
+ key: "apex.test.key",
+ certificate: ":apex.test.certificate",
+ min_sdk_version: "33",
+ installable: true,
+ updatable: true,
+}
+
+android_test_helper_app {
+ name: "PackageManagerTestApexApp",
+ manifest: "AndroidManifestApp.xml",
+ sdk_version: "33",
+ min_sdk_version: "33",
+ apex_available: ["PackageManagerTestApex"],
+ certificate: ":apex.test.certificate",
+}
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Apex/AndroidManifestApex.xml b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/AndroidManifestApex.xml
new file mode 100644
index 0000000..575b2bc
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/AndroidManifestApex.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 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.
+ -->
+
+<manifest package="com.android.server.pm.test.apex">
+ <application/>
+</manifest>
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Apex/AndroidManifestApp.xml b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/AndroidManifestApp.xml
new file mode 100644
index 0000000..87fb5cc
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/AndroidManifestApp.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 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.
+ -->
+
+<manifest package="com.android.server.pm.test.apex.app">
+ <application/>
+</manifest>
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Apex/apex_manifest.json b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/apex_manifest.json
new file mode 100644
index 0000000..b89581d
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/apex_manifest.json
@@ -0,0 +1,4 @@
+{
+ "name": "com.android.server.pm.test.apex",
+ "version": 1
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 9234431..c40017a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -2452,7 +2452,7 @@
if (record == null) {
record = makeServiceRecord(service);
}
- AppBindRecord binding = new AppBindRecord(record, null, client);
+ AppBindRecord binding = new AppBindRecord(record, null, client, null);
ConnectionRecord cr = spy(new ConnectionRecord(binding,
mock(ActivityServiceConnectionsHolder.class),
mock(IServiceConnection.class), bindFlags,
diff --git a/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java
index 757d27b..f6566a0d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java
@@ -39,6 +39,7 @@
import android.hardware.camera2.CameraManager;
import android.os.Process;
import android.os.UserManager;
+import android.platform.test.annotations.Presubmit;
import android.testing.TestableContext;
import android.util.ArraySet;
@@ -59,6 +60,7 @@
import java.util.ArrayList;
import java.util.List;
+@Presubmit
@RunWith(AndroidJUnit4.class)
public class CameraAccessControllerTest {
private static final String FRONT_CAMERA = "0";
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
index 2f909aa..5b0e2f3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
@@ -518,6 +518,7 @@
apexInfo.isActive = isActive;
apexInfo.isFactory = isFactory;
apexInfo.modulePath = apexFile.getPath();
+ apexInfo.preinstalledModulePath = apexFile.getPath();
return apexInfo;
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java
index 8b36da5..d5aa7fe 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java
@@ -413,8 +413,8 @@
mDexOptThread.join(TEST_WAIT_TIMEOUT_MS);
mCancelThread.join(TEST_WAIT_TIMEOUT_MS);
- // Always reschedule for periodic job
- verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, false);
+ // The job should be rescheduled.
+ verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true /* wantsReschedule */);
verifyLastControlDexOptBlockingCall(false);
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index bcba4a1..c08f6bf 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -28,7 +28,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.server.wallpaper.WallpaperManagerService.WALLPAPER;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assert.assertEquals;
@@ -71,7 +71,6 @@
import android.util.Xml;
import android.view.Display;
-import androidx.test.filters.FlakyTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
@@ -89,6 +88,7 @@
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -110,7 +110,6 @@
* atest FrameworksMockingServicesTests:WallpaperManagerServiceTests
*/
@Presubmit
-@FlakyTest(bugId = 129797242)
@RunWith(AndroidJUnit4.class)
public class WallpaperManagerServiceTests {
@@ -273,8 +272,10 @@
/**
* Tests setWallpaperComponent and clearWallpaper should work as expected.
+ * TODO ignored since the assumption never passes. to be investigated.
*/
@Test
+ @Ignore("b/264533465")
public void testSetThenClearComponent() {
// Skip if there is no pre-defined default wallpaper component.
assumeThat(sDefaultWallpaperComponent,
@@ -285,7 +286,7 @@
verifyLastWallpaperData(testUserId, sDefaultWallpaperComponent);
verifyCurrentSystemData(testUserId);
- mService.setWallpaperComponent(sImageWallpaperComponentName);
+ mService.setWallpaperComponent(sImageWallpaperComponentName, FLAG_SYSTEM, testUserId);
verifyLastWallpaperData(testUserId, sImageWallpaperComponentName);
verifyCurrentSystemData(testUserId);
@@ -312,7 +313,7 @@
WallpaperManagerService.WallpaperConnection.DisplayConnector connector =
mService.mLastWallpaper.connection.getDisplayConnectorOrCreate(DEFAULT_DISPLAY);
- mService.setWallpaperComponent(sDefaultWallpaperComponent);
+ mService.setWallpaperComponent(sDefaultWallpaperComponent, FLAG_SYSTEM, testUserId);
verify(connector.mEngine).dispatchWallpaperCommand(
eq(COMMAND_REAPPLY), anyInt(), anyInt(), anyInt(), any());
@@ -454,7 +455,7 @@
public void testGetAdjustedWallpaperColorsOnDimming() throws RemoteException {
final int testUserId = USER_SYSTEM;
mService.switchUser(testUserId, null);
- mService.setWallpaperComponent(sDefaultWallpaperComponent);
+ mService.setWallpaperComponent(sDefaultWallpaperComponent, FLAG_SYSTEM, testUserId);
WallpaperData wallpaper = mService.getCurrentWallpaperData(FLAG_SYSTEM, testUserId);
// Mock a wallpaper data with color hints that support dark text and dark theme
diff --git a/services/tests/servicestests/res/raw/dummy_keyboard_layout.kcm b/services/tests/servicestests/res/raw/dummy_keyboard_layout.kcm
new file mode 100644
index 0000000..ea6bc98
--- /dev/null
+++ b/services/tests/servicestests/res/raw/dummy_keyboard_layout.kcm
@@ -0,0 +1,311 @@
+# 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.
+
+#
+# English (US) keyboard layout.
+# Unlike the default (generic) keyboard layout, English (US) does not contain any
+# special ALT characters.
+#
+
+type OVERLAY
+
+### ROW 1
+
+key GRAVE {
+ label: '`'
+ base: '`'
+ shift: '~'
+}
+
+key 1 {
+ label: '1'
+ base: '1'
+ shift: '!'
+}
+
+key 2 {
+ label: '2'
+ base: '2'
+ shift: '@'
+}
+
+key 3 {
+ label: '3'
+ base: '3'
+ shift: '#'
+}
+
+key 4 {
+ label: '4'
+ base: '4'
+ shift: '$'
+}
+
+key 5 {
+ label: '5'
+ base: '5'
+ shift: '%'
+}
+
+key 6 {
+ label: '6'
+ base: '6'
+ shift: '^'
+}
+
+key 7 {
+ label: '7'
+ base: '7'
+ shift: '&'
+}
+
+key 8 {
+ label: '8'
+ base: '8'
+ shift: '*'
+}
+
+key 9 {
+ label: '9'
+ base: '9'
+ shift: '('
+}
+
+key 0 {
+ label: '0'
+ base: '0'
+ shift: ')'
+}
+
+key MINUS {
+ label: '-'
+ base: '-'
+ shift: '_'
+}
+
+key EQUALS {
+ label: '='
+ base: '='
+ shift: '+'
+}
+
+### ROW 2
+
+key Q {
+ label: 'Q'
+ base: 'q'
+ shift, capslock: 'Q'
+}
+
+key W {
+ label: 'W'
+ base: 'w'
+ shift, capslock: 'W'
+}
+
+key E {
+ label: 'E'
+ base: 'e'
+ shift, capslock: 'E'
+}
+
+key R {
+ label: 'R'
+ base: 'r'
+ shift, capslock: 'R'
+}
+
+key T {
+ label: 'T'
+ base: 't'
+ shift, capslock: 'T'
+}
+
+key Y {
+ label: 'Y'
+ base: 'y'
+ shift, capslock: 'Y'
+}
+
+key U {
+ label: 'U'
+ base: 'u'
+ shift, capslock: 'U'
+}
+
+key I {
+ label: 'I'
+ base: 'i'
+ shift, capslock: 'I'
+}
+
+key O {
+ label: 'O'
+ base: 'o'
+ shift, capslock: 'O'
+}
+
+key P {
+ label: 'P'
+ base: 'p'
+ shift, capslock: 'P'
+}
+
+key LEFT_BRACKET {
+ label: '['
+ base: '['
+ shift: '{'
+}
+
+key RIGHT_BRACKET {
+ label: ']'
+ base: ']'
+ shift: '}'
+}
+
+key BACKSLASH {
+ label: '\\'
+ base: '\\'
+ shift: '|'
+}
+
+### ROW 3
+
+key A {
+ label: 'A'
+ base: 'a'
+ shift, capslock: 'A'
+}
+
+key S {
+ label: 'S'
+ base: 's'
+ shift, capslock: 'S'
+}
+
+key D {
+ label: 'D'
+ base: 'd'
+ shift, capslock: 'D'
+}
+
+key F {
+ label: 'F'
+ base: 'f'
+ shift, capslock: 'F'
+}
+
+key G {
+ label: 'G'
+ base: 'g'
+ shift, capslock: 'G'
+}
+
+key H {
+ label: 'H'
+ base: 'h'
+ shift, capslock: 'H'
+}
+
+key J {
+ label: 'J'
+ base: 'j'
+ shift, capslock: 'J'
+}
+
+key K {
+ label: 'K'
+ base: 'k'
+ shift, capslock: 'K'
+}
+
+key L {
+ label: 'L'
+ base: 'l'
+ shift, capslock: 'L'
+}
+
+key SEMICOLON {
+ label: ';'
+ base: ';'
+ shift: ':'
+}
+
+key APOSTROPHE {
+ label: '\''
+ base: '\''
+ shift: '"'
+}
+
+### ROW 4
+
+key Z {
+ label: 'Z'
+ base: 'z'
+ shift, capslock: 'Z'
+}
+
+key X {
+ label: 'X'
+ base: 'x'
+ shift, capslock: 'X'
+}
+
+key C {
+ label: 'C'
+ base: 'c'
+ shift, capslock: 'C'
+}
+
+key V {
+ label: 'V'
+ base: 'v'
+ shift, capslock: 'V'
+}
+
+key B {
+ label: 'B'
+ base: 'b'
+ shift, capslock: 'B'
+}
+
+key N {
+ label: 'N'
+ base: 'n'
+ shift, capslock: 'N'
+}
+
+key M {
+ label: 'M'
+ base: 'm'
+ shift, capslock: 'M'
+}
+
+key COMMA {
+ label: ','
+ base: ','
+ shift: '<'
+}
+
+key PERIOD {
+ label: '.'
+ base: '.'
+ shift: '>'
+}
+
+key SLASH {
+ label: '/'
+ base: '/'
+ shift: '?'
+}
diff --git a/services/tests/servicestests/res/xml/keyboard_layouts.xml b/services/tests/servicestests/res/xml/keyboard_layouts.xml
new file mode 100644
index 0000000..b5a05fc
--- /dev/null
+++ b/services/tests/servicestests/res/xml/keyboard_layouts.xml
@@ -0,0 +1,81 @@
+<?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.
+ -->
+
+<keyboard-layouts xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <keyboard-layout
+ android:name="keyboard_layout_english_uk"
+ android:label="English (UK)"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="en-Latn-GB"
+ android:keyboardLayoutType="qwerty" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_english_us"
+ android:label="English (US)"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="en-Latn,en-Latn-US"
+ android:keyboardLayoutType="qwerty" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_english_us_intl"
+ android:label="English (International)"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="en-Latn-US"
+ android:keyboardLayoutType="extended" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_english_us_dvorak"
+ android:label="English (Dvorak)"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="en-Latn-US"
+ android:keyboardLayoutType="dvorak" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_german"
+ android:label="German"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="de-Latn"
+ android:keyboardLayoutType="qwertz" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_french"
+ android:label="French"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="fr-Latn-FR"
+ android:keyboardLayoutType="azerty" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_russian_qwerty"
+ android:label="Russian"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="ru-Cyrl"
+ android:keyboardLayoutType="qwerty" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_russian"
+ android:label="Russian"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="ru-Cyrl" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_vendorId:1,productId:1"
+ android:label="vendorId:1,productId:1"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ androidprv:vendorId="1"
+ androidprv:productId="1" />
+</keyboard-layouts>
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
index 0780d21..d996e37 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
@@ -214,7 +214,8 @@
}
private void notRegistered_publicMethodsShouldBeBenign(int displayId) {
- assertFalse(mFullScreenMagnificationController.isMagnifying(displayId));
+ checkActivatedAndMagnifyingState(/* activated= */false, /* magnifying= */false, displayId);
+
assertFalse(
mFullScreenMagnificationController.magnificationRegionContains(displayId, 100,
100));
@@ -646,9 +647,9 @@
.setScale(displayId, 1.5f, startCenter.x, startCenter.y, false,
SERVICE_ID_2);
assertFalse(mFullScreenMagnificationController.resetIfNeeded(displayId, SERVICE_ID_1));
- assertTrue(mFullScreenMagnificationController.isMagnifying(displayId));
+ checkActivatedAndMagnifyingState(/* activated= */true, /* magnifying= */true, displayId);
assertTrue(mFullScreenMagnificationController.resetIfNeeded(displayId, SERVICE_ID_2));
- assertFalse(mFullScreenMagnificationController.isMagnifying(displayId));
+ checkActivatedAndMagnifyingState(/* activated= */false, /* magnifying= */false, displayId);
}
@Test
@@ -667,7 +668,7 @@
assertTrue(mFullScreenMagnificationController.resetIfNeeded(displayId, false));
verify(mRequestObserver).onFullScreenMagnificationChanged(eq(displayId),
eq(INITIAL_MAGNIFICATION_REGION), any(MagnificationConfig.class));
- assertFalse(mFullScreenMagnificationController.isMagnifying(displayId));
+ checkActivatedAndMagnifyingState(/* activated= */false, /* magnifying= */false, displayId);
assertFalse(mFullScreenMagnificationController.resetIfNeeded(displayId, false));
}
@@ -731,7 +732,7 @@
mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
mStateListener.onAnimationEnd(mMockValueAnimator);
- assertFalse(mFullScreenMagnificationController.isMagnifying(DISPLAY_0));
+ checkActivatedAndMagnifyingState(/* activated= */false, /* magnifying= */false, displayId);
verify(lastAnimationCallback).onResult(true);
}
@@ -749,8 +750,8 @@
mMessageCapturingHandler.sendAllMessages();
br.onReceive(mMockContext, null);
mMessageCapturingHandler.sendAllMessages();
- assertFalse(mFullScreenMagnificationController.isMagnifying(DISPLAY_0));
- assertFalse(mFullScreenMagnificationController.isMagnifying(DISPLAY_1));
+ checkActivatedAndMagnifyingState(/* activated= */false, /* magnifying= */false, DISPLAY_0);
+ checkActivatedAndMagnifyingState(/* activated= */false, /* magnifying= */false, DISPLAY_1);
}
@Test
@@ -768,7 +769,7 @@
mMessageCapturingHandler.sendAllMessages();
callbacks.onUserContextChanged();
mMessageCapturingHandler.sendAllMessages();
- assertFalse(mFullScreenMagnificationController.isMagnifying(displayId));
+ checkActivatedAndMagnifyingState(/* activated= */false, /* magnifying= */false, displayId);
}
@Test
@@ -784,10 +785,10 @@
MagnificationCallbacks callbacks = getMagnificationCallbacks(displayId);
zoomIn2xToMiddle(displayId);
mMessageCapturingHandler.sendAllMessages();
- assertTrue(mFullScreenMagnificationController.isMagnifying(displayId));
+ checkActivatedAndMagnifyingState(/* activated= */true, /* magnifying= */true, displayId);
callbacks.onDisplaySizeChanged();
mMessageCapturingHandler.sendAllMessages();
- assertFalse(mFullScreenMagnificationController.isMagnifying(displayId));
+ checkActivatedAndMagnifyingState(/* activated= */false, /* magnifying= */false, DISPLAY_0);
}
@Test
@@ -1133,23 +1134,17 @@
}
@Test
- public void testSetForceShowMagnifiableBounds() {
+ public void testZoomTo1x_shouldActivatedAndForceShowMagnifiableBounds() {
register(DISPLAY_0);
+ final float scale = 1.0f;
+ mFullScreenMagnificationController.setScaleAndCenter(
+ DISPLAY_0, scale, Float.NaN, Float.NaN, true, SERVICE_ID_1);
- mFullScreenMagnificationController.setForceShowMagnifiableBounds(DISPLAY_0, true);
-
+ checkActivatedAndMagnifyingState(/* activated= */true, /* magnifying= */false, DISPLAY_0);
verify(mMockWindowManager).setForceShowMagnifiableBounds(DISPLAY_0, true);
}
@Test
- public void testIsForceShowMagnifiableBounds() {
- register(DISPLAY_0);
- mFullScreenMagnificationController.setForceShowMagnifiableBounds(DISPLAY_0, true);
-
- assertTrue(mFullScreenMagnificationController.isForceShowMagnifiableBounds(DISPLAY_0));
- }
-
- @Test
public void testSetScale_toMagnifying_shouldNotifyActivatedState() {
setScaleToMagnifying();
@@ -1220,7 +1215,15 @@
float scale = 2.0f;
mFullScreenMagnificationController.setScale(displayId, scale, startCenter.x, startCenter.y,
false, SERVICE_ID_1);
- assertTrue(mFullScreenMagnificationController.isMagnifying(displayId));
+ checkActivatedAndMagnifyingState(/* activated= */true, /* magnifying= */true, displayId);
+ }
+
+ private void checkActivatedAndMagnifyingState(
+ boolean activated, boolean magnifying, int displayId) {
+ final boolean isActivated = mFullScreenMagnificationController.isActivated(displayId);
+ final boolean isMagnifying = mFullScreenMagnificationController.getScale(displayId) > 1.0f;
+ assertTrue(isActivated == activated);
+ assertTrue(isMagnifying == magnifying);
}
private MagnificationCallbacks getMagnificationCallbacks(int displayId) {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index 0fed89b..5334e4c 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -19,6 +19,7 @@
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_POINTER_DOWN;
+import static android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT;
import static android.view.MotionEvent.ACTION_POINTER_UP;
import static android.view.MotionEvent.ACTION_UP;
@@ -78,24 +79,25 @@
* {@code
* digraph {
* IDLE -> SHORTCUT_TRIGGERED [label="a11y\nbtn"]
- * SHORTCUT_TRIGGERED -> IDLE [label="a11y\nbtn"]
* IDLE -> DOUBLE_TAP [label="2tap"]
* DOUBLE_TAP -> IDLE [label="timeout"]
- * DOUBLE_TAP -> TRIPLE_TAP_AND_HOLD [label="down"]
- * SHORTCUT_TRIGGERED -> TRIPLE_TAP_AND_HOLD [label="down"]
- * TRIPLE_TAP_AND_HOLD -> ZOOMED [label="up"]
- * TRIPLE_TAP_AND_HOLD -> DRAGGING_TMP [label="hold/\nswipe"]
- * DRAGGING_TMP -> IDLE [label="release"]
+ * DOUBLE_TAP -> ZOOMED [label="tap"]
+ * DOUBLE_TAP -> NON_ACTIVATED_ZOOMED_TMP [label="hold"]
+ * NON_ACTIVATED_ZOOMED_TMP -> IDLE [label="release"]
+ * SHORTCUT_TRIGGERED -> IDLE [label="a11y\nbtn"]
+ * SHORTCUT_TRIGGERED -> ZOOMED[label="tap"]
+ * SHORTCUT_TRIGGERED -> ACTIVATED_ZOOMED_TMP [label="hold"]
+ * SHORTCUT_TRIGGERED -> PANNING [label="2hold]
* ZOOMED -> ZOOMED_DOUBLE_TAP [label="2tap"]
- * ZOOMED_DOUBLE_TAP -> ZOOMED [label="timeout"]
- * ZOOMED_DOUBLE_TAP -> DRAGGING [label="hold"]
- * ZOOMED_DOUBLE_TAP -> IDLE [label="tap"]
- * DRAGGING -> ZOOMED [label="release"]
* ZOOMED -> IDLE [label="a11y\nbtn"]
* ZOOMED -> PANNING [label="2hold"]
+ * ZOOMED_DOUBLE_TAP -> ZOOMED [label="timeout"]
+ * ZOOMED_DOUBLE_TAP -> ACTIVATED_ZOOMED_TMP [label="hold"]
+ * ZOOMED_DOUBLE_TAP -> IDLE [label="tap"]
+ * ACTIVATED_ZOOMED_TMP -> ZOOMED [label="release"]
+ * PANNING -> ZOOMED [label="release"]
* PANNING -> PANNING_SCALING [label="pinch"]
* PANNING_SCALING -> ZOOMED [label="release"]
- * PANNING -> ZOOMED [label="release"]
* }
* }
*/
@@ -107,12 +109,11 @@
public static final int STATE_2TAPS = 3;
public static final int STATE_ZOOMED_2TAPS = 4;
public static final int STATE_SHORTCUT_TRIGGERED = 5;
- public static final int STATE_DRAGGING_TMP = 6;
- public static final int STATE_DRAGGING = 7;
+ public static final int STATE_NON_ACTIVATED_ZOOMED_TMP = 6;
+ public static final int STATE_ACTIVATED_ZOOMED_TMP = 7;
public static final int STATE_PANNING = 8;
public static final int STATE_SCALING_AND_PANNING = 9;
-
public static final int FIRST_STATE = STATE_IDLE;
public static final int LAST_STATE = STATE_SCALING_AND_PANNING;
@@ -164,10 +165,6 @@
public boolean magnificationRegionContains(int displayId, float x, float y) {
return true;
}
-
- @Override
- void setForceShowMagnifiableBounds(int displayId, boolean show) {
- }
};
mFullScreenMagnificationController.register(DISPLAY_0);
mClock = new OffsettableClock.Stopped();
@@ -266,11 +263,11 @@
@SuppressWarnings("Convert2MethodRef")
@Test
public void testAlternativeTransitions_areWorking() {
- // A11y button followed by a tap&hold turns temporary "viewport dragging" zoom on
+ // A11y button followed by a tap&hold turns temporary "viewport dragging" zoom in
assertTransition(STATE_SHORTCUT_TRIGGERED, () -> {
send(downEvent());
fastForward1sec();
- }, STATE_DRAGGING_TMP);
+ }, STATE_ACTIVATED_ZOOMED_TMP);
// A11y button followed by a tap turns zoom on
assertTransition(STATE_SHORTCUT_TRIGGERED, () -> tap(), STATE_ZOOMED);
@@ -281,7 +278,6 @@
// A11y button turns zoom off
assertTransition(STATE_ZOOMED, () -> triggerShortcut(), STATE_IDLE);
-
// Double tap times out while zoomed
assertTransition(STATE_ZOOMED_2TAPS, () -> {
allowEventDelegation();
@@ -291,8 +287,11 @@
// tap+tap+swipe doesn't get delegated
assertTransition(STATE_2TAPS, () -> swipe(), STATE_IDLE);
- // tap+tap+swipe initiates viewport dragging immediately
- assertTransition(STATE_2TAPS, () -> swipeAndHold(), STATE_DRAGGING_TMP);
+ // tap+tap+swipe&hold initiates temporary viewport dragging zoom in immediately
+ assertTransition(STATE_2TAPS, () -> swipeAndHold(), STATE_NON_ACTIVATED_ZOOMED_TMP);
+
+ // release when activated temporary zoom in back to zoomed
+ assertTransition(STATE_ACTIVATED_ZOOMED_TMP, () -> upEvent(), STATE_ZOOMED);
}
@Test
@@ -337,8 +336,10 @@
@Test
public void testTripleTapAndHold_zoomsImmediately() {
- assertZoomsImmediatelyOnSwipeFrom(STATE_2TAPS);
- assertZoomsImmediatelyOnSwipeFrom(STATE_SHORTCUT_TRIGGERED);
+ assertZoomsImmediatelyOnSwipeFrom(STATE_2TAPS, STATE_NON_ACTIVATED_ZOOMED_TMP);
+ assertZoomsImmediatelyOnSwipeFrom(STATE_SHORTCUT_TRIGGERED, STATE_ACTIVATED_ZOOMED_TMP);
+ assertZoomsImmediatelyOnSwipeFrom(STATE_ZOOMED_2TAPS, STATE_ACTIVATED_ZOOMED_TMP);
+
}
@Test
@@ -391,10 +392,10 @@
PointF pointer3 = new PointF(DEFAULT_X * 2, DEFAULT_Y);
send(downEvent());
- send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}));
- send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2, pointer3}));
- send(pointerEvent(ACTION_POINTER_UP, new PointF[] {pointer1, pointer2, pointer3}));
- send(pointerEvent(ACTION_POINTER_UP, new PointF[] {pointer1, pointer2, pointer3}));
+ send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}, 1));
+ send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2, pointer3}, 2));
+ send(pointerEvent(ACTION_POINTER_UP, new PointF[] {pointer1, pointer2, pointer3}, 2));
+ send(pointerEvent(ACTION_POINTER_UP, new PointF[] {pointer1, pointer2, pointer3}, 2));
send(upEvent());
assertIn(STATE_ZOOMED);
@@ -411,38 +412,53 @@
}
@Test
- public void testFirstFingerSwipe_TwoPinterDownAndZoomedState_panningState() {
+ public void testFirstFingerSwipe_twoPointerDownAndZoomedState_panningState() {
goFromStateIdleTo(STATE_ZOOMED);
PointF pointer1 = DEFAULT_POINT;
PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);
send(downEvent());
- send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}));
+ send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}, 1));
//The minimum movement to transit to panningState.
final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
pointer1.offset(sWipeMinDistance + 1, 0);
- send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}));
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 0));
assertIn(STATE_PANNING);
- assertIn(STATE_PANNING);
returnToNormalFrom(STATE_PANNING);
}
@Test
- public void testSecondFingerSwipe_TwoPinterDownAndZoomedState_panningState() {
+ public void testSecondFingerSwipe_twoPointerDownAndZoomedState_panningState() {
goFromStateIdleTo(STATE_ZOOMED);
PointF pointer1 = DEFAULT_POINT;
PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);
send(downEvent());
- send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}));
+ send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}, 1));
//The minimum movement to transit to panningState.
final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
pointer2.offset(sWipeMinDistance + 1, 0);
- send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}));
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 1));
assertIn(STATE_PANNING);
+ returnToNormalFrom(STATE_PANNING);
+ }
+
+ @Test
+ public void testSecondFingerSwipe_twoPointerDownAndShortcutTriggeredState_panningState() {
+ goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED);
+ PointF pointer1 = DEFAULT_POINT;
+ PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);
+
+ send(downEvent());
+ send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}, 1));
+ //The minimum movement to transit to panningState.
+ final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
+ pointer2.offset(sWipeMinDistance + 1, 0);
+ send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}, 1));
assertIn(STATE_PANNING);
+
returnToNormalFrom(STATE_PANNING);
}
@@ -474,11 +490,11 @@
}
}
- private void assertZoomsImmediatelyOnSwipeFrom(int state) {
- goFromStateIdleTo(state);
+ private void assertZoomsImmediatelyOnSwipeFrom(int fromState, int toState) {
+ goFromStateIdleTo(fromState);
swipeAndHold();
- assertIn(STATE_DRAGGING_TMP);
- returnToNormalFrom(STATE_DRAGGING_TMP);
+ assertIn(toState);
+ returnToNormalFrom(toState);
}
private void assertTransition(int fromState, Runnable transitionAction, int toState) {
@@ -522,44 +538,51 @@
case STATE_IDLE: {
check(tapCount() < 2, state);
check(!mMgh.mDetectingState.mShortcutTriggered, state);
+ check(!isActivated(), state);
check(!isZoomed(), state);
} break;
case STATE_ZOOMED: {
+ check(isActivated(), state);
check(isZoomed(), state);
check(tapCount() < 2, state);
} break;
case STATE_2TAPS: {
+ check(!isActivated(), state);
check(!isZoomed(), state);
check(tapCount() == 2, state);
} break;
case STATE_ZOOMED_2TAPS: {
+ check(isActivated(), state);
check(isZoomed(), state);
check(tapCount() == 2, state);
} break;
- case STATE_DRAGGING: {
+ case STATE_NON_ACTIVATED_ZOOMED_TMP: {
+ check(isActivated(), state);
check(isZoomed(), state);
check(mMgh.mCurrentState == mMgh.mViewportDraggingState,
state);
- check(mMgh.mViewportDraggingState.mZoomedInBeforeDrag, state);
+ check(!mMgh.mViewportDraggingState.mActivatedBeforeDrag, state);
} break;
- case STATE_DRAGGING_TMP: {
+ case STATE_ACTIVATED_ZOOMED_TMP: {
+ check(isActivated(), state);
check(isZoomed(), state);
check(mMgh.mCurrentState == mMgh.mViewportDraggingState,
state);
- check(!mMgh.mViewportDraggingState.mZoomedInBeforeDrag, state);
+ check(mMgh.mViewportDraggingState.mActivatedBeforeDrag, state);
} break;
case STATE_SHORTCUT_TRIGGERED: {
check(mMgh.mDetectingState.mShortcutTriggered, state);
+ check(isActivated(), state);
check(!isZoomed(), state);
} break;
case STATE_PANNING: {
- check(isZoomed(), state);
+ check(isActivated(), state);
check(mMgh.mCurrentState == mMgh.mPanningScalingState,
state);
check(!mMgh.mPanningScalingState.mScaling, state);
} break;
case STATE_SCALING_AND_PANNING: {
- check(isZoomed(), state);
+ check(isActivated(), state);
check(mMgh.mCurrentState == mMgh.mPanningScalingState,
state);
check(mMgh.mPanningScalingState.mScaling, state);
@@ -596,13 +619,13 @@
tap();
tap();
} break;
- case STATE_DRAGGING: {
- goFromStateIdleTo(STATE_ZOOMED_2TAPS);
+ case STATE_NON_ACTIVATED_ZOOMED_TMP: {
+ goFromStateIdleTo(STATE_2TAPS);
send(downEvent());
fastForward1sec();
} break;
- case STATE_DRAGGING_TMP: {
- goFromStateIdleTo(STATE_2TAPS);
+ case STATE_ACTIVATED_ZOOMED_TMP: {
+ goFromStateIdleTo(STATE_ZOOMED_2TAPS);
send(downEvent());
fastForward1sec();
} break;
@@ -654,13 +677,13 @@
case STATE_ZOOMED_2TAPS: {
tap();
} break;
- case STATE_DRAGGING: {
+ case STATE_NON_ACTIVATED_ZOOMED_TMP: {
+ send(upEvent());
+ } break;
+ case STATE_ACTIVATED_ZOOMED_TMP: {
send(upEvent());
returnToNormalFrom(STATE_ZOOMED);
} break;
- case STATE_DRAGGING_TMP: {
- send(upEvent());
- } break;
case STATE_SHORTCUT_TRIGGERED: {
triggerShortcut();
} break;
@@ -682,8 +705,12 @@
}
}
+ private boolean isActivated() {
+ return mMgh.mFullScreenMagnificationController.isActivated(DISPLAY_0);
+ }
+
private boolean isZoomed() {
- return mMgh.mFullScreenMagnificationController.isMagnifying(DISPLAY_0);
+ return mMgh.mFullScreenMagnificationController.getScale(DISPLAY_0) > 1.0f;
}
private int tapCount() {
@@ -770,10 +797,10 @@
private MotionEvent pointerEvent(int action, float x, float y) {
- return pointerEvent(action, new PointF[] {DEFAULT_POINT, new PointF(x, y)});
+ return pointerEvent(action, new PointF[] {DEFAULT_POINT, new PointF(x, y)}, 1);
}
- private MotionEvent pointerEvent(int action, PointF[] pointersPosition) {
+ private MotionEvent pointerEvent(int action, PointF[] pointersPosition, int changedIndex) {
final MotionEvent.PointerProperties[] PointerPropertiesArray =
new MotionEvent.PointerProperties[pointersPosition.length];
for (int i = 0; i < pointersPosition.length; i++) {
@@ -792,6 +819,8 @@
pointerCoordsArray[i] = pointerCoords;
}
+ action += (changedIndex << ACTION_POINTER_INDEX_SHIFT);
+
return MotionEvent.obtain(
/* downTime */ mClock.now(),
/* eventTime */ mClock.now(),
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
index 2a80ce0..231b2f32 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
@@ -495,16 +495,6 @@
}
@Test
- public void setScaleOneThroughExternalRequest_fullScreenEnabled_removeMagnificationButton()
- throws RemoteException {
- setMagnificationEnabled(MODE_FULLSCREEN);
- mScreenMagnificationController.setScaleAndCenter(TEST_DISPLAY, 1.0f,
- MAGNIFIED_CENTER_X, MAGNIFIED_CENTER_Y, false, TEST_SERVICE_ID);
-
- verify(mWindowMagnificationManager).removeMagnificationButton(eq(TEST_DISPLAY));
- }
-
- @Test
public void onPerformScaleAction_magnifierEnabled_handleScaleChange() throws RemoteException {
final float newScale = 4.0f;
setMagnificationEnabled(MODE_WINDOW);
@@ -756,7 +746,7 @@
mMagnificationController.onWindowMagnificationActivationState(TEST_DISPLAY, true);
- assertFalse(mScreenMagnificationController.isMagnifying(TEST_DISPLAY));
+ verify(mScreenMagnificationController).reset(eq(TEST_DISPLAY), eq(false));
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java
index 3a27e3b..798650d 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java
@@ -27,6 +27,7 @@
import android.companion.virtual.sensor.VirtualSensorConfig;
import android.os.Parcel;
import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -36,6 +37,7 @@
import java.util.List;
import java.util.Set;
+@Presubmit
@RunWith(AndroidJUnit4.class)
public class VirtualDeviceParamsTest {
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
index f473086..bb28a36 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
@@ -25,12 +25,14 @@
import android.companion.virtual.VirtualDevice;
import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
+@Presubmit
@RunWith(AndroidJUnit4.class)
public class VirtualDeviceTest {
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index c270435..7b5af1e 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -36,9 +36,11 @@
import android.media.PlayerBase;
import android.os.Parcel;
import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
import android.util.ArraySet;
import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.FlakyTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.companion.virtual.GenericWindowPolicyController;
@@ -53,6 +55,7 @@
import java.util.ArrayList;
import java.util.List;
+@Presubmit
@RunWith(AndroidJUnit4.class)
public class VirtualAudioControllerTest {
private static final int APP1_UID = 100;
@@ -92,6 +95,7 @@
}
+ @FlakyTest(bugId = 265155135)
@Test
public void startListening_receivesCallback() throws RemoteException {
ArraySet<Integer> runningUids = new ArraySet<>();
diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
index b5dad94..1d23e12 100644
--- a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
@@ -28,7 +28,8 @@
import android.platform.test.annotations.Presubmit
import android.view.InputDevice
import androidx.test.core.app.ApplicationProvider
-import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_LEVELS
+import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_VALUE_FOR_LEVEL
+import com.android.server.input.KeyboardBacklightController.USER_INACTIVITY_THRESHOLD_MILLIS
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@@ -78,6 +79,7 @@
const val DEVICE_ID = 1
const val LIGHT_ID = 2
const val SECOND_LIGHT_ID = 3
+ const val MAX_BRIGHTNESS = 255
}
@get:Rule
@@ -118,10 +120,6 @@
val args = it.arguments
lightColorMap.put(args[1] as Int, args[2] as Int)
}
- `when`(native.getLightColor(anyInt(), anyInt())).then {
- val args = it.arguments
- lightColorMap.getOrDefault(args[1] as Int, -1)
- }
lightColorMap.clear()
}
@@ -137,21 +135,17 @@
`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
`when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- // Initially backlight is at min
- lightColorMap[LIGHT_ID] = Color.argb(BRIGHTNESS_LEVELS.first(), 0, 0, 0)
- val brightnessLevelsArray = BRIGHTNESS_LEVELS.toTypedArray()
- for (level in 1 until brightnessLevelsArray.size) {
- keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
- testLooper.dispatchNext()
+ for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) {
+ incrementKeyboardBacklight(DEVICE_ID)
assertEquals(
"Light value for level $level mismatched",
- Color.argb(brightnessLevelsArray[level], 0, 0, 0),
+ Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
lightColorMap[LIGHT_ID]
)
assertEquals(
"Light value for level $level must be correctly stored in the datastore",
- brightnessLevelsArray[level],
+ BRIGHTNESS_VALUE_FOR_LEVEL[level],
dataStore.getKeyboardBacklightBrightness(
keyboardWithBacklight.descriptor,
LIGHT_ID
@@ -159,72 +153,49 @@
)
}
- for (level in brightnessLevelsArray.size - 2 downTo 0) {
- keyboardBacklightController.decrementKeyboardBacklight(DEVICE_ID)
- testLooper.dispatchNext()
- assertEquals(
- "Light value for level $level mismatched",
- Color.argb(brightnessLevelsArray[level], 0, 0, 0),
- lightColorMap[LIGHT_ID]
- )
- assertEquals(
- "Light value for level $level must be correctly stored in the datastore",
- brightnessLevelsArray[level],
- dataStore.getKeyboardBacklightBrightness(
- keyboardWithBacklight.descriptor,
- LIGHT_ID
- ).asInt
- )
- }
- }
-
- @Test
- fun testKeyboardBacklightIncrementAboveMaxLevel() {
- val keyboardWithBacklight = createKeyboard(DEVICE_ID)
- val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
- `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
- `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
- keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- // Initially backlight is at max
- lightColorMap[LIGHT_ID] = Color.argb(BRIGHTNESS_LEVELS.last(), 0, 0, 0)
-
- keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
- testLooper.dispatchNext()
+ // Increment above max level
+ incrementKeyboardBacklight(DEVICE_ID)
assertEquals(
"Light value for max level mismatched",
- Color.argb(BRIGHTNESS_LEVELS.last(), 0, 0, 0),
+ Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
lightColorMap[LIGHT_ID]
)
assertEquals(
"Light value for max level must be correctly stored in the datastore",
- BRIGHTNESS_LEVELS.last(),
+ MAX_BRIGHTNESS,
dataStore.getKeyboardBacklightBrightness(
keyboardWithBacklight.descriptor,
LIGHT_ID
).asInt
)
- }
- @Test
- fun testKeyboardBacklightDecrementBelowMin() {
- val keyboardWithBacklight = createKeyboard(DEVICE_ID)
- val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
- `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
- `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
- keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- // Initially backlight is at min
- lightColorMap[LIGHT_ID] = Color.argb(BRIGHTNESS_LEVELS.first(), 0, 0, 0)
+ for (level in BRIGHTNESS_VALUE_FOR_LEVEL.size - 2 downTo 0) {
+ decrementKeyboardBacklight(DEVICE_ID)
+ assertEquals(
+ "Light value for level $level mismatched",
+ Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
+ lightColorMap[LIGHT_ID]
+ )
+ assertEquals(
+ "Light value for level $level must be correctly stored in the datastore",
+ BRIGHTNESS_VALUE_FOR_LEVEL[level],
+ dataStore.getKeyboardBacklightBrightness(
+ keyboardWithBacklight.descriptor,
+ LIGHT_ID
+ ).asInt
+ )
+ }
- keyboardBacklightController.decrementKeyboardBacklight(DEVICE_ID)
- testLooper.dispatchNext()
+ // Decrement below min level
+ decrementKeyboardBacklight(DEVICE_ID)
assertEquals(
"Light value for min level mismatched",
- Color.argb(BRIGHTNESS_LEVELS.first(), 0, 0, 0),
+ Color.argb(0, 0, 0, 0),
lightColorMap[LIGHT_ID]
)
assertEquals(
"Light value for min level must be correctly stored in the datastore",
- BRIGHTNESS_LEVELS.first(),
+ 0,
dataStore.getKeyboardBacklightBrightness(
keyboardWithBacklight.descriptor,
LIGHT_ID
@@ -240,7 +211,7 @@
`when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight))
keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
+ incrementKeyboardBacklight(DEVICE_ID)
assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty())
}
@@ -258,8 +229,7 @@
)
keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
- testLooper.dispatchNext()
+ incrementKeyboardBacklight(DEVICE_ID)
assertEquals("Only keyboard backlights should change", 1, lightColorMap.size)
assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID])
assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID])
@@ -275,14 +245,15 @@
dataStore.setKeyboardBacklightBrightness(
keyboardWithBacklight.descriptor,
LIGHT_ID,
- BRIGHTNESS_LEVELS.last()
+ MAX_BRIGHTNESS
)
- lightColorMap.clear()
keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+ keyboardBacklightController.notifyUserActivity()
+ testLooper.dispatchNext()
assertEquals(
"Keyboard backlight level should be restored to the level saved in the data store",
- Color.argb(BRIGHTNESS_LEVELS.last(), 0, 0, 0),
+ Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
lightColorMap[LIGHT_ID]
)
}
@@ -295,11 +266,12 @@
dataStore.setKeyboardBacklightBrightness(
keyboardWithBacklight.descriptor,
LIGHT_ID,
- BRIGHTNESS_LEVELS.last()
+ MAX_BRIGHTNESS
)
- lightColorMap.clear()
keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+ keyboardBacklightController.notifyUserActivity()
+ testLooper.dispatchNext()
assertTrue(
"Keyboard backlight should not be changed until its added",
lightColorMap.isEmpty()
@@ -307,22 +279,22 @@
`when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
keyboardBacklightController.onInputDeviceChanged(DEVICE_ID)
+ keyboardBacklightController.notifyUserActivity()
+ testLooper.dispatchNext()
assertEquals(
"Keyboard backlight level should be restored to the level saved in the data store",
- Color.argb(BRIGHTNESS_LEVELS.last(), 0, 0, 0),
+ Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
lightColorMap[LIGHT_ID]
)
}
@Test
- fun testKeyboardBacklightT_registerUnregisterListener() {
+ fun testKeyboardBacklight_registerUnregisterListener() {
val keyboardWithBacklight = createKeyboard(DEVICE_ID)
val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
`when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- // Initially backlight is at min
- lightColorMap[LIGHT_ID] = Color.argb(BRIGHTNESS_LEVELS.first(), 0, 0, 0)
// Register backlight listener
val listener = KeyboardBacklightListener()
@@ -343,8 +315,8 @@
lastBacklightState!!.brightnessLevel
)
assertEquals(
- "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_LEVELS.size - 1),
- (BRIGHTNESS_LEVELS.size - 1),
+ "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1),
+ (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1),
lastBacklightState!!.maxBrightnessLevel
)
assertEquals(
@@ -357,12 +329,70 @@
keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0)
lastBacklightState = null
- keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
- testLooper.dispatchNext()
+ incrementKeyboardBacklight(DEVICE_ID)
assertNull("Listener should not receive any updates", lastBacklightState)
}
+ @Test
+ fun testKeyboardBacklight_userActivity() {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ dataStore.setKeyboardBacklightBrightness(
+ keyboardWithBacklight.descriptor,
+ LIGHT_ID,
+ MAX_BRIGHTNESS
+ )
+
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+ keyboardBacklightController.notifyUserActivity()
+ testLooper.dispatchNext()
+ assertEquals(
+ "Keyboard backlight level should be restored to the level saved in the data store",
+ Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
+ lightColorMap[LIGHT_ID]
+ )
+
+ testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000)
+ testLooper.dispatchNext()
+ assertEquals(
+ "Keyboard backlight level should be turned off after inactivity",
+ 0,
+ lightColorMap[LIGHT_ID]
+ )
+ }
+
+ @Test
+ fun testKeyboardBacklight_displayOnOff() {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ dataStore.setKeyboardBacklightBrightness(
+ keyboardWithBacklight.descriptor,
+ LIGHT_ID,
+ MAX_BRIGHTNESS
+ )
+
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+ keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */)
+ assertEquals(
+ "Keyboard backlight level should be restored to the level saved in the data " +
+ "store when display turned on",
+ Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
+ lightColorMap[LIGHT_ID]
+ )
+
+ keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */)
+ assertEquals(
+ "Keyboard backlight level should be turned off after display is turned off",
+ 0,
+ lightColorMap[LIGHT_ID]
+ )
+ }
+
inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() {
override fun onBrightnessChanged(
deviceId: Int,
@@ -378,6 +408,18 @@
}
}
+ private fun incrementKeyboardBacklight(deviceId: Int) {
+ keyboardBacklightController.incrementKeyboardBacklight(deviceId)
+ keyboardBacklightController.notifyUserActivity()
+ testLooper.dispatchAll()
+ }
+
+ private fun decrementKeyboardBacklight(deviceId: Int) {
+ keyboardBacklightController.decrementKeyboardBacklight(deviceId)
+ keyboardBacklightController.notifyUserActivity()
+ testLooper.dispatchAll()
+ }
+
class KeyboardBacklightState(
val deviceId: Int,
val brightnessLevel: Int,
diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt
new file mode 100644
index 0000000..34540c3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt
@@ -0,0 +1,820 @@
+/*
+ * 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.input
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.ServiceInfo
+import android.hardware.input.IInputManager
+import android.hardware.input.InputManager
+import android.hardware.input.KeyboardLayout
+import android.icu.lang.UScript
+import android.icu.util.ULocale
+import android.os.Bundle
+import android.os.test.TestLooper
+import android.platform.test.annotations.Presubmit
+import android.provider.Settings
+import android.view.InputDevice
+import android.view.inputmethod.InputMethodInfo
+import android.view.inputmethod.InputMethodSubtype
+import androidx.test.core.R
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.util.Locale
+
+private fun createKeyboard(
+ deviceId: Int,
+ vendorId: Int,
+ productId: Int,
+ languageTag: String,
+ layoutType: String
+): InputDevice =
+ InputDevice.Builder()
+ .setId(deviceId)
+ .setName("Device $deviceId")
+ .setDescriptor("descriptor $deviceId")
+ .setSources(InputDevice.SOURCE_KEYBOARD)
+ .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+ .setExternal(true)
+ .setVendorId(vendorId)
+ .setProductId(productId)
+ .setKeyboardLanguageTag(languageTag)
+ .setKeyboardLayoutType(layoutType)
+ .build()
+
+/**
+ * Tests for {@link Default UI} and {@link New UI}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:KeyboardLayoutManagerTests
+ */
+@Presubmit
+class KeyboardLayoutManagerTests {
+ companion object {
+ const val DEVICE_ID = 1
+ const val VENDOR_SPECIFIC_DEVICE_ID = 2
+ const val ENGLISH_DVORAK_DEVICE_ID = 3
+ const val USER_ID = 4
+ const val IME_ID = "ime_id"
+ const val PACKAGE_NAME = "KeyboardLayoutManagerTests"
+ const val RECEIVER_NAME = "DummyReceiver"
+ private const val ENGLISH_US_LAYOUT_NAME = "keyboard_layout_english_us"
+ private const val ENGLISH_UK_LAYOUT_NAME = "keyboard_layout_english_uk"
+ private const val VENDOR_SPECIFIC_LAYOUT_NAME = "keyboard_layout_vendorId:1,productId:1"
+ }
+
+ private val ENGLISH_US_LAYOUT_DESCRIPTOR = createLayoutDescriptor(ENGLISH_US_LAYOUT_NAME)
+ private val ENGLISH_UK_LAYOUT_DESCRIPTOR = createLayoutDescriptor(ENGLISH_UK_LAYOUT_NAME)
+ private val VENDOR_SPECIFIC_LAYOUT_DESCRIPTOR =
+ createLayoutDescriptor(VENDOR_SPECIFIC_LAYOUT_NAME)
+
+ @get:Rule
+ val rule = MockitoJUnit.rule()!!
+
+ @Mock
+ private lateinit var iInputManager: IInputManager
+
+ @Mock
+ private lateinit var native: NativeInputManagerService
+
+ @Mock
+ private lateinit var packageManager: PackageManager
+ private lateinit var keyboardLayoutManager: KeyboardLayoutManager
+
+ private lateinit var imeInfo: InputMethodInfo
+ private var nextImeSubtypeId = 0
+ private lateinit var context: Context
+ private lateinit var dataStore: PersistentDataStore
+ private lateinit var testLooper: TestLooper
+
+ // Devices
+ private lateinit var keyboardDevice: InputDevice
+ private lateinit var vendorSpecificKeyboardDevice: InputDevice
+ private lateinit var englishDvorakKeyboardDevice: InputDevice
+
+ @Before
+ fun setup() {
+ context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+ dataStore = PersistentDataStore(object : PersistentDataStore.Injector() {
+ override fun openRead(): InputStream? {
+ throw FileNotFoundException()
+ }
+
+ override fun startWrite(): FileOutputStream? {
+ throw IOException()
+ }
+
+ override fun finishWrite(fos: FileOutputStream?, success: Boolean) {}
+ })
+ testLooper = TestLooper()
+ keyboardLayoutManager = KeyboardLayoutManager(context, native, dataStore, testLooper.looper)
+ setupInputDevices()
+ setupBroadcastReceiver()
+ setupIme()
+ }
+
+ private fun setupInputDevices() {
+ val inputManager = InputManager.resetInstance(iInputManager)
+ Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
+ .thenReturn(inputManager)
+
+ keyboardDevice = createKeyboard(DEVICE_ID, 0, 0, "", "")
+ vendorSpecificKeyboardDevice = createKeyboard(VENDOR_SPECIFIC_DEVICE_ID, 1, 1, "", "")
+ englishDvorakKeyboardDevice =
+ createKeyboard(ENGLISH_DVORAK_DEVICE_ID, 0, 0, "en", "dvorak")
+ Mockito.`when`(iInputManager.inputDeviceIds)
+ .thenReturn(intArrayOf(DEVICE_ID, VENDOR_SPECIFIC_DEVICE_ID, ENGLISH_DVORAK_DEVICE_ID))
+ Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice)
+ Mockito.`when`(iInputManager.getInputDevice(VENDOR_SPECIFIC_DEVICE_ID))
+ .thenReturn(vendorSpecificKeyboardDevice)
+ Mockito.`when`(iInputManager.getInputDevice(ENGLISH_DVORAK_DEVICE_ID))
+ .thenReturn(englishDvorakKeyboardDevice)
+ }
+
+ private fun setupBroadcastReceiver() {
+ Mockito.`when`(context.packageManager).thenReturn(packageManager)
+
+ val info = createMockReceiver()
+ Mockito.`when`(packageManager.queryBroadcastReceivers(Mockito.any(), Mockito.anyInt()))
+ .thenReturn(listOf(info))
+ Mockito.`when`(packageManager.getReceiverInfo(Mockito.any(), Mockito.anyInt()))
+ .thenReturn(info.activityInfo)
+
+ val resources = context.resources
+ Mockito.`when`(
+ packageManager.getResourcesForApplication(
+ Mockito.any(
+ ApplicationInfo::class.java
+ )
+ )
+ ).thenReturn(resources)
+ }
+
+ private fun setupIme() {
+ imeInfo = InputMethodInfo(PACKAGE_NAME, RECEIVER_NAME, "", "", 0)
+ }
+
+ @Test
+ fun testDefaultUi_getKeyboardLayouts() {
+ NewSettingsApiFlag(false).use {
+ val keyboardLayouts = keyboardLayoutManager.keyboardLayouts
+ assertNotEquals(
+ "Default UI: Keyboard layout API should not return empty array",
+ 0,
+ keyboardLayouts.size
+ )
+ assertTrue(
+ "Default UI: Keyboard layout API should provide English(US) layout",
+ hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR)
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getKeyboardLayouts() {
+ NewSettingsApiFlag(true).use {
+ val keyboardLayouts = keyboardLayoutManager.keyboardLayouts
+ assertNotEquals(
+ "New UI: Keyboard layout API should not return empty array",
+ 0,
+ keyboardLayouts.size
+ )
+ assertTrue(
+ "New UI: Keyboard layout API should provide English(US) layout",
+ hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR)
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_getKeyboardLayoutsForInputDevice() {
+ NewSettingsApiFlag(false).use {
+ val keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutsForInputDevice(keyboardDevice.identifier)
+ assertNotEquals(
+ "Default UI: getKeyboardLayoutsForInputDevice API should not return empty array",
+ 0,
+ keyboardLayouts.size
+ )
+ assertTrue(
+ "Default UI: getKeyboardLayoutsForInputDevice API should provide English(US) " +
+ "layout",
+ hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR)
+ )
+
+ val vendorSpecificKeyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutsForInputDevice(
+ vendorSpecificKeyboardDevice.identifier
+ )
+ assertEquals(
+ "Default UI: getKeyboardLayoutsForInputDevice API should return only vendor " +
+ "specific layout",
+ 1,
+ vendorSpecificKeyboardLayouts.size
+ )
+ assertEquals(
+ "Default UI: getKeyboardLayoutsForInputDevice API should return vendor specific " +
+ "layout",
+ VENDOR_SPECIFIC_LAYOUT_DESCRIPTOR,
+ vendorSpecificKeyboardLayouts[0].descriptor
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getKeyboardLayoutsForInputDevice() {
+ NewSettingsApiFlag(true).use {
+ val keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutsForInputDevice(keyboardDevice.identifier)
+ assertEquals(
+ "New UI: getKeyboardLayoutsForInputDevice API should always return empty array",
+ 0,
+ keyboardLayouts.size
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_getSetCurrentKeyboardLayoutForInputDevice() {
+ NewSettingsApiFlag(false).use {
+ assertNull(
+ "Default UI: getCurrentKeyboardLayoutForInputDevice API should return null if " +
+ "nothing was set",
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+
+ keyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier,
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ val keyboardLayout =
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ assertEquals(
+ "Default UI: getCurrentKeyboardLayoutForInputDevice API should return the set " +
+ "layout",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayout
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getSetCurrentKeyboardLayoutForInputDevice() {
+ NewSettingsApiFlag(true).use {
+ keyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier,
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ assertNull(
+ "New UI: getCurrentKeyboardLayoutForInputDevice API should always return null " +
+ "even after setCurrentKeyboardLayoutForInputDevice",
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_getEnabledKeyboardLayoutsForInputDevice() {
+ NewSettingsApiFlag(false).use {
+ keyboardLayoutManager.addKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+
+ val keyboardLayouts =
+ keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice(
+ keyboardDevice.identifier
+ )
+ assertEquals(
+ "Default UI: getEnabledKeyboardLayoutsForInputDevice API should return added " +
+ "layout",
+ 1,
+ keyboardLayouts.size
+ )
+ assertEquals(
+ "Default UI: getEnabledKeyboardLayoutsForInputDevice API should return " +
+ "English(US) layout",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayouts[0]
+ )
+ assertEquals(
+ "Default UI: getCurrentKeyboardLayoutForInputDevice API should return " +
+ "English(US) layout (Auto select the first enabled layout)",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+
+ keyboardLayoutManager.removeKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ assertEquals(
+ "Default UI: getKeyboardLayoutsForInputDevice API should return 0 layouts",
+ 0,
+ keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice(
+ keyboardDevice.identifier
+ ).size
+ )
+ assertNull(
+ "Default UI: getCurrentKeyboardLayoutForInputDevice API should return null after " +
+ "the enabled layout is removed",
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getEnabledKeyboardLayoutsForInputDevice() {
+ NewSettingsApiFlag(true).use {
+ keyboardLayoutManager.addKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+
+ assertEquals(
+ "New UI: getEnabledKeyboardLayoutsForInputDevice API should return always return " +
+ "an empty array",
+ 0,
+ keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice(
+ keyboardDevice.identifier
+ ).size
+ )
+ assertNull(
+ "New UI: getCurrentKeyboardLayoutForInputDevice API should always return null",
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_switchKeyboardLayout() {
+ NewSettingsApiFlag(false).use {
+ keyboardLayoutManager.addKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ keyboardLayoutManager.addKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_UK_LAYOUT_DESCRIPTOR
+ )
+ assertEquals(
+ "Default UI: getCurrentKeyboardLayoutForInputDevice API should return " +
+ "English(US) layout",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+
+ keyboardLayoutManager.switchKeyboardLayout(DEVICE_ID, 1)
+
+ // Throws null pointer because trying to show toast using TestLooper
+ assertThrows(NullPointerException::class.java) { testLooper.dispatchAll() }
+ assertEquals("Default UI: getCurrentKeyboardLayoutForInputDevice API should return " +
+ "English(UK) layout",
+ ENGLISH_UK_LAYOUT_DESCRIPTOR,
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_switchKeyboardLayout() {
+ NewSettingsApiFlag(true).use {
+ keyboardLayoutManager.addKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ keyboardLayoutManager.addKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_UK_LAYOUT_DESCRIPTOR
+ )
+
+ keyboardLayoutManager.switchKeyboardLayout(DEVICE_ID, 1)
+ testLooper.dispatchAll()
+
+ assertNull("New UI: getCurrentKeyboardLayoutForInputDevice API should always return " +
+ "null",
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_getKeyboardLayout() {
+ NewSettingsApiFlag(false).use {
+ val keyboardLayout =
+ keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR)
+ assertEquals("Default UI: getKeyboardLayout API should return correct Layout from " +
+ "available layouts",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayout!!.descriptor
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getKeyboardLayout() {
+ NewSettingsApiFlag(true).use {
+ val keyboardLayout =
+ keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR)
+ assertEquals("New UI: getKeyboardLayout API should return correct Layout from " +
+ "available layouts",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayout!!.descriptor
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_getSetKeyboardLayoutForInputDevice_WithImeInfo() {
+ NewSettingsApiFlag(false).use {
+ val imeSubtype = createImeSubtype()
+ keyboardLayoutManager.setKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
+ ENGLISH_UK_LAYOUT_DESCRIPTOR
+ )
+ val keyboardLayout =
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype
+ )
+ assertNull(
+ "Default UI: getKeyboardLayoutForInputDevice API should always return null",
+ keyboardLayout
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getSetKeyboardLayoutForInputDevice_withImeInfo() {
+ NewSettingsApiFlag(true).use {
+ val imeSubtype = createImeSubtype()
+
+ keyboardLayoutManager.setKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
+ ENGLISH_UK_LAYOUT_DESCRIPTOR
+ )
+ assertEquals(
+ "New UI: getKeyboardLayoutForInputDevice API should return the set layout",
+ ENGLISH_UK_LAYOUT_DESCRIPTOR,
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype
+ )
+ )
+
+ // This should replace previously set layout
+ keyboardLayoutManager.setKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ assertEquals(
+ "New UI: getKeyboardLayoutForInputDevice API should return the last set layout",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_getKeyboardLayoutListForInputDevice() {
+ NewSettingsApiFlag(false).use {
+ val keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ createImeSubtype()
+ )
+ assertEquals("Default UI: getKeyboardLayoutListForInputDevice API should always " +
+ "return empty array",
+ 0,
+ keyboardLayouts.size
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getKeyboardLayoutListForInputDevice() {
+ NewSettingsApiFlag(true).use {
+ // Check Layouts for "hi-Latn". It should return all 'Latn' keyboard layouts
+ var keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ createImeSubtypeForLanguageTag("hi-Latn")
+ )
+ assertNotEquals(
+ "New UI: getKeyboardLayoutListForInputDevice API should return the list of " +
+ "supported layouts with matching script code",
+ 0,
+ keyboardLayouts.size
+ )
+
+ val englishScripts = UScript.getCode(Locale.forLanguageTag("hi-Latn"))
+ for (kl in keyboardLayouts) {
+ var isCompatible = false
+ for (i in 0 until kl.locales.size()) {
+ val locale: Locale = kl.locales.get(i) ?: continue
+ val scripts = UScript.getCode(locale)
+ if (scripts != null && areScriptsCompatible(scripts, englishScripts)) {
+ isCompatible = true
+ break
+ }
+ }
+ assertTrue(
+ "New UI: getKeyboardLayoutListForInputDevice API should only return " +
+ "compatible layouts but found " + kl.descriptor,
+ isCompatible
+ )
+ }
+
+ // Check Layouts for "hi" which by default uses 'Deva' script.
+ keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ createImeSubtypeForLanguageTag("hi")
+ )
+ assertEquals("New UI: getKeyboardLayoutListForInputDevice API should return empty " +
+ "list if no supported layouts available",
+ 0,
+ keyboardLayouts.size
+ )
+
+ // If user manually selected some layout, always provide it in the layout list
+ val imeSubtype = createImeSubtypeForLanguageTag("hi")
+ keyboardLayoutManager.setKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ imeSubtype
+ )
+ assertEquals("New UI: getKeyboardLayoutListForInputDevice API should return user " +
+ "selected layout even if the script is incompatible with IME",
+ 1,
+ keyboardLayouts.size
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withImeLanguageTag() {
+ NewSettingsApiFlag(true).use {
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTag("en-US"),
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTag("en-GB"),
+ ENGLISH_UK_LAYOUT_DESCRIPTOR
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTag("de"),
+ createLayoutDescriptor("keyboard_layout_german")
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTag("fr-FR"),
+ createLayoutDescriptor("keyboard_layout_french")
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTag("ru"),
+ createLayoutDescriptor("keyboard_layout_russian")
+ )
+ assertNull(
+ "New UI: getDefaultKeyboardLayoutForInputDevice should return null when no " +
+ "layout available",
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ createImeSubtypeForLanguageTag("it")
+ )
+ )
+ assertNull(
+ "New UI: getDefaultKeyboardLayoutForInputDevice should return null when no " +
+ "layout for script code is available",
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ createImeSubtypeForLanguageTag("en-Deva")
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withImeLanguageTagAndLayoutType() {
+ NewSettingsApiFlag(true).use {
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("en-US", "qwerty"),
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("en-US", "dvorak"),
+ createLayoutDescriptor("keyboard_layout_english_us_dvorak")
+ )
+ // Try to match layout type even if country doesn't match
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("en-GB", "dvorak"),
+ createLayoutDescriptor("keyboard_layout_english_us_dvorak")
+ )
+ // Choose layout based on layout type priority, if layout type is not provided by IME
+ // (Qwerty > Dvorak > Extended)
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("en-US", ""),
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("en-GB", "qwerty"),
+ ENGLISH_UK_LAYOUT_DESCRIPTOR
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("de", "qwertz"),
+ createLayoutDescriptor("keyboard_layout_german")
+ )
+ // Wrong layout type should match with language if provided layout type not available
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("de", "qwerty"),
+ createLayoutDescriptor("keyboard_layout_german")
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("fr-FR", "azerty"),
+ createLayoutDescriptor("keyboard_layout_french")
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("ru", "qwerty"),
+ createLayoutDescriptor("keyboard_layout_russian_qwerty")
+ )
+ // If layout type is empty then prioritize KCM with empty layout type
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("ru", ""),
+ createLayoutDescriptor("keyboard_layout_russian")
+ )
+ assertNull("New UI: getDefaultKeyboardLayoutForInputDevice should return null when " +
+ "no layout for script code is available",
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ createImeSubtypeForLanguageTagAndLayoutType("en-Deva-US", "")
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withHwLanguageTagAndLayoutType() {
+ NewSettingsApiFlag(true).use {
+ // Should return English dvorak even if IME current layout is qwerty, since HW says the
+ // keyboard is a Dvorak keyboard
+ assertCorrectLayout(
+ englishDvorakKeyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("en", "qwerty"),
+ createLayoutDescriptor("keyboard_layout_english_us_dvorak")
+ )
+
+ // Fallback to IME information if the HW provided layout script is incompatible with the
+ // provided IME subtype
+ assertCorrectLayout(
+ englishDvorakKeyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("ru", ""),
+ createLayoutDescriptor("keyboard_layout_russian")
+ )
+ }
+ }
+
+ private fun assertCorrectLayout(
+ device: InputDevice,
+ imeSubtype: InputMethodSubtype,
+ expectedLayout: String
+ ) {
+ assertEquals(
+ "New UI: getDefaultKeyboardLayoutForInputDevice should return $expectedLayout",
+ expectedLayout,
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ device.identifier, USER_ID, imeInfo, imeSubtype
+ )
+ )
+ }
+
+ private fun createImeSubtype(): InputMethodSubtype =
+ InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++).build()
+
+ private fun createImeSubtypeForLanguageTag(languageTag: String): InputMethodSubtype =
+ InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++)
+ .setLanguageTag(languageTag).build()
+
+ private fun createImeSubtypeForLanguageTagAndLayoutType(
+ languageTag: String,
+ layoutType: String
+ ): InputMethodSubtype =
+ InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++)
+ .setPhysicalKeyboardHint(ULocale.forLanguageTag(languageTag), layoutType).build()
+
+ private fun hasLayout(layoutList: Array<KeyboardLayout>, layoutDesc: String): Boolean {
+ for (kl in layoutList) {
+ if (kl.descriptor == layoutDesc) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun createLayoutDescriptor(keyboardName: String): String =
+ "$PACKAGE_NAME/$RECEIVER_NAME/$keyboardName"
+
+ private fun areScriptsCompatible(scriptList1: IntArray, scriptList2: IntArray): Boolean {
+ for (s1 in scriptList1) {
+ for (s2 in scriptList2) {
+ if (s1 == s2) return true
+ }
+ }
+ return false
+ }
+
+ private fun createMockReceiver(): ResolveInfo {
+ val info = ResolveInfo()
+ info.activityInfo = ActivityInfo()
+ info.activityInfo.packageName = PACKAGE_NAME
+ info.activityInfo.name = RECEIVER_NAME
+ info.activityInfo.applicationInfo = ApplicationInfo()
+ info.activityInfo.metaData = Bundle()
+ info.activityInfo.metaData.putInt(
+ InputManager.META_DATA_KEYBOARD_LAYOUTS,
+ R.xml.keyboard_layouts
+ )
+ info.serviceInfo = ServiceInfo()
+ info.serviceInfo.packageName = PACKAGE_NAME
+ info.serviceInfo.name = RECEIVER_NAME
+ return info
+ }
+
+ private inner class NewSettingsApiFlag constructor(enabled: Boolean) : AutoCloseable {
+ init {
+ Settings.Global.putString(
+ context.contentResolver,
+ "settings_new_keyboard_ui", enabled.toString()
+ )
+ }
+
+ override fun close() {
+ Settings.Global.putString(
+ context.contentResolver,
+ "settings_new_keyboard_ui",
+ ""
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
index f5029ec..f2e03aa 100644
--- a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
@@ -165,6 +165,7 @@
awaitJobStart(DEFAULT_WAIT_TIMEOUT));
}
+ @FlakyTest
@Test
public void testFeatureFlag() throws Exception {
Settings.Global.putInt(mContext.getContentResolver(),
diff --git a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
index d80aa57..ccf530f 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
@@ -539,7 +539,8 @@
NoSuchFieldException, PackageManager.NameNotFoundException {
assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
InstallSourceInfo installSourceInfo = new InstallSourceInfo(
- /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null,
+ /* initiatingPackageName = */ INSTALLER_NAME_1,
+ /* initiatingPackageSigningInfo = */ null,
/* originatingPackageName = */ null,
/* installingPackageName = */ INSTALLER_NAME_1);
assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
@@ -575,7 +576,8 @@
NoSuchFieldException, PackageManager.NameNotFoundException {
assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
InstallSourceInfo installSourceInfo = new InstallSourceInfo(
- /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null,
+ /* initiatingPackageName = */ INSTALLER_NAME_1,
+ /* initiatingPackageSigningInfo = */ null,
/* originatingPackageName = */ null,
/* installingPackageName = */ INSTALLER_NAME_1);
assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
@@ -619,7 +621,8 @@
NoSuchFieldException, PackageManager.NameNotFoundException {
assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
InstallSourceInfo installSourceInfo = new InstallSourceInfo(
- /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null,
+ /* initiatingPackageName = */ INSTALLER_NAME_1,
+ /* initiatingPackageSigningInfo = */ null,
/* originatingPackageName = */ null,
/* installingPackageName = */ INSTALLER_NAME_1);
assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
@@ -667,7 +670,8 @@
NoSuchFieldException, PackageManager.NameNotFoundException {
assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
InstallSourceInfo installSourceInfo = new InstallSourceInfo(
- /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null,
+ /* initiatingPackageName = */ INSTALLER_NAME_1,
+ /* initiatingPackageSigningInfo = */ null,
/* originatingPackageName = */ null,
/* installingPackageName = */ INSTALLER_NAME_1);
assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
@@ -711,7 +715,52 @@
assertEquals(1, packages.size());
assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
}
+ @Test
+ public void testHandleUsageEvent_packageAddedThroughAdb() throws
+ NoSuchFieldException, PackageManager.NameNotFoundException {
+ assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+ InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+ /* initiatingPackageName = */ null, //currently ADB installer sets field to null
+ /* initiatingPackageSigningInfo = */ null,
+ /* originatingPackageName = */ null,
+ /* installingPackageName = */ INSTALLER_NAME_1);
+ // b/265203007
+ when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
+ ApplicationInfo appInfo = mock(ApplicationInfo.class);
+ when(mPackageManager.getApplicationInfoAsUser(
+ eq(PACKAGE_NAME_1),
+ any(),
+ anyInt())
+ ).thenReturn(appInfo);
+
+ long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+ - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+ FieldSetter.setField(appInfo,
+ ApplicationInfo.class.getDeclaredField("createTimestamp"),
+ createTimestamp);
+
+ int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+ assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+ // The following usage events generation is the same as
+ // testHandleUsageEvent_packageAddedOutsideTimeFrame2 test. The only difference is that
+ // for ADB installs the initiatingPackageName is null, despite being detected as a
+ // background install. Since we do not want to treat side-loaded apps as background install
+ // getBackgroundInstalledPackages() is expected to return null
+ doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+ anyString(), anyString(), anyInt());
+ generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+ USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+ generateUsageEvent(Event.ACTIVITY_STOPPED,
+ USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
+
+ mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
+ mTestLooper.dispatchAll();
+
+ var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+ assertNull(packages);
+ }
@Test
public void testPackageRemoved() {
assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
index 2ebe215..cff4cc7 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
@@ -18,6 +18,7 @@
import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL;
import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync.UPDATE_BT;
+import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync.UPDATE_CAMERA;
import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync.UPDATE_CPU;
import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync.UPDATE_DISPLAY;
import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync.UPDATE_RADIO;
@@ -104,6 +105,11 @@
tempAllIds.add(gnssId);
mPowerStatsInternal.incrementEnergyConsumption(gnssId, 787878);
+ final int cameraId =
+ mPowerStatsInternal.addEnergyConsumer(EnergyConsumerType.CAMERA, 0, "camera");
+ tempAllIds.add(cameraId);
+ mPowerStatsInternal.incrementEnergyConsumption(cameraId, 901234);
+
final int mobileRadioId = mPowerStatsInternal.addEnergyConsumer(
EnergyConsumerType.MOBILE_RADIO, 0, "mobile_radio");
tempAllIds.add(mobileRadioId);
@@ -171,6 +177,12 @@
Arrays.sort(receivedCpuIds);
assertArrayEquals(cpuClusterIds, receivedCpuIds);
+ final EnergyConsumerResult[] cameraResults =
+ mBatteryExternalStatsWorker.getEnergyConsumersLocked(UPDATE_CAMERA).getNow(null);
+ // Results should only have the camera energy consumer
+ assertEquals(1, cameraResults.length);
+ assertEquals(cameraId, cameraResults[0].id);
+
final EnergyConsumerResult[] allResults =
mBatteryExternalStatsWorker.getEnergyConsumersLocked(UPDATE_ALL).getNow(null);
// All energy consumer results should be available
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java b/services/tests/servicestests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java
index e4ab21b..5fce32f0 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java
@@ -36,41 +36,113 @@
public class CameraPowerCalculatorTest {
private static final double PRECISION = 0.00001;
- private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
+ private static final int APP1_UID = Process.FIRST_APPLICATION_UID + 42;
+ private static final int APP2_UID = Process.FIRST_APPLICATION_UID + 43;
@Rule
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
- .setAveragePower(PowerProfile.POWER_CAMERA, 360.0);
+ .setAveragePower(PowerProfile.POWER_CAMERA, 360.0)
+ .initMeasuredEnergyStatsLocked();
@Test
public void testTimerBasedModel() {
BatteryStatsImpl stats = mStatsRule.getBatteryStats();
- stats.noteCameraOnLocked(APP_UID, 1000, 1000);
- stats.noteCameraOffLocked(APP_UID, 2000, 2000);
+ synchronized (stats) { // To keep the GuardedBy check happy
+ stats.noteCameraOnLocked(APP1_UID, 1000, 1000);
+ stats.noteCameraOffLocked(APP1_UID, 2000, 2000);
+ stats.noteCameraOnLocked(APP2_UID, 3000, 3000);
+ stats.noteCameraOffLocked(APP2_UID, 5000, 5000);
+ }
+
+ CameraPowerCalculator calculator =
+ new CameraPowerCalculator(mStatsRule.getPowerProfile());
+
+ mStatsRule.apply(BatteryUsageStatsRule.POWER_PROFILE_MODEL_ONLY, calculator);
+
+ UidBatteryConsumer app1Consumer = mStatsRule.getUidBatteryConsumer(APP1_UID);
+ assertThat(app1Consumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(1000);
+ assertThat(app1Consumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isWithin(PRECISION).of(0.1);
+ assertThat(app1Consumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ UidBatteryConsumer app2Consumer = mStatsRule.getUidBatteryConsumer(APP2_UID);
+ assertThat(app2Consumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(2000);
+ assertThat(app2Consumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isWithin(PRECISION).of(0.2);
+ assertThat(app2Consumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ final BatteryConsumer deviceBatteryConsumer = mStatsRule.getDeviceBatteryConsumer();
+ assertThat(deviceBatteryConsumer
+ .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(3000);
+ assertThat(deviceBatteryConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isWithin(PRECISION).of(0.3);
+ assertThat(deviceBatteryConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ final BatteryConsumer appsBatteryConsumer = mStatsRule.getAppsBatteryConsumer();
+ assertThat(appsBatteryConsumer
+ .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(3000);
+ assertThat(appsBatteryConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isWithin(PRECISION).of(0.3);
+ assertThat(appsBatteryConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+ }
+
+ @Test
+ public void testEnergyConsumptionBasedModel() {
+ BatteryStatsImpl stats = mStatsRule.getBatteryStats();
+ synchronized (stats) { // To keep the GuardedBy check happy
+ stats.noteCameraOnLocked(APP1_UID, 1000, 1000);
+ stats.noteCameraOffLocked(APP1_UID, 2000, 2000);
+ stats.updateCameraEnergyConsumerStatsLocked(720_000, 2100); // 0.72C == 0.2mAh
+ stats.noteCameraOnLocked(APP2_UID, 3000, 3000);
+ stats.noteCameraOffLocked(APP2_UID, 5000, 5000);
+ stats.updateCameraEnergyConsumerStatsLocked(1_080_000, 5100); // 0.3mAh
+ }
CameraPowerCalculator calculator =
new CameraPowerCalculator(mStatsRule.getPowerProfile());
mStatsRule.apply(calculator);
- UidBatteryConsumer consumer = mStatsRule.getUidBatteryConsumer(APP_UID);
- assertThat(consumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ UidBatteryConsumer app1Consumer = mStatsRule.getUidBatteryConsumer(APP1_UID);
+ assertThat(app1Consumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
.isEqualTo(1000);
- assertThat(consumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
- .isWithin(PRECISION).of(0.1);
+ assertThat(app1Consumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isWithin(PRECISION).of(0.2);
+ assertThat(app1Consumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION);
+
+ UidBatteryConsumer app2Consumer = mStatsRule.getUidBatteryConsumer(APP2_UID);
+ assertThat(app2Consumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(2000);
+ assertThat(app2Consumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isWithin(PRECISION).of(0.3);
+ assertThat(app2Consumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION);
final BatteryConsumer deviceBatteryConsumer = mStatsRule.getDeviceBatteryConsumer();
assertThat(deviceBatteryConsumer
.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
- .isEqualTo(1000);
+ .isEqualTo(3000);
assertThat(deviceBatteryConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
- .isWithin(PRECISION).of(0.1);
+ .isWithin(PRECISION).of(0.5);
+ assertThat(deviceBatteryConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION);
final BatteryConsumer appsBatteryConsumer = mStatsRule.getAppsBatteryConsumer();
assertThat(appsBatteryConsumer
.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
- .isEqualTo(1000);
+ .isEqualTo(3000);
assertThat(appsBatteryConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
- .isWithin(PRECISION).of(0.1);
+ .isWithin(PRECISION).of(0.5);
+ assertThat(appsBatteryConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/EnergyConsumerSnapshotTest.java b/services/tests/servicestests/src/com/android/server/power/stats/EnergyConsumerSnapshotTest.java
index 558f396..28f4799 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/EnergyConsumerSnapshotTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/EnergyConsumerSnapshotTest.java
@@ -248,6 +248,32 @@
assertThat(details.toString()).isEqualTo("DISPLAY=2667 HPU=3200000 GPU=0 IPU &_=0");
}
+ @Test
+ public void testUpdateAndGetDelta_updatesCameraCharge() {
+ EnergyConsumer cameraConsumer =
+ createEnergyConsumer(7, 0, EnergyConsumerType.CAMERA, "CAMERA");
+ final EnergyConsumerSnapshot snapshot =
+ new EnergyConsumerSnapshot(createIdToConsumerMap(cameraConsumer));
+
+ // An initial result with only one energy consumer
+ EnergyConsumerResult[] result0 = new EnergyConsumerResult[]{
+ createEnergyConsumerResult(cameraConsumer.id, 60_000, null, null),
+ };
+ snapshot.updateAndGetDelta(result0, VOLTAGE_1);
+
+ // A subsequent result
+ EnergyConsumerResult[] result1 = new EnergyConsumerResult[]{
+ createEnergyConsumerResult(cameraConsumer.id, 90_000, null, null),
+ };
+ EnergyConsumerDeltaData delta = snapshot.updateAndGetDelta(result1, VOLTAGE_1);
+
+ // Verify that the delta between the two results is reported.
+ BatteryStats.EnergyConsumerDetails details = snapshot.getEnergyConsumerDetails(delta);
+ assertThat(details.consumers).hasLength(1);
+ long expectedDeltaUC = calculateChargeConsumedUC(60_000, VOLTAGE_1, 90_000, VOLTAGE_1);
+ assertThat(details.chargeUC[0]).isEqualTo(expectedDeltaUC);
+ }
+
private static EnergyConsumer createEnergyConsumer(int id, int ord, byte type, String name) {
final EnergyConsumer ec = new EnergyConsumer();
ec.id = id;
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java
index 08d08b6..1001422 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java
@@ -74,10 +74,13 @@
// Simulated NTP client behavior: No cached time value available initially, then a
// successful refresh.
NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
- mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1);
+ mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
+ // Simulate the passage of time for realism.
+ mFakeElapsedRealtimeClock.incrementMillis(5000);
+
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
@@ -86,10 +89,9 @@
verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
// Check everything happened that was supposed to.
- long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(
- timeResult, normalPollingIntervalMillis);
+ long expectedDelayMillis = normalPollingIntervalMillis;
verify(mockCallback).scheduleNextRefresh(
- mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
+ timeResult.getElapsedRealtimeMillis() + expectedDelayMillis);
NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult);
verify(mockCallback).submitSuggestion(expectedSuggestion);
@@ -108,6 +110,9 @@
mMockNtpTrustedTime);
for (int i = 0; i < tryAgainTimesMax + 1; i++) {
+ // Simulate the passage of time for realism.
+ mFakeElapsedRealtimeClock.incrementMillis(5000);
+
// Simulated NTP client behavior: No cached time value available and failure to refresh.
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
@@ -140,7 +145,6 @@
mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
int normalPollingIntervalMillis = 7777777;
- int maxTimeResultAgeMillis = normalPollingIntervalMillis;
int shortPollingIntervalMillis = 3333;
int tryAgainTimesMax = 5;
NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
@@ -149,7 +153,7 @@
mMockNtpTrustedTime);
NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
- mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1);
+ mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult);
{
@@ -158,6 +162,9 @@
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
+ // Simulate the passage of time for realism.
+ mFakeElapsedRealtimeClock.incrementMillis(5000);
+
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
@@ -167,17 +174,16 @@
// initially.
verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
- long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(
- timeResult, normalPollingIntervalMillis);
+ long expectedDelayMillis = normalPollingIntervalMillis;
verify(mockCallback).scheduleNextRefresh(
- mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
+ timeResult.getElapsedRealtimeMillis() + expectedDelayMillis);
verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
reset(mMockNtpTrustedTime);
}
// Increment the current time by enough so that an attempt to refresh the time should be
// made every time refreshIfRequiredAndReschedule() is called.
- mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis);
+ mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis);
// Test multiple follow-up calls.
for (int i = 0; i < tryAgainTimesMax + 1; i++) {
@@ -208,30 +214,37 @@
verify(mockCallback, never()).submitSuggestion(any());
reset(mMockNtpTrustedTime);
+
+ // Simulate the passage of time for realism.
+ mFakeElapsedRealtimeClock.incrementMillis(5000);
}
}
@Test
- public void engineImpl_refreshIfRequiredAndReschedule_successFailSuccess() {
+ public void engineImpl_refreshIfRequiredAndReschedule_successThenFail_tryAgainTimesZero() {
mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
int normalPollingIntervalMillis = 7777777;
- int maxTimeResultAgeMillis = normalPollingIntervalMillis;
int shortPollingIntervalMillis = 3333;
- int tryAgainTimesMax = 5;
+ int tryAgainTimesMax = 0;
NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
mFakeElapsedRealtimeClock,
normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
mMockNtpTrustedTime);
- NtpTrustedTime.TimeResult timeResult1 = createNtpTimeResult(
- mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1);
+ NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
+ mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
+ NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult);
+
{
// Simulated NTP client behavior: No cached time value available initially, with a
// successful refresh.
- when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult1);
+ when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
+ // Simulate the passage of time for realism.
+ mFakeElapsedRealtimeClock.incrementMillis(5000);
+
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
@@ -241,10 +254,159 @@
// initially.
verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
- long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(
- timeResult1, normalPollingIntervalMillis);
+ long expectedDelayMillis = normalPollingIntervalMillis;
+ verify(mockCallback).scheduleNextRefresh(
+ timeResult.getElapsedRealtimeMillis() + expectedDelayMillis);
+ verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
+ reset(mMockNtpTrustedTime);
+ }
+
+ // Increment the current time by enough so that an attempt to refresh the time should be
+ // made every time refreshIfRequiredAndReschedule() is called.
+ mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis);
+
+ // Test multiple follow-up calls.
+ for (int i = 0; i < 3; i++) {
+ // Simulated NTP client behavior: (Too old) cached time value available, unsuccessful
+ // refresh.
+ when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
+ when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
+
+ RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
+
+ // Trigger the engine's logic.
+ engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
+
+ // Expect a refresh attempt each time as the cached network time is too old.
+ verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
+
+ // Check the scheduling. tryAgainTimesMax == 0, so the algorithm should start with
+
+ long expectedDelayMillis = normalPollingIntervalMillis;
verify(mockCallback).scheduleNextRefresh(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
+
+ // No valid time, no suggestion.
+ verify(mockCallback, never()).submitSuggestion(any());
+
+ reset(mMockNtpTrustedTime);
+
+ // Simulate the passage of time for realism.
+ mFakeElapsedRealtimeClock.incrementMillis(5000);
+ }
+ }
+
+ @Test
+ public void engineImpl_refreshIfRequiredAndReschedule_successThenFail_tryAgainTimesNegative() {
+ mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
+
+ int normalPollingIntervalMillis = 7777777;
+ int shortPollingIntervalMillis = 3333;
+ int tryAgainTimesMax = -1;
+ NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
+ mFakeElapsedRealtimeClock,
+ normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
+ mMockNtpTrustedTime);
+
+ NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
+ mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
+ NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult);
+
+ {
+ // Simulated NTP client behavior: No cached time value available initially, with a
+ // successful refresh.
+ when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult);
+ when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
+
+ // Simulate the passage of time for realism.
+ mFakeElapsedRealtimeClock.incrementMillis(5000);
+
+ RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
+
+ // Trigger the engine's logic.
+ engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
+
+ // Expect the refresh attempt to have been made: there is no cached network time
+ // initially.
+ verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
+
+ long expectedDelayMillis = normalPollingIntervalMillis;
+ verify(mockCallback).scheduleNextRefresh(
+ timeResult.getElapsedRealtimeMillis() + expectedDelayMillis);
+ verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
+ reset(mMockNtpTrustedTime);
+ }
+
+ // Increment the current time by enough so that an attempt to refresh the time should be
+ // made every time refreshIfRequiredAndReschedule() is called.
+ mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis);
+
+ // Test multiple follow-up calls.
+ for (int i = 0; i < 3; i++) {
+ // Simulated NTP client behavior: (Too old) cached time value available, unsuccessful
+ // refresh.
+ when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
+ when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
+
+ RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
+
+ // Trigger the engine's logic.
+ engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
+
+ // Expect a refresh attempt each time as the cached network time is too old.
+ verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
+
+ // Check the scheduling. tryAgainTimesMax == -1, so it should always be
+ // shortPollingIntervalMillis.
+ long expectedDelayMillis = shortPollingIntervalMillis;
+ verify(mockCallback).scheduleNextRefresh(
+ mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
+
+ // No valid time, no suggestion.
+ verify(mockCallback, never()).submitSuggestion(any());
+
+ reset(mMockNtpTrustedTime);
+
+ // Simulate the passage of time for realism.
+ mFakeElapsedRealtimeClock.incrementMillis(5000);
+ }
+ }
+
+ @Test
+ public void engineImpl_refreshIfRequiredAndReschedule_successFailSuccess() {
+ mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
+
+ int normalPollingIntervalMillis = 7777777;
+ int shortPollingIntervalMillis = 3333;
+ int tryAgainTimesMax = 5;
+ NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
+ mFakeElapsedRealtimeClock,
+ normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
+ mMockNtpTrustedTime);
+
+ NtpTrustedTime.TimeResult timeResult1 = createNtpTimeResult(
+ mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
+ {
+ // Simulated NTP client behavior: No cached time value available initially, with a
+ // successful refresh.
+ when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult1);
+ when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
+
+ // Simulate the passage of time for realism.
+ mFakeElapsedRealtimeClock.incrementMillis(5000);
+
+ RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
+
+ // Trigger the engine's logic.
+ engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
+
+ // Expect the refresh attempt to have been made: there is no cached network time
+ // initially.
+ verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
+
+ long expectedDelayMillis = normalPollingIntervalMillis;
+ verify(mockCallback).scheduleNextRefresh(
+ timeResult1.getElapsedRealtimeMillis() + expectedDelayMillis);
NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult1);
verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
reset(mMockNtpTrustedTime);
@@ -253,7 +415,7 @@
// Increment the current time by enough so that the cached time result is too old and an
// attempt to refresh the time should be made every time refreshIfRequiredAndReschedule() is
// called.
- mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis);
+ mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis);
{
// Simulated NTP client behavior: (Old) cached time value available initially, with an
@@ -278,8 +440,11 @@
reset(mMockNtpTrustedTime);
}
+ // Increment time enough to avoid the minimum refresh interval protection.
+ mFakeElapsedRealtimeClock.incrementMillis(shortPollingIntervalMillis);
+
NtpTrustedTime.TimeResult timeResult2 = createNtpTimeResult(
- mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1);
+ mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
{
// Simulated NTP client behavior: (Old) cached time value available initially, with a
@@ -287,6 +452,9 @@
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult1, timeResult2);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
+ // Simulate the passage of time for realism.
+ mFakeElapsedRealtimeClock.incrementMillis(5000);
+
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
@@ -295,10 +463,9 @@
// Expect the refresh attempt to have been made: the timeResult is too old.
verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
- long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(
- timeResult2, normalPollingIntervalMillis);
+ long expectedDelayMillis = normalPollingIntervalMillis;
verify(mockCallback).scheduleNextRefresh(
- mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
+ timeResult2.getElapsedRealtimeMillis() + expectedDelayMillis);
NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult2);
verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
reset(mMockNtpTrustedTime);
@@ -311,11 +478,10 @@
* A suggestion will still be made.
*/
@Test
- public void engineImpl_refreshIfRequiredAndReschedule_noRefreshIfLatestIsNotTooOld() {
+ public void engineImpl_refreshIfRequiredAndReschedule_noRefreshIfLatestIsFresh() {
mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
int normalPollingIntervalMillis = 7777777;
- int maxTimeResultAgeMillis = normalPollingIntervalMillis;
int shortPollingIntervalMillis = 3333;
int tryAgainTimesMax = 5;
NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
@@ -323,12 +489,12 @@
normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
mMockNtpTrustedTime);
- // Simulated NTP client behavior: A cached time value is available, increment the clock, but
- // not enough to consider the cached value too old.
+ // Simulated NTP client behavior: A cached time value is available.
NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
- mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis - 1);
+ // Increment the clock, but not enough to consider the cached value too old.
+ mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis - 1);
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
@@ -339,10 +505,9 @@
// The next wake-up should be rescheduled for when the cached time value will become too
// old.
- long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(timeResult,
- normalPollingIntervalMillis);
+ long expectedDelayMillis = normalPollingIntervalMillis;
verify(mockCallback).scheduleNextRefresh(
- mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
+ timeResult.getElapsedRealtimeMillis() + expectedDelayMillis);
// Suggestions must be made every time if the cached time value is not too old in case it
// was refreshed by a different component.
@@ -352,15 +517,13 @@
/**
* Confirms that if a refreshIfRequiredAndReschedule() call is made, e.g. for reasons besides
- * scheduled alerts, and the latest time is not too old, then an NTP refresh won't be attempted.
- * A suggestion will still be made.
+ * scheduled alerts, and the latest time is too old, then an NTP refresh will be attempted.
*/
@Test
public void engineImpl_refreshIfRequiredAndReschedule_failureHandlingAfterLatestIsTooOld() {
mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
int normalPollingIntervalMillis = 7777777;
- int maxTimeResultAgeMillis = normalPollingIntervalMillis;
int shortPollingIntervalMillis = 3333;
int tryAgainTimesMax = 5;
NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
@@ -373,7 +536,7 @@
NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
- mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis);
+ mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
@@ -392,11 +555,87 @@
verify(mockCallback, never()).submitSuggestion(any());
}
- private long calculateRefreshDelayMillisForTimeResult(NtpTrustedTime.TimeResult timeResult,
- int normalPollingIntervalMillis) {
- long currentElapsedRealtimeMillis = mFakeElapsedRealtimeClock.getElapsedRealtimeMillis();
- long timeResultAgeMillis = timeResult.getAgeMillis(currentElapsedRealtimeMillis);
- return normalPollingIntervalMillis - timeResultAgeMillis;
+ /**
+ * Confirms that if a refreshIfRequiredAndReschedule() call is made and there was a recently
+ * failed refresh, then another won't be scheduled too soon.
+ */
+ @Test
+ public void engineImpl_refreshIfRequiredAndReschedule_minimumRefreshTimeEnforced() {
+ mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
+
+ int normalPollingIntervalMillis = 7777777;
+ int shortPollingIntervalMillis = 3333;
+ int tryAgainTimesMax = 0;
+ NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
+ mFakeElapsedRealtimeClock,
+ normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
+ mMockNtpTrustedTime);
+
+ NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
+ mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
+
+ // Simulate an initial call to refreshIfRequiredAndReschedule() prime the "last refresh
+ // attempt" time. A cached time value is available, but it's too old but the refresh
+ // attempt will fail.
+ long lastRefreshAttemptElapsedMillis;
+ {
+ // Increment the clock, enough to consider the cached value too old.
+ mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis);
+ when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
+ when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
+
+ RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
+ // Trigger the engine's logic.
+ engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
+
+ // Expect a refresh attempt to have been made.
+ verify(mMockNtpTrustedTime, times(1)).forceRefresh(mDummyNetwork);
+ lastRefreshAttemptElapsedMillis = mFakeElapsedRealtimeClock.getElapsedRealtimeMillis();
+
+ // The next wake-up should be rescheduled using the normalPollingIntervalMillis.
+ // Because the time signal age > normalPollingIntervalMillis, the last refresh attempt
+ // time will be used.
+ long expectedDelayMillis = normalPollingIntervalMillis;
+ long expectedNextRefreshElapsedMillis =
+ lastRefreshAttemptElapsedMillis + expectedDelayMillis;
+ verify(mockCallback).scheduleNextRefresh(expectedNextRefreshElapsedMillis);
+
+ // Suggestions should not be made if the cached time value is too old.
+ verify(mockCallback, never()).submitSuggestion(any());
+
+ reset(mMockNtpTrustedTime);
+ }
+
+ // Simulate a second call to refreshIfRequiredAndReschedule() very soon after the first, as
+ // might happen if there were a network state change.
+ // The cached time value is available, but it's still too old. Because the last call was so
+ // recent, no refresh should take place and the next scheduled refresh time should be
+ // set appropriately based on the last attempt.
+ {
+ // Increment the clock by a relatively small amount so that it's considered "too soon".
+ mFakeElapsedRealtimeClock.incrementMillis(shortPollingIntervalMillis / 2);
+
+ when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
+
+ RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
+ // Trigger the engine's logic.
+ engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
+
+ // Expect no refresh attempt to have been made: time elapsed isn't enough.
+ verify(mMockNtpTrustedTime, never()).forceRefresh(any());
+
+ // The next wake-up should be rescheduled using the normal polling interval and the last
+ // refresh attempt time.
+ long expectedDelayMillis = normalPollingIntervalMillis;
+ long expectedNextRefreshElapsedMillis =
+ lastRefreshAttemptElapsedMillis + expectedDelayMillis;
+ verify(mockCallback).scheduleNextRefresh(expectedNextRefreshElapsedMillis);
+
+ // Suggestions should not be made if the cached time value is too old.
+ verify(mockCallback, never()).submitSuggestion(any());
+
+ reset(mMockNtpTrustedTime);
+ }
}
private static NetworkTimeSuggestion createExpectedSuggestion(
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index a76b82b..fd1ca68 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -407,7 +407,7 @@
void assertShowRecentApps() {
waitForIdle();
- verify(mStatusBarManagerInternal).showRecentApps(anyBoolean());
+ verify(mStatusBarManagerInternal).showRecentApps(anyBoolean(), anyBoolean());
}
void assertSwitchKeyboardLayout() {
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 113f5ec..3eb7fe3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -295,6 +295,15 @@
assertEquals(RESTARTING_PROCESS, mActivity.getState());
assertNotEquals(originalOverrideBounds, mActivity.getBounds());
+
+ // Even if the state is changed (e.g. a floating activity on top is finished and make it
+ // resume), the restart procedure should recover the state and continue to kill the process.
+ mActivity.setState(RESUMED, "anyStateChange");
+ doReturn(true).when(mSupervisor).hasScheduledRestartTimeouts(mActivity);
+ mAtm.mActivityClientController.activityStopped(mActivity.token, null /* icicle */,
+ null /* persistentState */, null /* description */);
+ assertEquals(RESTARTING_PROCESS, mActivity.getState());
+ verify(mSupervisor).removeRestartTimeouts(mActivity);
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index a405e5a..035d73d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -545,6 +545,7 @@
mTransaction.reparentActivityToTaskFragment(mFragmentToken, mock(IBinder.class));
mTransaction.setAdjacentTaskFragments(mFragmentToken, mock(IBinder.class),
null /* options */);
+ mTransaction.clearAdjacentTaskFragments(mFragmentToken);
assertApplyTransactionAllowed(mTransaction);
// Successfully created a TaskFragment.
@@ -619,12 +620,16 @@
// Not allowed because TaskFragments are not organized by the caller organizer.
assertApplyTransactionDisallowed(mTransaction);
+ assertNull(mTaskFragment.getAdjacentTaskFragment());
+ assertNull(taskFragment2.getAdjacentTaskFragment());
mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */,
"Test:TaskFragmentOrganizer" /* processName */);
// Not allowed because TaskFragment2 is not organized by the caller organizer.
assertApplyTransactionDisallowed(mTransaction);
+ assertNull(mTaskFragment.getAdjacentTaskFragment());
+ assertNull(taskFragment2.getAdjacentTaskFragment());
mTaskFragment.onTaskFragmentOrganizerRemoved();
taskFragment2.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */,
@@ -632,11 +637,46 @@
// Not allowed because mTaskFragment is not organized by the caller organizer.
assertApplyTransactionDisallowed(mTransaction);
+ assertNull(mTaskFragment.getAdjacentTaskFragment());
+ assertNull(taskFragment2.getAdjacentTaskFragment());
mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */,
"Test:TaskFragmentOrganizer" /* processName */);
assertApplyTransactionAllowed(mTransaction);
+ assertEquals(taskFragment2, mTaskFragment.getAdjacentTaskFragment());
+ }
+
+ @Test
+ public void testApplyTransaction_enforceTaskFragmentOrganized_clearAdjacentTaskFragments() {
+ final Task task = createTask(mDisplayContent);
+ mTaskFragment = new TaskFragmentBuilder(mAtm)
+ .setParentTask(task)
+ .setFragmentToken(mFragmentToken)
+ .build();
+ mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
+ final IBinder fragmentToken2 = new Binder();
+ final TaskFragment taskFragment2 = new TaskFragmentBuilder(mAtm)
+ .setParentTask(task)
+ .setFragmentToken(fragmentToken2)
+ .build();
+ mWindowOrganizerController.mLaunchTaskFragments.put(fragmentToken2, taskFragment2);
+ mTaskFragment.setAdjacentTaskFragment(taskFragment2);
+
+ mTransaction.clearAdjacentTaskFragments(mFragmentToken);
+ mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE,
+ false /* shouldApplyIndependently */);
+
+ // Not allowed because TaskFragment is not organized by the caller organizer.
+ assertApplyTransactionDisallowed(mTransaction);
+ assertEquals(taskFragment2, mTaskFragment.getAdjacentTaskFragment());
+
+ mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */,
+ "Test:TaskFragmentOrganizer" /* processName */);
+
+ assertApplyTransactionAllowed(mTransaction);
+ assertNull(mTaskFragment.getAdjacentTaskFragment());
+ assertNull(taskFragment2.getAdjacentTaskFragment());
}
@Test
@@ -1016,8 +1056,7 @@
spyOn(mWindowOrganizerController);
// Not allow to set adjacent on a TaskFragment that is in a PIP Task.
- mTransaction.setAdjacentTaskFragments(mFragmentToken, null /* fragmentToken2 */,
- null /* options */)
+ mTransaction.setAdjacentTaskFragments(mFragmentToken, new Binder(), null /* options */)
.setErrorCallbackToken(mErrorToken);
assertApplyTransactionAllowed(mTransaction);
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 9643282..13c0f17 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -251,11 +251,29 @@
@Nullable String attributionTag,
@Nullable IVoiceInteractionSessionShowCallback showCallback,
@Nullable IBinder activityToken) {
+ try {
+ if (mService != null) {
+ mService.prepareToShowSession(args, flags);
+ }
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException while calling prepareToShowSession", e);
+ }
+
if (mActiveSession == null) {
mActiveSession = new VoiceInteractionSessionConnection(mServiceStub,
mSessionComponentName, mUser, mContext, this,
mInfo.getServiceInfo().applicationInfo.uid, mHandler);
}
+ if (!mActiveSession.mBound) {
+ try {
+ if (mService != null) {
+ mService.showSessionFailed();
+ }
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException while calling showSessionFailed", e);
+ }
+ }
+
List<ActivityAssistInfo> allVisibleActivities =
LocalServices.getService(ActivityTaskManagerInternal.class)
.getTopVisibleActivities();
diff --git a/telephony/java/android/telephony/ModemActivityInfo.java b/telephony/java/android/telephony/ModemActivityInfo.java
index 2d0135a..64b3c0a 100644
--- a/telephony/java/android/telephony/ModemActivityInfo.java
+++ b/telephony/java/android/telephony/ModemActivityInfo.java
@@ -567,14 +567,14 @@
/** @hide */
@TestApi
public boolean isEmpty() {
- boolean isTxPowerEmpty = false;
- boolean isRxPowerEmpty = false;
+ boolean isTxPowerEmpty = true;
+ boolean isRxPowerEmpty = true;
for (int i = 0; i < getSpecificInfoLength(); i++) {
- if (mActivityStatsTechSpecificInfo[i].isTxPowerEmpty()) {
- isTxPowerEmpty = true;
+ if (!mActivityStatsTechSpecificInfo[i].isTxPowerEmpty()) {
+ isTxPowerEmpty = false;
}
- if (mActivityStatsTechSpecificInfo[i].isRxPowerEmpty()) {
- isRxPowerEmpty = true;
+ if (!mActivityStatsTechSpecificInfo[i].isRxPowerEmpty()) {
+ isRxPowerEmpty = false;
}
}
return isTxPowerEmpty
diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java
index a2a110d..26b4bbc 100644
--- a/telephony/java/android/telephony/SmsManager.java
+++ b/telephony/java/android/telephony/SmsManager.java
@@ -46,6 +46,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.telephony.IIntegerConsumer;
+import com.android.internal.telephony.IPhoneSubInfo;
import com.android.internal.telephony.ISms;
import com.android.internal.telephony.ITelephony;
import com.android.internal.telephony.SmsRawData;
@@ -3508,4 +3509,41 @@
private static String formatCrossStackMessageId(long id) {
return "{x-message-id:" + id + "}";
}
+
+ /**
+ * Fetches the EF_PSISMSC value from the UICC that contains the Public Service Identity of
+ * the SM-SC (either a SIP URI or tel URI). The EF_PSISMSC of ISIM and USIM can be found in
+ * DF_TELECOM.
+ * The EF_PSISMSC value is used by the ME to submit SMS over IP as defined in 24.341 [55].
+ *
+ * @return Uri : Public Service Identity of SM-SC from the ISIM or USIM if the ISIM is not
+ * available.
+ * @throws SecurityException if the caller does not have the required permission/privileges.
+ * @throws IllegalStateException in case of telephony service is not available.
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
+ public Uri getSmscIdentity() {
+ Uri smscUri = Uri.EMPTY;
+ try {
+ IPhoneSubInfo info = TelephonyManager.getSubscriberInfoService();
+ if (info == null) {
+ Rlog.e(TAG, "getSmscIdentity(): IPhoneSubInfo instance is NULL");
+ throw new IllegalStateException("Telephony service is not available");
+ }
+ /** Fetches the SIM EF_PSISMSC value based on subId and appType */
+ smscUri = info.getSmscIdentity(getSubscriptionId(), TelephonyManager.APPTYPE_ISIM);
+ if (Uri.EMPTY.equals(smscUri)) {
+ /** Fallback in case where ISIM is not available */
+ smscUri = info.getSmscIdentity(getSubscriptionId(), TelephonyManager.APPTYPE_USIM);
+ }
+ } catch (RemoteException ex) {
+ Rlog.e(TAG, "getSmscIdentity(): Exception : " + ex);
+ ex.rethrowAsRuntimeException();
+ }
+ return smscUri;
+ }
}
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 0ad5ba0..4812806 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -17715,37 +17715,6 @@
}
/**
- * Fetches the EFPSISMSC value from the SIM that contains the Public Service Identity
- * of the SM-SC (either a SIP URI or tel URI), the value is common for both appType
- * {@link #APPTYPE_ISIM} and {@link #APPTYPE_SIM}.
- * The EFPSISMSC value is used by the ME to submit SMS over IP as defined in 24.341 [55].
- *
- * @param appType ICC Application type {@link #APPTYPE_ISIM} or {@link #APPTYPE_USIM}
- * @return SIP URI or tel URI of the Public Service Identity of the SM-SC
- * @throws SecurityException if the caller does not have the required permission/privileges
- * @hide
- */
- @NonNull
- @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
- @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
- public String getSmscIdentity(int appType) {
- try {
- IPhoneSubInfo info = getSubscriberInfoService();
- if (info == null) {
- Rlog.e(TAG, "getSmscIdentity(): IPhoneSubInfo instance is NULL");
- return null;
- }
- /** Fetches the SIM PSISMSC params based on subId and appType */
- return info.getSmscIdentity(getSubId(), appType);
- } catch (RemoteException ex) {
- Rlog.e(TAG, "getSmscIdentity(): RemoteException: " + ex.getMessage());
- } catch (NullPointerException ex) {
- Rlog.e(TAG, "getSmscIdentity(): NullPointerException: " + ex.getMessage());
- }
- return null;
- }
-
- /**
* Returns a constant indicating the state of sim for the slot index.
*
* @param slotIndex Logical SIM slot index.
diff --git a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl
index 4fa7f43..3dfc81e 100644
--- a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl
+++ b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl
@@ -17,6 +17,7 @@
package com.android.internal.telephony;
import android.telephony.ImsiEncryptionInfo;
+import android.net.Uri;
/**
* Interface used to retrieve various phone-related subscriber information.
@@ -220,18 +221,17 @@
String callingPackage, String callingFeatureId);
/**
- * Fetches the EFPSISMSC value from the SIM that contains the Public Service Identity
- * of the SM-SC (either a SIP URI or tel URI), the value is common for both appType
- * {@link #APPTYPE_ISIM} and {@link #APPTYPE_SIM}.
- * The EFPSISMSC value is used by the ME to submit SMS over IP as defined in 24.341 [55].
+ * Fetches the EF_PSISMSC value from the UICC that contains the Public Service Identity of
+ * the SM-SC (either a SIP URI or tel URI). The EF_PSISMSC of ISIM and USIM can be found in
+ * DF_TELECOM.
+ * The EF_PSISMSC value is used by the ME to submit SMS over IP as defined in 24.341 [55].
*
- * @param appType ICC Application type {@link #APPTYPE_ISIM} or {@link #APPTYPE_USIM}
- * @return SIP URI or tel URI of the Public Service Identity of the SM-SC
+ * @return Uri : Public Service Identity of SM-SC
* @throws SecurityException if the caller does not have the required permission/privileges
* @hide
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)")
- String getSmscIdentity(int subId, int appType);
+ Uri getSmscIdentity(int subId, int appType);
/**
* Fetches the sim service table from the EFUST/EFIST based on the application type
@@ -249,9 +249,9 @@
* @param appType of type int of either {@link #APPTYPE_USIM} or {@link #APPTYPE_ISIM}.
* @return HexString represents sim service table else null.
* @throws SecurityException if the caller does not have the required permission/privileges
+ * @throws IllegalStateException in case if phone or UiccApplication is not available.
* @hide
*/
-
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)")
String getSimServiceTable(int subId, int appType);
}
diff --git a/tests/Internal/src/com/android/internal/app/OWNERS b/tests/Internal/src/com/android/internal/app/OWNERS
new file mode 100644
index 0000000..d55dc78
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/app/OWNERS
@@ -0,0 +1,2 @@
+# Locale related test
+per-file *Locale* = file:/services/core/java/com/android/server/locales/OWNERS
diff --git a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java
index ef324e7..6c89e49 100644
--- a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java
+++ b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java
@@ -1156,12 +1156,12 @@
private PendingIntent makeIntent() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
- return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
+ return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
}
private PendingIntent makeIntent2() {
Intent intent = new Intent(this, StatusBarTest.class);
- return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
+ return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
}