diff options
389 files changed, 12288 insertions, 3541 deletions
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java index 18ddffbaf885..86e61832611e 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 @@ public class JobParameters implements Parcelable { * @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 33668c76c152..4aec484aad09 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 @@ public abstract class JobScheduler { * 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 @@ public abstract class JobScheduler { * 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 class JobScheduler { 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 @@ public abstract class JobScheduler { * 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 ce7da8607497..b89337f8fd4e 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 @@ public final class JobServiceContext implements ServiceConnection { 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 0627709f704f..a9b9476d6c10 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -745,6 +745,7 @@ package android { 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 @@ package android { 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 @@ package android { 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 @@ package android.app.job { 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 @@ package android.app.job { 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 @@ package android.content.pm { 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 @@ package android.content.pm { 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 @@ package android.content.pm { 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 @@ package android.content.pm { 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 @@ package android.credentials { 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 @@ package android.hardware.camera2 { 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 @@ package android.hardware.camera2 { 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 @@ package android.nfc { 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 @@ package android.nfc { 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 @@ package android.provider { 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 @@ package android.service.voice { 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 @@ package android.view { 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 @@ package android.view { 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 @@ package android.view { 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 @@ package android.view { 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 @@ package android.widget { 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 @@ package android.widget { 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 @@ package android.widget { 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 @@ package android.widget { 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 @@ package android.widget { 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 @@ package android.widget { 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 3216bd12118b..55ef6de47aee 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -82,6 +82,7 @@ package android.content { } 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 1fa1e89fb46e..5c4fd10fc05b 100644 --- a/core/api/removed.txt +++ b/core/api/removed.txt @@ -252,6 +252,34 @@ package android.net { } +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 85f88135510b..33ea8e1e0991 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -9668,18 +9668,14 @@ package android.nfc { 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 @@ package android.telephony { 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 2c5acf182d51..1c10356c6b03 100644 --- a/core/api/system-removed.txt +++ b/core/api/system-removed.txt @@ -140,6 +140,17 @@ package android.media.tv { } +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 d25c3f5bf538..84ac868c7e17 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3206,6 +3206,14 @@ package android.view.animation { 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 @@ package android.view.autofill { } 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 @@ package android.window { 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 @@ package android.window { 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 3f279644720b..421ba7cb63eb 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -2640,7 +2640,7 @@ public final class ActivityThread extends ClientTransactionHandler 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 563f6d4d9544..a14f3d35fa48 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -2489,7 +2489,7 @@ public class AppOpsManager { "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 befe833a6a11..b91fa35942bb 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -2582,6 +2582,22 @@ class ContextImpl extends Context { } @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 @@ class ContextImpl extends Context { 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 f0c39ab0dc26..c19a8652c98d 100644 --- a/core/java/android/app/ForegroundServiceTypePolicy.java +++ b/core/java/android/app/ForegroundServiceTypePolicy.java @@ -259,12 +259,15 @@ public abstract class ForegroundServiceTypePolicy { 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 f2eced309b7d..20869e01c012 100644 --- a/core/java/android/app/OWNERS +++ b/core/java/android/app/OWNERS @@ -47,6 +47,9 @@ per-file *Alarm* = file:/apex/jobscheduler/OWNERS # 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 ad27b33835a3..e48539791cc1 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -1080,7 +1080,6 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac 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 0ec825371515..270cb1841464 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 fb00fe506ed1..6ce25cc42876 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 3ad315753a29..f7216e7acd79 100644 --- a/core/java/android/app/admin/PolicyUpdatesReceiver.java +++ b/core/java/android/app/admin/PolicyUpdatesReceiver.java @@ -21,7 +21,6 @@ import android.annotation.IntDef; 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 abstract class PolicyUpdatesReceiver extends BroadcastReceiver { 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 abstract class PolicyUpdatesReceiver extends BroadcastReceiver { 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 000000000000..c59a9dc51aa7 --- /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 82afdddf288b..203334311da9 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 000000000000..260b672788d1 --- /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 000000000000..6a67b7f60ad0 --- /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 74c3f45d7f6e..c8d48c189247 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -6859,6 +6859,26 @@ public abstract class Context { @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 0dc4adc79f30..e65e91c45396 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -1073,6 +1073,15 @@ public class ContextWrapper extends Context { /** @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 9dd9c0f757d5..9c1318ee52da 100644 --- a/core/java/android/content/pm/IPackageInstallerSession.aidl +++ b/core/java/android/content/pm/IPackageInstallerSession.aidl @@ -64,7 +64,7 @@ interface IPackageInstallerSession { 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 df1340d318d8..0b74dd1a267c 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -2010,9 +2010,9 @@ public class PackageInstaller { * @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 @@ public class PackageInstaller { /** {@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 @@ public class PackageInstaller { rollbackDataPolicy = source.readInt(); requireUserAction = source.readInt(); packageSource = source.readInt(); - keepApplicationEnabledSetting = source.readBoolean(); + applicationEnabledSettingPersistent = source.readBoolean(); } /** {@hide} */ @@ -2341,7 +2341,7 @@ public class PackageInstaller { ret.rollbackDataPolicy = rollbackDataPolicy; ret.requireUserAction = requireUserAction; ret.packageSource = packageSource; - ret.keepApplicationEnabledSetting = keepApplicationEnabledSetting; + ret.applicationEnabledSettingPersistent = applicationEnabledSettingPersistent; return ret; } @@ -2839,8 +2839,8 @@ public class PackageInstaller { * 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 @@ public class PackageInstaller { 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 @@ public class PackageInstaller { 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 class PackageInstaller { public boolean isPreapprovalRequested; /** @hide */ - public boolean keepApplicationEnabledSetting; + public boolean applicationEnabledSettingPersistent; /** @hide */ public int pendingUserActionReason; @@ -3197,7 +3198,7 @@ public class PackageInstaller { requireUserAction = source.readInt(); installerUid = source.readInt(); packageSource = source.readInt(); - keepApplicationEnabledSetting = source.readBoolean(); + applicationEnabledSettingPersistent = source.readBoolean(); pendingUserActionReason = source.readInt(); } @@ -3732,8 +3733,8 @@ public class PackageInstaller { * 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 @@ public class PackageInstaller { 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 fe063662db0d..900454d59e50 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1552,7 +1552,7 @@ public abstract class PackageManager { * * @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 4e2acc036386..0b503eb06c3c 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -214,12 +214,15 @@ public class ServiceInfo extends ComponentInfo * {@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 @@ public class ServiceInfo extends ComponentInfo 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 576da8b6184a..4f8b026ccb83 100644 --- a/core/java/android/credentials/GetCredentialResponse.java +++ b/core/java/android/credentials/GetCredentialResponse.java @@ -19,7 +19,6 @@ package android.credentials; 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 @@ public final class GetCredentialResponse implements Parcelable { /** * 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 @@ public final class GetCredentialResponse implements Parcelable { 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 1542d61fb54f..21fead980cdd 100644 --- a/core/java/android/hardware/camera2/CameraExtensionSession.java +++ b/core/java/android/hardware/camera2/CameraExtensionSession.java @@ -19,10 +19,7 @@ package android.hardware.camera2; 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 @@ public abstract class CameraExtensionSession implements AutoCloseable { } /** - * 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 @@ public abstract class CameraExtensionSession implements AutoCloseable { * 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 b5a58b2f9456..e2dedd691407 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -2342,6 +2342,15 @@ public final class CameraManager { 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 9437ea76180a..709fa609a96a 100644 --- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java @@ -361,7 +361,7 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes } @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 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes 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 ed48a6dc25be..3f85d44b3b53 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java @@ -513,7 +513,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { } @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 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { 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 58f7759551e8..0311da4b645f 100644 --- a/core/java/android/hardware/input/KeyboardLayout.java +++ b/core/java/android/hardware/input/KeyboardLayout.java @@ -23,6 +23,7 @@ import android.os.Parcelable; import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * Describes a keyboard layout. @@ -30,6 +31,37 @@ import java.util.Map; * @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 @@ public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayo /** 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 @@ public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayo // 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 @@ public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayo + ", 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 6a40f98fe21c..000000000000 --- 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 133146de2aa1..b06bf06d5197 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 @@ import android.nfc.Tag; */ 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 de107a2c7c70..f6aa4b44d6cd 100644 --- a/core/java/android/nfc/INfcAdapter.aidl +++ b/core/java/android/nfc/INfcAdapter.aidl @@ -18,7 +18,6 @@ package android.nfc; 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 @@ interface INfcAdapter 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 911aaf317e3e..8d75cac531fb 100644 --- a/core/java/android/nfc/NfcActivityManager.java +++ b/core/java/android/nfc/NfcActivityManager.java @@ -19,9 +19,6 @@ package android.nfc; 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 @@ public final class NfcActivityManager extends IAppCallback.Stub 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 @@ public final class NfcActivityManager extends IAppCallback.Stub 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 final class NfcActivityManager extends IAppCallback.Stub } } - 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 @@ public final class NfcActivityManager extends IAppCallback.Stub } } - /** 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 0c7f52999513..c4b3c220b925 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -338,7 +338,10 @@ public final class NfcAdapter { */ 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 @@ public final class NfcAdapter { // 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 @@ public final class NfcAdapter { * 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 @@ public final class NfcAdapter { * 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 @@ public final class NfcAdapter { /** - * @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 @@ public final class NfcAdapter { } /** - * 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 final class NfcAdapter { 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 @@ public final class NfcAdapter { * @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 @@ public final class NfcAdapter { 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 @@ public final class NfcAdapter { * @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 @@ public final class NfcAdapter { 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 @@ public final class NfcAdapter { * 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 @@ public final class NfcAdapter { 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 @@ public final class NfcAdapter { throw new UnsupportedOperationException(); } } - if (activity == null) { - throw new NullPointerException("activity cannot be null"); - } - mNfcActivityManager.setNdefPushMessage(activity, message, flags); } /** @@ -1414,7 +1341,7 @@ public final class NfcAdapter { * 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,47 +1351,10 @@ public final class NfcAdapter { 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); - } - - /** * Set a callback on successful Android Beam (TM). * * <p>This method may be called at any time before {@link Activity#onDestroy}, @@ -1501,7 +1391,7 @@ public final class NfcAdapter { * 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 @@ public final class NfcAdapter { 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 @@ public final class NfcAdapter { * @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 @@ public final class NfcAdapter { 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 @@ public final class NfcAdapter { * * @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 @@ public final class NfcAdapter { 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 @@ public final class NfcAdapter { * <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 @@ public final class NfcAdapter { 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 @@ public final class NfcAdapter { * 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 @@ public final class NfcAdapter { * @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 @@ public final class NfcAdapter { } /** - * @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 34aa7efb2d3b..547d406751f9 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -1142,6 +1142,16 @@ public abstract class BatteryStats { @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 class BatteryStats { 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 249f48608d40..32773a027ff1 100755 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -754,12 +754,9 @@ public class Build { * 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 cdde18aed604..adc73c882af0 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -395,19 +395,6 @@ public final class Trace { } /** - * @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 ec3ef9de5c7d..76ca20cb4718 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -603,6 +603,8 @@ public final class Settings { * 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 @@ public final class Settings { * 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 48923126117a..13f7e5d4232b 100644 --- a/core/java/android/security/net/config/SystemCertificateSource.java +++ b/core/java/android/security/net/config/SystemCertificateSource.java @@ -41,7 +41,7 @@ public final class SystemCertificateSource extends DirectoryCertificateSource { 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 24819a6785fb..efae5c1569a7 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 @@ oneway interface IVoiceInteractionService { 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 9e1518d899e0..d49b5a5f3c6e 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -160,6 +160,20 @@ public class VoiceInteractionService extends Service { 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 @@ public class VoiceInteractionService extends Service { } /** + * 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 b91019c01610..84bbdd1266d0 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -1283,6 +1283,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { * 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 @@ public final class MotionEvent extends InputEvent implements Parcelable { * <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 final class MotionEvent extends InputEvent implements Parcelable { */ 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 @@ public final class MotionEvent extends InputEvent implements Parcelable { 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 final class MotionEvent extends InputEvent implements Parcelable { */ 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 01984571a8f4..c73cfc22104b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -141,6 +141,7 @@ import android.view.accessibility.AccessibilityWindowInfo; 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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * 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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, 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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, 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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * 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 2114ce7d7ffa..74e521afdddb 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -8823,6 +8823,10 @@ public final class ViewRootImpl implements ViewParent, 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 000000000000..59ad15139de1 --- /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 a92bc945c004..58e7a70b155c 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -60,7 +60,6 @@ import android.os.Looper; 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 @@ public final class AutofillManager { @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 final class AutofillManager { */ 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 @@ public final class AutofillManager { 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 @@ public final class AutofillManager { } } - 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 @@ public final class AutofillManager { } /** - * 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 @@ public final class AutofillManager { 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 @@ public final class AutofillManager { } } + @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 4005bc86af39..d6c2d301443b 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -937,6 +937,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ 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 class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + addSearchHighlightPaths(); + if (hasGesturePreviewHighlight()) { final Path path; if (mPathRecyclePool.isEmpty()) { @@ -8381,6 +8649,67 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 3272c3bc0d09..0d6a58b166ae 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -58,14 +58,17 @@ public final class TaskFragmentOperation implements Parcelable { /** 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 @@ public final class TaskFragmentOperation implements Parcelable { 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 80b2161edf09..cc48d468258e 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 @@ public final class WindowContainerTransaction implements Parcelable { * {@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 @@ public final class WindowContainerTransaction implements Parcelable { } /** + * 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 02cbd4101903..9f283d4e81bf 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -2953,12 +2953,24 @@ public class ChooserActivity extends ResolverActivity implements 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 0706ee52ec3a..f098e2c6bd12 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -209,7 +209,7 @@ public class ResolverActivity extends Activity implements * <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 @@ public class ResolverActivity extends Activity implements 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 077657216119..e47c335f9c7e 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.ComponentName; 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 @@ public class UnlaunchableAppActivity extends Activity private int mUserId; private int mReason; private IntentSender mTarget; + private TelecomManager mTelecomManager; @Override protected void onCreate(Bundle savedInstanceState) { @@ -60,9 +63,11 @@ public class UnlaunchableAppActivity extends Activity // 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 @@ public class UnlaunchableAppActivity extends Activity 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 @@ public class UnlaunchableAppActivity extends Activity 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 @@ public class UnlaunchableAppActivity extends Activity @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 @@ public class UnlaunchableAppActivity extends Activity 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 25ac1bd678c6..600ae50e8d02 100644 --- a/core/java/com/android/internal/policy/TransitionAnimation.java +++ b/core/java/com/android/internal/policy/TransitionAnimation.java @@ -1279,10 +1279,15 @@ public class TransitionAnimation { 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; + } + final HardwareBuffer hwBuffer = buffer.getHardwareBuffer(); + final float luma = getBorderLuma(hwBuffer, buffer.getColorSpace()); + if (hwBuffer != null) { + hwBuffer.close(); } - return 0; + 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 325df576f10a..8cf17cdaab83 100644 --- a/core/java/com/android/internal/power/EnergyConsumerStats.java +++ b/core/java/com/android/internal/power/EnergyConsumerStats.java @@ -58,7 +58,8 @@ public class EnergyConsumerStats { 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 @@ public class EnergyConsumerStats { 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 db288c0dca90..8fb345b3a900 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -53,7 +53,7 @@ oneway interface IStatusBar 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 836786d7c592..7e12574e08f1 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 f76dd095de29..5677699537ed 100644 --- a/core/java/com/android/server/backup/AccountManagerBackupHelper.java +++ b/core/java/com/android/server/backup/AccountManagerBackupHelper.java @@ -18,8 +18,8 @@ package com.android.server.backup; 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 @@ public class AccountManagerBackupHelper extends BlobBackupHelper { // 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 @@ public class AccountManagerBackupHelper extends BlobBackupHelper { try { switch (key) { case KEY_ACCOUNT_ACCESS_GRANTS: { - return am.backupAccountAccessPermissions(UserHandle.USER_SYSTEM); + return am.backupAccountAccessPermissions(mUserId); } default: { @@ -71,7 +74,7 @@ public class AccountManagerBackupHelper extends BlobBackupHelper { 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 2df1d5d210da..1dcd5cc210d9 100644 --- a/core/proto/android/nfc/nfc_service.proto +++ b/core/proto/android/nfc/nfc_service.proto @@ -60,7 +60,7 @@ message NfcServiceDumpProto { 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 @@ message DiscoveryParamsProto { 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 35aae8f92d93..5a18d9e627e8 100644 --- a/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto +++ b/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto @@ -31,7 +31,7 @@ message InputMethodManagerServiceProto { 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 7d5be4805ab0..8ee88ad07d0e 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 000000000000..d9b68794b909 --- /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 1d1c02d648d8..1b6f88f3feee 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 a9c56f0f802b..f4b49e6ce38e 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 b754440df803..7c6f81db8e9b 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 476c18ee0c6c..0a7ffca8c986 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -30,7 +30,7 @@ please see styles_device_defaults.xml. --> <resources> <!-- Global Theme Styles --> - <eat-comment /> + <eat-comment/> <style name="WindowTitleBackground"> <item name="background">@drawable/title_bar</item> @@ -69,6 +69,19 @@ please see styles_device_defaults.xml. <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 @@ please see styles_device_defaults.xml. </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 @@ please see styles_device_defaults.xml. </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 @@ please see styles_device_defaults.xml. <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 @@ please see styles_device_defaults.xml. <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 @@ please see styles_device_defaults.xml. </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 @@ please see styles_device_defaults.xml. </style> <!-- Text Appearances --> - <eat-comment /> + <eat-comment/> <style name="TextAppearance"> <item name="textColor">?textColorPrimary</item> @@ -878,9 +893,9 @@ please see styles_device_defaults.xml. <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 @@ please see styles_device_defaults.xml. </style> <!-- @hide --> - <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.Subtitle"> - <item name="textSize">14sp</item> - <item name="textColor">?textColorSecondaryInverse</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.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 @@ please see styles_device_defaults.xml. </style> <!-- Other Misc Styles --> - <eat-comment /> + <eat-comment/> <style name="MediaButton"> <item name="background">@null</item> @@ -1298,10 +1313,12 @@ please see styles_device_defaults.xml. <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 @@ please see styles_device_defaults.xml. <!-- 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 @@ please see styles_device_defaults.xml. <!-- 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 4131887fc9d0..2abb0d89835b 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 869b4844e529..8f655efe4f07 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 ed885376378c..b7e93cd49b7e 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 index 65e55a2c753b..63de759282cf 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 @@ public final class DefaultRadioTunerTest { @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 @@ public final class DefaultRadioTunerTest { @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 @@ public final class DefaultRadioTunerTest { @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 index 9a999e4067c5..f807badb6228 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.timeout; 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 index 9399907cf33c..ae43a1c3f335 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 index 6e1bb4b4c5a3..b0fb26ad84ae 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 index afbf8c304e3d..79a6b0d72e37 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 static org.mockito.Mockito.when; 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 index 5771135e32b8..e348a51f6214 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 index c8b4493a07d7..487086c8bf23 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 static org.mockito.Mockito.when; 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 cabeb13b2124..000000000000 --- 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 694b3128998c..f96d138c463b 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 static org.junit.Assert.fail; 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.io.InputStream; 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 11afd045f364..2a1881e56a6d 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 org.mockito.Mockito.verify; 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 org.mockito.junit.MockitoRule; 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 a9583fdc2e2d..c260ef90cd4e 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 static org.testng.Assert.assertThrows; 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 000000000000..e03b722c9c6c --- /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 07d0d96a120b..ee8ec1dd1008 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 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { 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 cd61dbb5b7d1..f6d67d858f98 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 @@ public class PipBoundsAlgorithm { 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 class PipBoundsAlgorithm { 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 index e3495e100c62..5045cf905ee6 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 @@ import java.util.Set; * 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 690505e03fce..ed8dc7ded654 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 android.view.Gravity; 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 3153313de42f..e83854e22fa2 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.content.res.Configuration; 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.PipAnimationController; 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 @@ public class PipController implements PipTransitionController.PipTransitionCallb 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 @@ public class PipController implements PipTransitionController.PipTransitionCallb PipAnimationController pipAnimationController, PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, - PipKeepClearAlgorithm pipKeepClearAlgorithm, + PipKeepClearAlgorithmInterface pipKeepClearAlgorithm, PipBoundsState pipBoundsState, PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, @@ -419,7 +417,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb 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 31490e427a53..b042063c23c9 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.internal.protolog.common.ProtoLog; 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 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { @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 66d0a2aa409b..665267fde6ff 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 @@ class ScreenRotationAnimation { 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 ea6c14d7152e..02d50f47288e 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.entireScreenCovered 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 @@ open class EnterPipToOtherOrientationTest(flicker: FlickerTest) : PipTransition( } /** - * 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 @@ open class EnterPipToOtherOrientationTest(flicker: FlickerTest) : PipTransition( @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 @@ open class EnterPipToOtherOrientationTest(flicker: FlickerTest) : PipTransition( @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 b5a5004aa553..3bfcde3dbc48 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 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.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 @@ open class ExitPipViaExpandButtonClickTest(flicker: FlickerTest) : ExitPipToAppT } /** {@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 f213cc96ecdb..c90c2d42e9bf 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 @@ class ExpandPipOnDoubleClickTest(flicker: FlickerTest) : PipTransition(flicker) * 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 @@ class ExpandPipOnDoubleClickTest(flicker: FlickerTest) : PipTransition(flicker) * 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 @@ class ExpandPipOnDoubleClickTest(flicker: FlickerTest) : PipTransition(flicker) } } - @FlakyTest(bugId = 249308003) + @Presubmit @Test fun pipSameAspectRatio() { flicker.assertLayers { @@ -116,92 +116,26 @@ class ExpandPipOnDoubleClickTest(flicker: FlickerTest) : PipTransition(flicker) } /** 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 34f6659ca36e..cb2326c85eb2 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 @@ class ExpandPipOnPinchOpenTest(flicker: FlickerTest) : PipTransition(flicker) { 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 eee00bd1e699..4557a15222b9 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.FlickerTest 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 @@ open class PipRotationTest(flicker: FlickerTest) : PipTransition(flicker) { 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 @@ open class PipRotationTest(flicker: FlickerTest) : PipTransition(flicker) { 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 d0d9167555eb..000000000000 --- 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 d7107db7be2d..871515ba9147 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.FlickerBuilder 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 @@ open class SetRequestedOrientationWhilePinnedTest(flicker: FlickerTest) : PipTra 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 @@ open class SetRequestedOrientationWhilePinnedTest(flicker: FlickerTest) : PipTra 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 @@ open class SetRequestedOrientationWhilePinnedTest(flicker: FlickerTest) : PipTra 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 65cbea03a044..c08ad697b6ac 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 @@ class CopyContentInSplit(flicker: FlickerTest) : SplitScreenBase(flicker) { /** {@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 fcdad960107f..514365fbd71a 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 @@ 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 @@ -149,60 +148,9 @@ class DragDividerToResize(flicker: FlickerTest) : SplitScreenBase(flicker) { ) /** {@inheritDoc} */ - @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() - - /** {@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 + @FlakyTest(bugId = 263213649) @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 af63f7c26a8c..d086f7e04486 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 @@ class EnterSplitScreenByDragFromShortcut(flicker: FlickerTest) : SplitScreenBase } /** {@inheritDoc} */ - @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() - - /** {@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 + @FlakyTest(bugId = 241523824) @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 c09ca914caff..a9cbb7419417 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 @@ 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 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.splitAppLayerBoundsBecomesVisible 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 @@ class EnterSplitScreenFromOverview(flicker: FlickerTest) : SplitScreenBase(flick @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 @@ class EnterSplitScreenFromOverview(flicker: FlickerTest) : SplitScreenBase(flick 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 09568b291830..c7b81d924a9b 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 @@ class SwitchAppByDoubleTapDivider(flicker: FlickerTest) : SplitScreenBase(flicke /** {@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 262e4290ef44..298d0a624869 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 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { 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 90880772b25d..17e7d74c57e0 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 @@ public class PipTaskOrganizerTest extends ShellTestCase { 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 3bd2ae76ebfd..c1993b25030b 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.FloatingContentCoordinator; 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 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { 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 474d6aaf4623..8ad2932b69e4 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.FloatingContentCoordinator; 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 @@ public class PipTouchHandlerTest extends ShellTestCase { 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 533106db37e5..1524dff739d5 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 273c7af7d244..4323c738f910 100644 --- a/media/java/android/media/MediaPlayer.java +++ b/media/java/android/media/MediaPlayer.java @@ -708,12 +708,10 @@ public class MediaPlayer extends PlayerBase * 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 @@ public class MediaPlayer extends PlayerBase 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 @@ public class MediaPlayer extends PlayerBase 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 0982132e006d..e1af909d63fc 100644 --- a/media/java/android/media/RoutingSessionInfo.java +++ b/media/java/android/media/RoutingSessionInfo.java @@ -23,7 +23,6 @@ import android.os.Bundle; 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 @@ public final class RoutingSessionInfo implements Parcelable { 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 @@ public final class RoutingSessionInfo implements Parcelable { 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 a1a6b516859c..a9ea6d3999d0 100644 --- a/media/java/android/media/tv/TableRequest.java +++ b/media/java/android/media/tv/TableRequest.java @@ -33,11 +33,54 @@ public final class TableRequest extends BroadcastInfoRequest implements Parcelab /** @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 afc9bee5fb85..1c314b09db9a 100644 --- a/media/java/android/media/tv/TableResponse.java +++ b/media/java/android/media/tv/TableResponse.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; 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 @@ public final class TableResponse extends BroadcastInfoResponse implements Parcel 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 final class TableResponse extends BroadcastInfoResponse implements Parcel public TableResponse(int requestId, int sequence, @ResponseResult int responseResult, @Nullable Uri tableUri, int version, int size) { super(RESPONSE_TYPE, requestId, sequence, responseResult); + 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 @@ public final class TableResponse extends BroadcastInfoResponse implements Parcel 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 @@ public final class TableResponse extends BroadcastInfoResponse implements Parcel } /** + * 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 @@ public final class TableResponse extends BroadcastInfoResponse implements Parcel 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 03c62f0824f3..d04c58a9b869 100644 --- a/media/java/android/media/tv/TimelineRequest.java +++ b/media/java/android/media/tv/TimelineRequest.java @@ -42,6 +42,7 @@ public final class TimelineRequest extends BroadcastInfoRequest implements Parce }; private final int mIntervalMillis; + private final String mSelector; static TimelineRequest createFromParcelBody(Parcel in) { return new TimelineRequest(in); @@ -50,11 +51,21 @@ public final class TimelineRequest extends BroadcastInfoRequest implements Parce 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 @@ public final class TimelineRequest extends BroadcastInfoRequest implements Parce 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 final class TimelineRequest extends BroadcastInfoRequest implements Parce 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 537e71122cef..ad9312f6975d 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl @@ -41,7 +41,9 @@ oneway interface ITvInteractiveAppClient { 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 5a0ac84e5c5e..c0723f7b9d78 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.AppLinkInfo; 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 @@ import android.view.Surface; */ 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 @@ interface ITvInteractiveAppManager { 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 @@ interface ITvInteractiveAppManager { 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 20ba57b11b11..9ae9ca791949 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.graphics.Rect; 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 @@ oneway interface ITvInteractiveAppSession { 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 @@ oneway interface ITvInteractiveAppSession { 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 c5dbd1941c90..d84affd850a9 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl @@ -40,7 +40,9 @@ oneway interface ITvInteractiveAppSessionCallback { 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 a55e1acf7837..8a23e65ec3a6 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.NonNull; 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 @@ public class ITvInteractiveAppSessionWrapper 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 @@ public class ITvInteractiveAppSessionWrapper 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 @@ public class ITvInteractiveAppSessionWrapper 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 @@ public class ITvInteractiveAppSessionWrapper } @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 @@ public class ITvInteractiveAppSessionWrapper } @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 f4847f70fcee..fd3c29bb24aa 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.Nullable; 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 @@ public final class TvInteractiveAppManager { } @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 @@ public final class TvInteractiveAppManager { } @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 @@ public final class TvInteractiveAppManager { } /** + * 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 @@ public final class TvInteractiveAppManager { } } + 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 @@ public final class TvInteractiveAppManager { } } + 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 @@ public final class TvInteractiveAppManager { }); } + 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 @@ public final class TvInteractiveAppManager { }); } + void postRequestCurrentVideoBounds() { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onRequestCurrentVideoBounds(mSession); + } + }); + } + void postRequestCurrentChannelUri() { mHandler.post(new Runnable() { @Override @@ -2003,6 +2130,20 @@ public final class TvInteractiveAppManager { } /** + * 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 @@ public final class TvInteractiveAppManager { } /** + * 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 3ca9f2fa2986..be2c16c90b9e 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.Context; 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 @@ public abstract class TvInteractiveAppService extends Service { * 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 abstract class TvInteractiveAppService extends Service { 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 @@ public abstract class TvInteractiveAppService extends Service { } /** + * 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 @@ public abstract class TvInteractiveAppService extends Service { } /** + * 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 @@ public abstract class TvInteractiveAppService extends Service { } /** + * 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 @@ public abstract class TvInteractiveAppService extends Service { } /** + * 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 @@ public abstract class TvInteractiveAppService extends Service { onSetTeletextAppEnabled(enable); } + void sendCurrentVideoBounds(@NonNull Rect bounds) { + onCurrentVideoBounds(bounds); + } + void sendCurrentChannelUri(@Nullable Uri channelUri) { onCurrentChannelUri(channelUri); } @@ -1330,6 +1538,34 @@ public abstract class TvInteractiveAppService extends Service { } /** + * 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 3e0885214dcd..acc2444e1213 100644 --- a/media/java/android/media/tv/interactive/TvInteractiveAppServiceInfo.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppServiceInfo.java @@ -57,6 +57,8 @@ public final class TvInteractiveAppServiceInfo implements Parcelable { 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 final class TvInteractiveAppServiceInfo implements Parcelable { 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 @@ public final class TvInteractiveAppServiceInfo implements Parcelable { 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 @@ public final class TvInteractiveAppServiceInfo implements Parcelable { mService.writeToParcel(dest, flags); dest.writeString(mId); dest.writeInt(mTypes); + dest.writeStringList(mExtraTypes); } /** @@ -171,6 +188,17 @@ public final class TvInteractiveAppServiceInfo implements Parcelable { 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 @@ public final class TvInteractiveAppServiceInfo implements Parcelable { } } - 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 6777d1a3c1eb..af03bb86d364 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.content.res.XmlResourceParser; 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 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * 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 @@ public class TvInteractiveAppView extends ViewGroup { } } + /** + * 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 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * 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 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * 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 @@ public class TvInteractiveAppView extends ViewGroup { } @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 @@ public class TvInteractiveAppView extends ViewGroup { } @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 d8705a7ce9ca..5b0c2a203022 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -2432,13 +2432,12 @@ static void android_media_MediaCodec_native_queueLinearBlock( 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 da920bb63178..95522001d342 100644 --- a/media/jni/android_media_MediaPlayer.cpp +++ b/media/jni/android_media_MediaPlayer.cpp @@ -956,14 +956,16 @@ android_media_MediaPlayer_native_init(JNIEnv *env) 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 @@ static const JNINativeMethod gMethods[] = { {"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 2cba03bc5c57..8752e3d40b02 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 @@ public abstract class RemoteDisplayProvider { | 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 f48056633e4e..f812d5fa1c20 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 android.media.AudioManager.AUDIO_SESSION_ID_GENERATE; 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 @@ public class MediaPlayerUnitTest { 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 5e5ebed78e61..f1c30889c4db 100644 --- a/native/android/input.cpp +++ b/native/android/input.cpp @@ -299,6 +299,8 @@ int32_t AMotionEvent_getClassification(const AInputEvent* motion_event) { 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 1e5bd7bf843e..7d433648df28 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -152,22 +152,6 @@ class CredentialManagerRepo( 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 686415f9d3e9..0620f9aa8c82 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -45,19 +45,19 @@ import kotlinx.coroutines.launch 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 @@ class CredentialSelectorActivity : ComponentActivity() { } 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 @@ class CredentialSelectorActivity : ComponentActivity() { 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 3e1137df345f..ac84503583a8 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 @@ data class CreateCredentialUiState( ) 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 @@ class CreateCredentialViewModel( } 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 @@ class CreateCredentialViewModel( hidden = true, ) } else { - CredentialManagerRepo.getInstance().onOptionSelected( + credManRepo.onOptionSelected( providerId, entryKey, entrySubkey @@ -243,7 +242,7 @@ class CreateCredentialViewModel( "$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 af59a0a39c72..6f0f76b72e50 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 @@ data class GetCredentialUiState( 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 @@ class GetCredentialViewModel( 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 @@ class GetCredentialViewModel( "${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 @@ class GetCredentialViewModel( } 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 468a97630e19..15920940140c 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 @@ public class IllustrationPreference extends Preference { 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 @@ public class IllustrationPreference extends Preference { 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 2efa10744bb3..d1dceb309b99 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 25dbe007bac7..e1e7649de5c1 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 90710db6388b..18b207337ad4 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 @@ package com.android.settingslib.spaprivileged.model.app 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 @@ interface AppRepository { 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 16ca70fe90b9..602df54ed3fb 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 @@ import com.android.settingslib.spaprivileged.model.app.rememberAppRepository 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 @@ class AppInfoProvider(private val packageInfo: PackageInfo) { 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 @@ internal fun AppIcon(app: ApplicationInfo, size: Dp) { } @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 29549d9a7fa7..103512d4a28a 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 @@ public class IllustrationPreferenceTest { 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 @@ public class IllustrationPreferenceTest { 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 @@ public class IllustrationPreferenceTest { 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 ecb88f68d46a..4e620cd178b2 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 a494f5e086ae..0b1a3e272d01 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp +++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp @@ -20,6 +20,17 @@ package { 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 000000000000..c89e4c318805 --- /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 Binary files differnew file mode 100644 index 000000000000..6149ee4b2365 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/a11ymenu_intro.png 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 000000000000..5ff245d6a11e --- /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 000000000000..5ff245d6a11e --- /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 000000000000..a2eaf95dcef0 --- /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 000000000000..7e1262c2b4e7 --- /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 000000000000..f6af270095c3 --- /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 000000000000..2f7b632d6adc --- /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 000000000000..79e0e08d8899 --- /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 000000000000..ebeebf81eedc --- /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 Binary files differnew file mode 100644 index 000000000000..b0d169642461 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_0deg.9.png diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_270deg.9.png b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_270deg.9.png Binary files differnew file mode 100644 index 000000000000..b777ffee62fd --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_270deg.9.png diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_90deg.9.png b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_90deg.9.png Binary files differnew file mode 100644 index 000000000000..998bd9037462 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_90deg.9.png 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 000000000000..c1f76f370c52 --- /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 000000000000..658c03bd388f --- /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 000000000000..39e5a8c6876b --- /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 000000000000..c198443415dd --- /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 000000000000..28a633e5d17a --- /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 000000000000..69f09343b1d1 --- /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 000000000000..33c0cca5131e --- /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 000000000000..81b3152375ff --- /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 000000000000..2f9d6b5c8a19 --- /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 000000000000..36d1fc1263c4 --- /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 000000000000..7ed18977cd54 --- /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 000000000000..0c25ec4353a5 --- /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 000000000000..30fd0173ff3f --- /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 000000000000..a2cf26730960 --- /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 96882d335d4b..3dbbb1a658c9 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 8b759004f657..5c4fdcc0e5d8 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 onCreate() { + super.onCreate(); + } @Override - public void onAccessibilityEvent(AccessibilityEvent event) { + 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 000000000000..fa42e61899fd --- /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 000000000000..28ba4b54107f --- /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 000000000000..e3401a9a7915 --- /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 000000000000..20c63df885d2 --- /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 000000000000..740bc8a412af --- /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 000000000000..c510b876e847 --- /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 000000000000..5670d72842f4 --- /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 af6e6467039a..6d5eb6adb99e 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 e4f339af9bcb..274520291e8f 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 a71fb5611bd9..fa484c794a5b 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 @@ oneway interface IOverviewProxy { /** * 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 eed55315e836..9b2a224f17e0 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 @@ interface ControlsBindingController : UserAwareController { 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 2f0fd99337e5..3d6d3356fb55 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 @@ open class ControlsBindingControllerImpl @Inject constructor( 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 2f49c3fe863e..f29f6d0dd0cb 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 @@ interface ControlsController : UserAwareController { 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 7b1c62326a68..49771ddbe0da 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 @@ class ControlsControllerImpl @Inject constructor ( 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 5b38e5b28be9..72c3a943c30b 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 @@ class ControlsProviderLifecycleManager( 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 @@ class ControlsProviderLifecycleManager( }) } - 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 @@ class ControlsProviderLifecycleManager( 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 1e3e5cd1c31c..6289788f650a 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 @@ class ControlsUiControllerImpl @Inject constructor ( 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 d040f8fe6c61..c880c59b4f19 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 @@ object Flags { /** 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 d14b66a68f11..0c4bca616e12 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 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio 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 3b09ae7ba8ea..7134ec0d64f0 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.animation.Interpolators 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 @@ constructor( 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 @@ constructor( ), ::toTriple ) - .collect { triple -> - val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple + .collect { (isDreaming, dozeTransitionModel, lastStartedTransition) -> if ( !isDreaming && isDozeOff(dozeTransitionModel.to) && @@ -96,8 +95,7 @@ constructor( ), ::toTriple ) - .collect { triple -> - val (isDreaming, isOccluded, lastStartedTransition) = triple + .collect { (isDreaming, isOccluded, lastStartedTransition) -> if ( isOccluded && !isDreaming && @@ -123,24 +121,18 @@ constructor( 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 @@ constructor( 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 64028ceb2fbe..5674e2a15271 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 @@ constructor( private val keyguardTransitionRepository: KeyguardTransitionRepository, ) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) { - private var transitionId: UUID? = null - override fun start() { listenForLockscreenToGone() listenForLockscreenToOccluded() @@ -104,6 +102,7 @@ constructor( /* 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 @@ constructor( ), ::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 490d22eb0820..4cf56fe2c031 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.StatusBarState 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 @@ constructor( /** * 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 9cdbcda1343d..ad6dbea7ae43 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.data.repository.KeyguardTransitionRepositor 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 @@ constructor( 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 @@ constructor( ): 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 e164f5d58b07..6627865ecc79 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.dagger.SysUISingleton 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 @@ constructor( /** 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 000000000000..5a4796096eeb --- /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 d48f87deaaf4..e05adbdab583 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.dagger.SysUISingleton 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 @@ constructor( }, // 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 899148b0014c..8f1c9048026f 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 @@ constructor( 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 8356440714e6..08d18575da79 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 @@ constructor( 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 d14b7a766762..d5f4a5a5d351 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 @@ constructor( 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 98d69910aac3..26e3f49828c7 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.Intent 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 @@ constructor( ) { 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 @@ constructor( } 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 @@ constructor( } 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 47fe67638cd0..f203e7a51643 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 @@ constructor( 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 30f81243e8d0..19215867e678 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 @@ object FooterActionsViewBinder { // 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 5ea1c0b4ce85..c335a6d74e9b 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 @@ public class OverviewProxyRecentsImpl implements RecentsImplementation { } @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 b041f957d771..95d6c187a224 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 @@ public class Recents implements CoreStartable, CommandQueue.Callbacks { } @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 8848dbbda5e7..010ceda84068 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 @@ public interface RecentsImplementation { 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 93e815174b50..964d0b23fa8d 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.domain.interactor.KeyguardTransitionInterac 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 @@ public final class NotificationPanelViewController implements Dumpable { private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel; private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel; private LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel; + private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel; private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel; private KeyguardTransitionInteractor mKeyguardTransitionInteractor; @@ -700,6 +702,7 @@ public final class NotificationPanelViewController implements Dumpable { private int mDreamingToLockscreenTransitionTranslationY; private int mOccludedToLockscreenTransitionTranslationY; private int mLockscreenToDreamingTransitionTranslationY; + private int mGoneToDreamingTransitionTranslationY; private int mLockscreenToOccludedTransitionTranslationY; private boolean mUnocclusionTransitionFlagEnabled = false; @@ -735,6 +738,12 @@ public final class NotificationPanelViewController implements Dumpable { 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 @@ public final class NotificationPanelViewController implements Dumpable { DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel, OccludedToLockscreenTransitionViewModel occludedToLockscreenTransitionViewModel, LockscreenToDreamingTransitionViewModel lockscreenToDreamingTransitionViewModel, + GoneToDreamingTransitionViewModel goneToDreamingTransitionViewModel, LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel, @Main CoroutineDispatcher mainDispatcher, KeyguardTransitionInteractor keyguardTransitionInteractor, @@ -834,6 +844,7 @@ public final class NotificationPanelViewController implements Dumpable { mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel; mOccludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel; mLockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel; + mGoneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel; mLockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel; mKeyguardTransitionInteractor = keyguardTransitionInteractor; mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @@ -1172,6 +1183,17 @@ public final class NotificationPanelViewController implements Dumpable { 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 @@ public final class NotificationPanelViewController implements Dumpable { 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 @@ public final class NotificationPanelViewController implements Dumpable { 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 @@ public final class NotificationPanelViewController implements Dumpable { } } + /** 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 @@ public final class NotificationPanelViewController implements Dumpable { 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 8314ec713ccb..26f8b6222dc1 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 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW && !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 bad942fe3da9..04adaae209fb 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 @@ public class CommandQueue extends IStatusBar.Stub implements */ 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 class CommandQueue extends IStatusBar.Stub implements } } - 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 @@ public class CommandQueue extends IStatusBar.Stub implements 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 1e7fc93cb9fa..197cf5608cf5 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 @@ import javax.inject.Inject * 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 @@ class SystemStatusAnimationScheduler @Inject constructor( 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 @@ class SystemStatusAnimationScheduler @Inject constructor( /** 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 @@ class SystemStatusAnimationScheduler @Inject constructor( } } - 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 59603874efde..5562e73f0478 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 @@ sealed interface ResolvedNetworkType { 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 d04996b4d6ce..6187f64e011d 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 android.telephony.TelephonyManager 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 @@ interface MobileConnectionRepository { * 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 8ac12379e59e..22aca0a8b0d7 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.MobileConn 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 @@ import kotlinx.coroutines.launch 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 @@ constructor( 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 { + mobileDataSource.mobileEvents.filterNotNull().collect { event -> + processMobileEvent(event) + } + } + wifiDemoCommandJob = scope.launch { - dataSource.mobileEvents.filterNotNull().collect { event -> processEvent(event) } + 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 @@ constructor( 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 @@ constructor( _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 @@ constructor( ) } + 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 @@ constructor( 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 000000000000..c783b12e0c0b --- /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 000000000000..0f30ae249c31 --- /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 4e42f9b31e5c..3f2ce4000ff1 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.common.coroutine.ConflatedCallbackFlow.conflatedCall 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.merge 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 @@ class MobileConnectionRepositoryImpl( 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 @@ class MobileConnectionRepositoryImpl( ) } } - - 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 c88c70064238..4472e0972a0b 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.dagger.qualifiers.Background 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( .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 @@ constructor( @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 9427c6b9fece..003df2482c6e 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.dagger.qualifiers.Application 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 @@ class MobileIconInteractorImpl( 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 4251d18357f7..da2daf2c55ea 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 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { 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 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { 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 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { * * 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 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { 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 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { 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 TYPE_ACTIVE = "Active" 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 c588945fbd67..caac8fa2f2c3 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.SysUISingleton 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 @@ constructor( 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 @@ constructor( ) } + 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 @@ constructor( "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 be3d7d4e65c4..e161b3e42d02 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 @@ constructor( 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 @@ constructor( _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 @@ constructor( 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 2353fb82f3b1..518f8ce66d2e 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 @@ sealed interface FakeWifiEventModel { 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 c47c20d280c7..d26499c18661 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.NetworkRequest 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 @@ constructor( 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 @@ constructor( .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 980560ab5d58..86dcd18c643c 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 @@ class WifiInteractorImpl @Inject constructor( 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 824b5972ba4b..95431afb71bb 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 @@ constructor( 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 235495cfa50d..b22af3b5056c 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( * 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 @@ constructor( 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 14a9161ac291..5a8850a9f89b 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 @@ constructor( eventTimeMillis: Long, batteryState: BatteryState ) { - if (batteryState.isPresent) { - stylusUsiPowerUi.updateBatteryState(batteryState) + if (batteryState.isPresent && batteryState.capacity > 0f) { + stylusUsiPowerUi.updateBatteryState(deviceId, batteryState) } } @@ -61,6 +61,7 @@ constructor( 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 e8216576811a..8d5e01c5b782 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 @@ package com.android.systemui.stylus 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 @@ constructor( // 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 @@ constructor( } } - 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 @@ constructor( } 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 @@ constructor( 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 0a81c38e7448..ebbe096b0da3 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 @@ class ControlsBindingControllerImplTest : SysuiTestCase() { } @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 1b34706bd220..25f471b0d3e0 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 @@ class ControlsControllerImplTest : SysuiTestCase() { .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 af3f24a1c58a..da548f7ccef2 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 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() { } @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 d172c9a2e630..edc6882e71c0 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 @@ class ControlsUiControllerImplTest : SysuiTestCase() { } @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 f8f2a56d4808..32cec09c3580 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 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { 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 b3cee2273012..a1b6d478d799 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.android.systemui.util.mockito.withArgCaptor 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 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { keyguardRepository.setDreamingWithOverlay(false) // AND occluded has stopped keyguardRepository.setKeyguardOccluded(false) - runCurrent() + advanceUntilIdle() val info = withArgCaptor<TransitionInfo> { @@ -506,7 +507,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { 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 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { 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 000000000000..7fa204bb980b --- /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 739059126b04..539fc2c1548e 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 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { 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 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { 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 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) ) * pixels ) + assertThat(values[3]).isEqualTo(0f) + job.cancel() } @@ -123,12 +126,15 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { 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 fc90c1add5e9..8440455127bd 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.os.UserManager 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 @@ import org.mockito.MockitoAnnotations @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 538131a4dd73..010ac5bbb2d9 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 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { // 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 dd2cc2ffc9db..bbe60f4ba493 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.ApplicationInfo 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 @@ internal class NoteTaskIntentResolverTest : SysuiTestCase() { } 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 @@ internal class NoteTaskIntentResolverTest : SysuiTestCase() { if (turnScreenOn) { flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON } + this.applicationInfo = ApplicationInfo().apply { this.packageName = packageName } } } @@ -107,7 +101,8 @@ internal class NoteTaskIntentResolverTest : SysuiTestCase() { 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 @@ internal class NoteTaskIntentResolverTest : SysuiTestCase() { @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 a1e9a2700c23..6dd2d6118f6d 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.AlternateBouncerInteracto 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 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @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 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { 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 08a9c9664ae0..526dc8d150fe 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 android.view.WindowManager; 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.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import java.util.List; + @RunWith(AndroidTestingRunner.class) @RunWithLooper @SmallTest @@ -91,13 +96,21 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @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 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mNotificationShadeWindowController.attach(); verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any()); + verify(mStatusBarStateController).addCallback(mStateListener.capture(), anyInt()); } @Test @@ -334,4 +348,59 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { 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 0000c32aa60d..fc7cd890af36 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 @@ public class CommandQueueTest extends SysuiTestCase { @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 5d377a8658a5..0859d140c3b4 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.demo.valid 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 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { 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 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { // 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 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { 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 210208532dd4..6989b514a703 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.MobileConnectio 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 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC 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 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC 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 cdbe75e855bc..9d16b7fe5246 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.model.SubscriptionMod 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 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { 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 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { } @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 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { } @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 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { } @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 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { 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 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { 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 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { 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 @@ fun validMobileEvent( 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 000000000000..ea90150b432a --- /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 000000000000..c02a4dfd074c --- /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 09589707b331..813b0ed041a7 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.SysuiTestCase 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 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { 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 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { mock<TableLogBuffer>() } + wifiRepository = FakeWifiRepository() + connectionFactory = MobileConnectionRepositoryImpl.Factory( fakeBroadcastDispatcher, @@ -110,7 +116,18 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { 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 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { context, IMMEDIATE, scope, - connectionFactory, + wifiRepository, + fullConnectionFactory, ) } @@ -180,6 +198,40 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @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 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @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 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { 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 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { 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 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @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 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { context, IMMEDIATE, scope, - connectionFactory, + wifiRepository, + fullConnectionFactory, ) var latest: MobileMappings.Config? = null @@ -571,5 +702,16 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { 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 61e13b85db6c..e6be7f15235b 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.SysuiTestCase 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 @@ class MobileIconInteractorTest : SysuiTestCase() { } @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 30ac8d432e8a..824cebdc3c08 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 @@ class WifiNetworkModelTest : SysuiTestCase() { 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 @@ class WifiNetworkModelTest : SysuiTestCase() { 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 @@ class WifiNetworkModelTest : SysuiTestCase() { 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 @@ class WifiNetworkModelTest : SysuiTestCase() { 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 8f07615b19b2..87ce8faff5a5 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.vcn.VcnTransportInfo 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 @@ class WifiRepositoryImplTest : SysuiTestCase() { .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 @@ class WifiRepositoryImplTest : SysuiTestCase() { } @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 01d59f96c221..089a170aa2be 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 @@ class WifiInteractorImplTest : SysuiTestCase() { @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 726e813ec414..b9328377772a 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 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase // 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 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase // 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 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase // 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 index a47e24057a54..7e010886c394 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 6d6e40a90fa6..a08e00223381 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.any 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 @@ class StylusManagerTest : SysuiTestCase() { 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 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onInputDeviceAdded_btStylus_firstUsed_callsCallbacksOnStylusFirstUsed() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -178,7 +176,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onInputDeviceAdded_btStylus_firstUsed_setsFlag() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -186,7 +183,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onInputDeviceAdded_btStylus_callsCallbacksWithAddress() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -215,10 +211,9 @@ class StylusManagerTest : SysuiTestCase() { } @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 @@ class StylusManagerTest : SysuiTestCase() { } @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 @@ class StylusManagerTest : SysuiTestCase() { } @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 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_btConnection_stylusAlreadyBtConnected_onlyCallsListenersOnce() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -265,7 +257,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_noBtConnection_stylusNeverBtConnected_doesNotCallCallbacks() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) @@ -317,7 +308,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onInputDeviceRemoved_btStylus_callsCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -331,7 +321,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothConnected_registersMetadataListener() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -339,7 +328,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothConnected_noBluetoothDevice_doesNotRegisterMetadataListener() { whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(null) @@ -349,7 +337,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothDisconnected_unregistersMetadataListener() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -359,7 +346,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) stylusManager.registerBatteryCallback(otherStylusBatteryCallback) @@ -377,7 +363,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -392,7 +377,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateFalse_executesBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -407,7 +391,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateNoDevice_doesNotExecuteBatteryCallbacks() { stylusManager.onMetadataChanged( bluetoothDevice, @@ -419,7 +402,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_notChargingState_doesNotExecuteBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -434,7 +416,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("TODO(b/261826950): remove on main") fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_updateEverUsedFlag() { whenever(batteryState.isPresent).thenReturn(true) @@ -444,7 +425,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - @Ignore("TODO(b/261826950): remove on main") fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_executesStylusFirstUsed() { whenever(batteryState.isPresent).thenReturn(true) @@ -454,7 +434,6 @@ class StylusManagerTest : SysuiTestCase() { } @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 @@ class StylusManagerTest : SysuiTestCase() { } @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 117e00dd9b2a..1cccd65c8dbc 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 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { } @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 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { } @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 a7951f4fa068..1e81dc761b18 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 androidx.test.filters.SmallTest 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.junit.runner.RunWith 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 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @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 @@ class StylusUsiPowerUiTest : SysuiTestCase() { 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 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @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 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @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 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @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 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @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 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Test fun updateSuppression_existingNotification_cancelsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) stylusUsiPowerUi.updateSuppression(true) @@ -159,18 +180,18 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Test @Ignore("TODO(b/257936830): get bt address once input api available") - fun refresh_hasConnectedBluetoothStylus_doesNotNotify() { + fun refresh_hasConnectedBluetoothStylus_cancelsNotification() { whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0)) stylusUsiPowerUi.refresh() - verifyNoMoreInteractions(notificationManager) + verify(notificationManager).cancel(R.string.stylus_battery_low_percentage) } @Test @Ignore("TODO(b/257936830): get bt address once input api available") fun refresh_hasConnectedBluetoothStylus_existingNotification_cancelsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0)) stylusUsiPowerUi.refresh() @@ -178,9 +199,27 @@ class StylusUsiPowerUiTest : SysuiTestCase() { 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 + 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 010189a245e5..b28ab7a0081c 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 6cfbfb8888fb..de7184c4a41a 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 @@ public class FullScreenMagnificationController implements private boolean mUnregisterPending; private boolean mDeleteAfterUnregister; - private boolean mForceShowMagnifiableBounds; - private final int mDisplayId; private int mIdOfLastServiceToMagnify = INVALID_SERVICE_ID; @@ -213,8 +211,8 @@ public class FullScreenMagnificationController implements return mRegistered; } - boolean isMagnifying() { - return mCurrentMagnificationSpec.scale > 1.0f; + boolean isActivated() { + return mMagnificationActivated; } float getScale() { @@ -370,12 +368,6 @@ public class FullScreenMagnificationController implements @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 @@ public class FullScreenMagnificationController implements .setCenterY(getCenterY()).build(); mMagnificationInfoChangedCallback.onFullScreenMagnificationChanged(mDisplayId, mMagnificationRegion, config); - if (mUnregisterPending && !isMagnifying()) { + if (mUnregisterPending && !isActivated()) { unregister(mDeleteAfterUnregister); } } @@ -476,21 +468,22 @@ public class FullScreenMagnificationController implements } @GuardedBy("mLock") - void setForceShowMagnifiableBounds(boolean show) { - if (mRegistered) { - mForceShowMagnifiableBounds = show; - if (traceEnabled()) { - logTrace("setForceShowMagnifiableBounds", - "displayID=" + mDisplayId + ";show=" + show); - } + private boolean setActivated(boolean activated) { + if (DEBUG) { + Slog.i(LOG_TAG, "setActivated(activated = " + activated + ")"); + } + + final boolean changed = (mMagnificationActivated != activated); + + if (changed) { + mMagnificationActivated = activated; + mMagnificationInfoChangedCallback.onFullScreenMagnificationActivationState( + mDisplayId, mMagnificationActivated); mControllerCtx.getWindowManager().setForceShowMagnifiableBounds( - mDisplayId, show); + mDisplayId, activated); } - } - @GuardedBy("mLock") - boolean isForceShowMagnifiableBounds() { - return mRegistered && mForceShowMagnifiableBounds; + return changed; } @GuardedBy("mLock") @@ -504,13 +497,13 @@ public class FullScreenMagnificationController implements 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 @@ public class FullScreenMagnificationController implements + ", 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 @@ public class FullScreenMagnificationController implements if (display == null) { return; } - if (!display.isMagnifying()) { + if (!display.isActivated()) { return; } final Rect magnifiedRegionBounds = mTempRect; @@ -831,16 +825,16 @@ public class FullScreenMagnificationController implements /** * @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 class FullScreenMagnificationController implements */ 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 @@ public class FullScreenMagnificationController implements * 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 @@ public class FullScreenMagnificationController implements * * @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 @@ public class FullScreenMagnificationController implements 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 @@ public class FullScreenMagnificationController implements } } - 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 @@ public class FullScreenMagnificationController implements 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 @@ public class FullScreenMagnificationController implements } 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 dc39b01cf6b6..6bf37a10ba93 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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH // 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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH @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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH 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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH 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 class FullScreenMagnificationGestureHandler extends MagnificationGestureH } } + 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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH 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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH } 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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH // (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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH // 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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH 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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH 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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH if (DEBUG_DETECTING) Slog.i(mLogTag, "setShortcutTriggered(" + state + ")"); mShortcutTriggered = state; - mFullScreenMagnificationController.setForceShowMagnifiableBounds(mDisplayId, state); + } + + private boolean isShortcutTriggered() { + return mShortcutTriggered; } /** @@ -948,12 +986,29 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH } } + 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 02e810f0e671..129bc16ce493 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 @@ public class MagnificationController implements WindowMagnificationManager.Callb 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 @@ public class MagnificationController implements WindowMagnificationManager.Callb */ 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 @@ public class MagnificationController implements WindowMagnificationManager.Callb mWindowMagnificationMgr.getCenterY(displayId)); } else { if (mFullScreenMagnificationController == null - || !mFullScreenMagnificationController.isMagnifying(displayId)) { + || !mFullScreenMagnificationController.isActivated(displayId)) { return null; } mTempPoint.set(mFullScreenMagnificationController.getCenterX(displayId), @@ -766,9 +768,7 @@ public class MagnificationController implements WindowMagnificationManager.Callb 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 @@ public class MagnificationController implements WindowMagnificationManager.Callb 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 a356ae6799d7..75fe0268892f 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 @@ public class MagnificationProcessor { } /** - * {@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 aab6a6cf71ae..3926b39fb30f 100644 --- a/services/api/current.txt +++ b/services/api/current.txt @@ -38,7 +38,8 @@ package com.android.server { 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 54f77b1e7928..6b61e978ea7b 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.Slog; 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 @@ public final class AutofillManagerService 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 @@ public final class AutofillManagerService 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 @@ public final class AutofillManagerService 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 a4ea698c5c4f..05327dcc7903 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 @@ public class TransportManager { } 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 @@ public class TransportManager { // 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 @@ public class TransportManager { 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 @@ public class TransportManager { 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 @@ public class TransportManager { 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 @@ public class TransportManager { 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 @@ public class TransportManager { 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 @@ public class TransportManager { 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 @@ public class TransportManager { 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 @@ public class TransportManager { 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 @@ public class TransportManager { 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 @@ public class TransportManager { 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 000000000000..279981bb719a --- /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 17b6f5da0fb6..ec09acd39d44 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -170,7 +170,7 @@ java_library_static { "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 68722d207ae3..d485441a605f 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.app.job.JobService; 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 @@ public class BinaryTransparencyService extends SystemService { 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 @@ public class BinaryTransparencyService extends SystemService { mServiceImpl = new BinaryTransparencyServiceImpl(); mVbmetaDigest = VBMETA_DIGEST_UNINITIALIZED; mMeasurementsLastRecordedMs = 0; - mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); } /** @@ -1062,38 +1063,6 @@ public class BinaryTransparencyService extends SystemService { } 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 @@ public class BinaryTransparencyService extends SystemService { */ @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 @@ public class BinaryTransparencyService extends SystemService { 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 bcea40e5a9db..ece72543bbc8 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 @@ public final class ActiveServices { 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 @@ public final class ActiveServices { 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 @@ public final class ActiveServices { 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 5175a31c16b5..fa0972a6862a 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 @@ public interface ActivityManagerLocal { * @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 @@ public interface ActivityManagerLocal { */ @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 1bc312e08575..a386bafbeb67 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 @@ public class ActivityManagerService extends IActivityManager.Stub 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 @@ public class ActivityManagerService extends IActivityManager.Stub 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 class ActivityManagerService extends IActivityManager.Stub 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 @@ public class ActivityManagerService extends IActivityManager.Stub @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 @@ public class ActivityManagerService extends IActivityManager.Stub } 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 28756a4e4191..f7b3d3ac7060 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 class AppBindRecord { 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 @@ final class AppBindRecord { } 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 8c242743fe0b..def51b0258f1 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 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN } 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 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN 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 7edd911299e5..919b85061db5 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 @@ public class SoundDoseHelper { } } 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 @@ public class SoundDoseHelper { 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 1b20e43c966c..59d8afabbbe1 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 @@ public class SystemBackupAgent extends BackupAgentHelper { 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 @@ public class SystemBackupAgent extends BackupAgentHelper { 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 bde14ee2000d..dcc98e17fadf 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.Intent; 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 @@ public class SyncManager { */ 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 @@ public class SyncManager { 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 @@ public class SyncManager { 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 @@ public class SyncManager { } 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++; - } - return 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; + } + } + // 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 @@ public class SyncManager { 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 @@ public class SyncManager { }); } - /** - * 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 @@ public class SyncManager { 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 @@ public class SyncManager { // 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 f7468fc97a3d..9c1cf38500f3 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 static com.android.server.content.SyncLogger.logSafe; 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 @@ public class SyncStorageEngine { 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 6b5af88d5300..59aa3f96e3fa 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 @@ final class Constants { @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 f6566d8a6ed1..7ac8fd0cf2cc 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 @@ package com.android.server.hdmi; 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 @@ public class HdmiControlService extends SystemService { 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 @@ public class HdmiControlService extends SystemService { 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 @@ public class HdmiControlService extends SystemService { } } }, 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 @@ public class HdmiControlService extends SystemService { 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 ca42614624ea..4d03e44bfd19 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 @@ public abstract class InputManagerInternal { * @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 ea7f0bb55945..be4373ab48fc 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.RemoteCallbackList; 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 @@ public class InputManagerService extends IInputManager.Stub 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 @@ public class InputManagerService extends IInputManager.Stub 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 @@ public class InputManagerService extends IInputManager.Stub 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 class InputManagerService extends IInputManager.Stub public void setInteractive(boolean interactive) { mNative.setInteractive(interactive); mBatteryController.onInteractiveChanged(interactive); + mKeyboardBacklightController.onInteractiveChanged(interactive); } @Override @@ -3346,10 +3355,12 @@ public class InputManagerService extends IInputManager.Stub 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 @@ public class InputManagerService extends IInputManager.Stub 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 77b0d4f39ae3..e1e3dd9967e0 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.IBinder; 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.GuardedBy; 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 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe 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 long USER_INACTIVITY_THRESHOLD_MILLIS = Duration.ofSeconds(30).toMillis(); + @VisibleForTesting - static final TreeSet<Integer> BRIGHTNESS_LEVELS = new TreeSet<>(); + 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 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe @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 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe // 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 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe 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; - } - @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); + newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0); } - - 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 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe 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 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe 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 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe /** Register the keyboard backlight listener for a process. */ @BinderThread + @Override public void registerKeyboardBacklightListener(IKeyboardBacklightListener listener, int pid) { synchronized (mKeyboardBacklightListenerRecords) { @@ -252,6 +335,7 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe /** Unregister the keyboard backlight listener for a process. */ @BinderThread + @Override public void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener, int pid) { synchronized (mKeyboardBacklightListenerRecords) { @@ -269,13 +353,29 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe } } - 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 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe } } - 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 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe } } - 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 b8eb901b1b7d..9e8b9f15a7c5 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.content.res.XmlResourceParser; 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.Message; 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 android.widget.Toast; 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 libcore.io.Streams; 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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { @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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { @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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { 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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } + 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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { 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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { KeyboardLayout[]::new); } + @Nullable public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) { Objects.requireNonNull(keyboardLayoutDescriptor, "keyboardLayoutDescriptor must not be null"); @@ -434,21 +464,25 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { 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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { 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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } 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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { 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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { 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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } - 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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } + @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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { 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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } + 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 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } + 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 375377a7510d..a2b183628686 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 @@ package com.android.server.input; 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.io.InputStream; 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 @@ final class PersistentDataStore { return false; } + @Nullable public String getCurrentKeyboardLayout(String inputDeviceDescriptor) { InputDeviceState state = getInputDeviceState(inputDeviceDescriptor); return state != null ? state.getCurrentKeyboardLayout() : null; @@ -136,6 +139,22 @@ final class PersistentDataStore { 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 @@ final class PersistentDataStore { 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 @@ final class PersistentDataStore { } @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 @@ final class PersistentDataStore { 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 @@ final class PersistentDataStore { } 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 @@ final class PersistentDataStore { 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 86a08579e38b..e21895aced1f 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 android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY; 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.os.ResultReceiver; 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 @@ final class DefaultImeVisibilityApplier implements ImeVisibilityApplier { @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 @@ final class DefaultImeVisibilityApplier implements ImeVisibilityApplier { 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 a2655f4c0f4d..795e4bf9d5cb 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 @@ package com.android.server.inputmethod; 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 static com.android.server.inputmethod.InputMethodManagerService.computeIm 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.ImeTracker; 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 @@ public final class ImeVisibilityStateComputer { */ 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 @@ public final class ImeVisibilityStateComputer { /** 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 @@ public final class ImeVisibilityStateComputer { 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 @@ public final class ImeVisibilityStateComputer { */ 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 @@ public final class ImeVisibilityStateComputer { void clearImeShowFlags() { mRequestedShowExplicitly = false; mShowForced = false; + mInputShown = false; } int computeImeDisplayId(@NonNull ImeTargetWindowState state, int displayId) { @@ -207,15 +254,22 @@ public final class ImeVisibilityStateComputer { * {@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 @@ public final class ImeVisibilityStateComputer { ImeTargetWindowState state = getWindowStateOrNull(windowToken); if (state != null) { state.setRequestImeToken(token); - setWindowState(windowToken, state); + setWindowStateInner(windowToken, state); + } + } + + 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); } - void setWindowState(IBinder windowToken, ImeTargetWindowState newState) { - if (DEBUG) Slog.d(TAG, "setWindowState, windowToken=" + windowToken + 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 @@ public final class ImeVisibilityStateComputer { 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 @@ public final class ImeVisibilityStateComputer { p.println(" mRequestedShowExplicitly=" + mRequestedShowExplicitly + " mShowForced=" + mShowForced); p.println(" mImeHiddenByDisplayPolicy=" + mPolicy.isImeHiddenByDisplayPolicy()); + p.println(" mInputShown=" + mInputShown); } /** @@ -309,6 +533,14 @@ public final class ImeVisibilityStateComputer { */ 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 @@ public final class ImeVisibilityStateComputer { void setA11yRequestNoSoftKeyboard(int keyboardShowMode) { mA11yRequestingNoSoftKeyboard = (keyboardShowMode & AccessibilityService.SHOW_MODE_MASK) == SHOW_MODE_HIDDEN; + if (mA11yRequestingNoSoftKeyboard) { + mPendingA11yRequestingHideKeyboard = true; + } } boolean isA11yRequestNoSoftKeyboard() { @@ -335,11 +570,14 @@ public final class ImeVisibilityStateComputer { * 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 @@ public final class ImeVisibilityStateComputer { */ 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 @@ public final class ImeVisibilityStateComputer { */ 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 @@ public final class ImeVisibilityStateComputer { 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 @@ public final class ImeVisibilityStateComputer { + " 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 079234c2f95c..187de930cff3 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 @@ final class InputMethodBindingController { // 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 2dbbb1085bb1..a94c90c8ee63 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_TOKE 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_HIDE; 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.PackageManager; 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub * 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!mPreventImeStartupUnlessTextEditor) { return false; } - if (mShowRequested) { + if (isShowRequestedForCurrentWindow()) { return false; } if (isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags)) { @@ -3370,9 +3367,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub "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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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; + } + + 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(); } - break; + } } - 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub synchronized (ImfLock.class) { try { if (mEnabledSession != null && mEnabledSession.mSession != null - && !mShowRequested) { + && !isShowRequestedForCurrentWindow()) { mEnabledSession.mSession.removeImeSurface(); } } catch (RemoteException e) { @@ -5864,7 +5731,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 c90554d9cdd8..8182fe92ff2c 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 @@ class BluetoothRouteProvider { 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 @@ class BluetoothRouteProvider { 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()); - - 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); - - mContext.registerReceiverAsUser(mBroadcastReceiver, user, - mIntentFilter, null, null); + IntentFilter adapterStateChangedIntentFilter = new IntentFilter(); + + adapterStateChangedIntentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + mContext.registerReceiverAsUser(mAdapterStateChangedReceiver, user, + adapterStateChangedIntentFilter, 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 @@ class BluetoothRouteProvider { } } - 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 @@ class BluetoothRouteProvider { } } - 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 @@ class BluetoothRouteProvider { } } - 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 628a322bf8cd..dc0cf4e09207 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 @@ public class NotificationShellCmd extends ShellCommand { 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 71593e10d45b..5e62b56c7bcd 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 @@ public abstract class ApexManager { 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 9785d478a6d1..f85d6af9efee 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 @@ public final class BackgroundDexOptService { 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 @@ public final class BackgroundDexOptService { 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 db3a3434388e..69436da981b1 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.app.usage.UsageStatsManagerInternal; 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 @@ public class BackgroundInstallControlService extends SystemService { 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 @@ public class BackgroundInstallControlService extends SystemService { 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 @@ public class BackgroundInstallControlService extends SystemService { 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 cf447a75ea86..9da319dae1a4 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.LocalManagerRegistry; 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 @@ public final class DexOptHelper { * 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 @@ public final class DexOptHelper { 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 @@ public final class DexOptHelper { } } - 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 @@ public final class DexOptHelper { 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 @@ public final class DexOptHelper { } /** - * 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 3385a09e49db..fcaaa90dbc8a 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 @@ final class DumpHelper { 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 6225753cc38f..0bdce21f885f 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 final class DumpState { 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 6825dd7832ce..5c4447eb99a4 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 android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; 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 @@ final class InitAppsHelper { 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 @@ final class InitAppsHelper { } 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 @@ final class InitAppsHelper { } 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 @@ final class InitAppsHelper { 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 @@ final class InitAppsHelper { @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 @@ final class InitAppsHelper { 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 ced547c35899..9cf91227e184 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 class InstallArgs { 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 @@ final class InstallArgs { 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 @@ final class InstallArgs { 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 ac4da2ed7d73..f0f23cd4b932 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 @@ final class InstallPackageHelper { 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 @@ final class InstallPackageHelper { 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 @@ final class InstallPackageHelper { + ", old=" + oldPackage); } request.setReturnCode(PackageManager.INSTALL_SUCCEEDED); + request.setApexModuleName(oldPackageState.getApexModuleName()); targetParseFlags = systemParseFlags; targetScanFlags = systemScanFlags; } else { // non system replace @@ -2127,7 +2129,7 @@ final class InstallPackageHelper { } // 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 @@ final class InstallPackageHelper { // 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 @@ final class InstallPackageHelper { // 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 @@ final class InstallPackageHelper { } } 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 @@ final class InstallPackageHelper { } 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 @@ final class InstallPackageHelper { // 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 @@ final class InstallPackageHelper { } 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 class InstallPackageHelper { 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 @@ final class InstallPackageHelper { | 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 @@ final class InstallPackageHelper { 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 @@ final class InstallPackageHelper { 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 @@ final class InstallPackageHelper { @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 @@ final class InstallPackageHelper { } 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 @@ final class InstallPackageHelper { 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 @@ final class InstallPackageHelper { */ @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 @@ final class InstallPackageHelper { * 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 @@ final class InstallPackageHelper { } return addForInitLI(parsedPackage, parseFlags, scanFlags, - new UserHandle(UserHandle.USER_SYSTEM)); + new UserHandle(UserHandle.USER_SYSTEM), apexInfo); } /** @@ -3768,7 +3773,26 @@ final class InstallPackageHelper { 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 class InstallPackageHelper { 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 c6cdc4cd350d..a9c5773d8f2b 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 android.util.Slog; 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 @@ final class InstallRequest { /** 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 @@ final class InstallRequest { @Nullable private ApexInfo mApexInfo; + /** + * For tracking {@link PackageState#getApexModuleName()}. + */ + @Nullable + private String mApexModuleName; + @Nullable private ScanResult mScanResult; @@ -129,7 +136,7 @@ final class InstallRequest { 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 @@ final class InstallRequest { mUserId = user.getIdentifier(); } else { // APEX - mUserId = INVALID_UID; + mUserId = UserHandle.USER_SYSTEM; } mInstallArgs = null; mParsedPackage = parsedPackage; @@ -348,6 +355,11 @@ final class InstallRequest { } @Nullable + public String getApexModuleName() { + return mApexModuleName; + } + + @Nullable public String getSourceInstallerPackageName() { return mInstallArgs.mInstallSource.mInstallerPackageName; } @@ -367,8 +379,8 @@ final class InstallRequest { return mOrigUsers; } - public int getUid() { - return mUid; + public int getAppId() { + return mAppId; } @Nullable @@ -499,8 +511,8 @@ final class InstallRequest { 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 @@ final class InstallRequest { 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 @@ final class InstallRequest { } } - 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 eb3b29ce4b71..7b759e3cfadb 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 @@ class InstallingSession { 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 @@ class InstallingSession { mIsInherit = false; mSessionId = -1; mRequireUserAction = USER_ACTION_UNSPECIFIED; - mKeepApplicationEnabledSetting = false; + mApplicationEnabledSettingPersistent = false; } InstallingSession(int sessionId, File stagedDir, IPackageInstallObserver2 observer, @@ -164,7 +164,7 @@ class InstallingSession { mIsInherit = sessionParams.mode == MODE_INHERIT_EXISTING; mSessionId = sessionId; mRequireUserAction = sessionParams.requireUserAction; - mKeepApplicationEnabledSetting = sessionParams.keepApplicationEnabledSetting; + mApplicationEnabledSettingPersistent = sessionParams.applicationEnabledSettingPersistent; } @Override @@ -535,7 +535,7 @@ class InstallingSession { mInstallPackageHelper.installPackagesTraced(installRequests); for (InstallRequest request : installRequests) { - request.onInstallCompleted(mUser.getIdentifier()); + request.onInstallCompleted(); doPostInstall(request); } } @@ -609,6 +609,7 @@ class InstallingSession { // 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 350f5ef7439c..47e18f1df774 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @Override - public boolean isKeepApplicationEnabledSetting() { - return params.keepApplicationEnabledSetting; + public boolean isApplicationEnabledSettingPersistent() { + return params.applicationEnabledSettingPersistent; } @Override @@ -4945,8 +4945,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { 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 92bbb7e86327..9cc03340b08d 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 @@ public class PackageManagerService implements PackageSender, TestUtilityService * 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 849cbeba0300..12841a447cf7 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 @@ class PackageManagerShellCommand extends ShellCommand { 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 8252a9fa65c5..d4c1256bd8f9 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 @@ package com.android.server.pm; 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 @@ final class PackageMetrics { mInstallRequest = installRequest; } - public void onInstallSucceed(int userId) { - reportInstallationToSecurityLog(userId); + public void onInstallSucceed() { + reportInstallationToSecurityLog(mInstallRequest.getUserId()); reportInstallationStats(true /* success */); } @@ -110,10 +112,11 @@ final class PackageMetrics { } } + 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 @@ final class PackageMetrics { ); } + 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 class PackageMetrics { 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 6562de96388f..53fdfaad38ac 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 @@ public class PackageSetting extends SettingBase implements PackageStateInternal return pkgState.isApkInUpdatedApex(); } + @Nullable + @Override + public String getApexModuleName() { + return pkgState.getApexModuleName(); + } + public PackageSetting setDomainSetId(@NonNull UUID domainSetId) { mDomainSetId = domainSetId; onChanged(); @@ -1317,6 +1323,11 @@ public class PackageSetting extends SettingBase implements PackageStateInternal 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 e1d2b3bcfefe..9ee6035f6f1a 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 @@ import java.io.File; /** * 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 @@ public class ScanPartition extends PackagePartitions.SystemPartition { * 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 97fb0c2e3fe9..aedf78255422 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 @@ public final class Settings implements Watchable, Snappable { 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 @@ public final class Settings implements Watchable, Snappable { && !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 @@ public final class Settings implements Watchable, Snappable { && !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 4f7c2bdf3057..23156d177abf 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 @@ public final class StorageEventHelper extends StorageEventListener { 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 411c19ff7310..d3fba7c3308d 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 android.annotation.Nullable; 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 @@ public final class DexoptOptions { } /** - * 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 @@ public final class DexoptOptions { "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 5fdead0a8883..a12c9d0498a1 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 @@ public interface PackageState { * @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 8dee8ee2fa57..bc6dab41fc7e 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 @@ public class PackageStateImpl implements PackageState { 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 @@ public class PackageStateImpl implements PackageState { mUserStates.put(userStates.keyAt(index), UserStateImpl.copy(userStates.valueAt(index))); } + + mApexModuleName = pkgState.getApexModuleName(); } @NonNull @@ -714,6 +718,11 @@ public class PackageStateImpl implements PackageState { } @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 @@ public class PackageStateImpl implements PackageState { 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 57fbfe91193b..19c08866e348 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 @@ public class PackageStateUnserialized { private List<String> usesLibraryFiles = emptyList(); private boolean updatedSystemApp; - private boolean apkInApex; private boolean apkInUpdatedApex; @NonNull @@ -70,6 +69,9 @@ public class PackageStateUnserialized { @NonNull private final PackageSetting mPackageSetting; + @Nullable + private String mApexModuleName; + public PackageStateUnserialized(@NonNull PackageSetting packageSetting) { mPackageSetting = packageSetting; } @@ -138,11 +140,11 @@ public class PackageStateUnserialized { } 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 @@ public class PackageStateUnserialized { 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 @@ public class PackageStateUnserialized { 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 @@ public class PackageStateUnserialized { } @DataClass.Generated.Member - public boolean isApkInApex() { - return apkInApex; - } - - @DataClass.Generated.Member public boolean isApkInUpdatedApex() { return apkInUpdatedApex; } @@ -292,11 +290,16 @@ public class PackageStateUnserialized { 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 65acdc118265..a099e724fd26 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { } 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 326d70930639..ed6a46f9b43f 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 @@ public class Notifier { } 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 60dbbddf8b2c..3fcb08a58000 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 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat 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 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat 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 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat 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 c559436b4a8d..d622fd717611 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 @@ public class BatteryStatsImpl extends BatteryStats { // 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 @@ public class BatteryStatsImpl extends BatteryStats { 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 @@ public class BatteryStatsImpl extends BatteryStats { UPDATE_BT, UPDATE_RPM, UPDATE_DISPLAY, + UPDATE_CAMERA, UPDATE_ALL, }) @Retention(RetentionPolicy.SOURCE) @@ -6244,6 +6247,8 @@ public class BatteryStatsImpl extends BatteryStats { } getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs) .noteCameraTurnedOnLocked(elapsedRealtimeMs); + + scheduleSyncExternalStatsLocked("camera-on", ExternalStatsSync.UPDATE_CAMERA); } @GuardedBy("this") @@ -6259,6 +6264,8 @@ public class BatteryStatsImpl extends BatteryStats { } getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs) .noteCameraTurnedOffLocked(elapsedRealtimeMs); + + scheduleSyncExternalStatsLocked("camera-off", ExternalStatsSync.UPDATE_CAMERA); } @GuardedBy("this") @@ -6273,6 +6280,8 @@ public class BatteryStatsImpl extends BatteryStats { uid.noteResetCameraLocked(elapsedRealtimeMs); } } + + scheduleSyncExternalStatsLocked("camera-reset", ExternalStatsSync.UPDATE_CAMERA); } @GuardedBy("this") @@ -7414,6 +7423,12 @@ public class BatteryStatsImpl extends BatteryStats { 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 @@ public class BatteryStatsImpl extends BatteryStats { 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 @@ public class BatteryStatsImpl extends BatteryStats { 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 @@ public class BatteryStatsImpl extends BatteryStats { } /** + * 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 16892034b1be..89991bf8e4e1 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 @@ public class CameraPowerCalculator extends PowerCalculator { 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 18595cad7741..939a08ba0b6a 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 @@ public class EnergyConsumerSnapshot { /** 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 @@ public class EnergyConsumerSnapshot { 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 @@ public class EnergyConsumerSnapshot { 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 eec3a0259a3e..94aa518b4594 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 @@ public class ResourcesManagerService extends SystemService { @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 7d8336a0d3e9..a75d110e3cd1 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 @@ public class ResourcesManagerShellCommand extends ShellCommand { 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 392fda9520fe..0fd6d9b39605 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 @@ public interface StatusBarManagerInternal { 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 8d71d9cc6dc8..97ca8df268a9 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 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @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 b0d301e42634..080929729fe3 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 @@ public class NetworkTimeUpdateService extends Binder { @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 @@ public class NetworkTimeUpdateService extends Binder { 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 @@ public class NetworkTimeUpdateService extends Binder { @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) { + 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) { + // Manage mTryAgainCounter. + if (shouldAttemptRefresh) { + if (refreshSuccessful) { + // Reset failure tracking. mTryAgainCounter = 0; + } 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; + } + } } - } else { - String logMsg = "forceRefresh() returned false:" - + " initialTimeResult=" + initialTimeResult - + ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis; - logToDebugAndDumpsys(logMsg); } - } - synchronized (this) { - long 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(); - if (calculateTimeResultAgeMillis(latestTimeResult, currentElapsedRealtimeMillis) - < maxNetworkTimeAgeMillis) { - // Obtained fresh fix; schedule next normal update - nextPollDelayMillis = mNormalPollingIntervalMillis - - latestTimeResult.getAgeMillis(currentElapsedRealtimeMillis); - + 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); - } else { - // No fresh fix; schedule retry - mTryAgainCounter++; - if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) { - nextPollDelayMillis = mShortPollingIntervalMillis; - } else { - // Try much later - mTryAgainCounter = 0; + } - nextPollDelayMillis = mNormalPollingIntervalMillis; - } + // (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; } - long nextRefreshElapsedRealtimeMillis = - currentElapsedRealtimeMillis + nextPollDelayMillis; 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 @@ public class NetworkTimeUpdateService extends Binder { : 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 @@ public class NetworkTimeUpdateService extends Binder { 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 ebee99553309..d4f2f2dcbdf8 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.Context; 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 @@ import java.util.Set; 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 @@ public class TvInteractiveAppManagerService extends SystemService { // 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 @@ public class TvInteractiveAppManagerService extends SystemService { } @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 @@ public class TvInteractiveAppManagerService extends SystemService { } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) { synchronized (mLock) { buildTvInteractiveAppServiceListLocked(mCurrentUserId, null); + buildAppLinkInfoLocked(mCurrentUserId); } } } @@ -321,6 +366,7 @@ public class TvInteractiveAppManagerService extends SystemService { synchronized (mLock) { if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) { buildTvInteractiveAppServiceListLocked(userId, packages); + buildAppLinkInfoLocked(userId); } } } @@ -427,6 +473,7 @@ public class TvInteractiveAppManagerService extends SystemService { mCurrentUserId = userId; buildTvInteractiveAppServiceListLocked(userId, null); + buildAppLinkInfoLocked(userId); } } @@ -512,6 +559,7 @@ public class TvInteractiveAppManagerService extends SystemService { private void startProfileLocked(int userId) { mRunningProfiles.add(userId); buildTvInteractiveAppServiceListLocked(userId, null); + buildAppLinkInfoLocked(userId); } @GuardedBy("mLock") @@ -667,6 +715,26 @@ public class TvInteractiveAppManagerService extends SystemService { } @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 @@ public class TvInteractiveAppManagerService extends SystemService { } @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 @@ public class TvInteractiveAppManagerService extends SystemService { } @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 @@ public class TvInteractiveAppManagerService extends SystemService { // 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 @@ public class TvInteractiveAppManagerService extends SystemService { } @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 @@ public class TvInteractiveAppManagerService extends SystemService { } @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 79de282d226c..25ce28047fe1 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 @@ package com.android.server.wallpaper; 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 3d59b7b92a82..6edfebf666ce 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.os.ParcelFileDescriptor.MODE_TRUNCATE; 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 @@ public class WallpaperManagerService extends IWallpaperManager.Stub */ 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 @@ public class WallpaperManagerService extends IWallpaperManager.Stub */ 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 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } - 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 @@ public class WallpaperManagerService extends IWallpaperManager.Stub 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 @@ public class WallpaperManagerService extends IWallpaperManager.Stub 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 @@ public class WallpaperManagerService extends IWallpaperManager.Stub 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 @@ public class WallpaperManagerService extends IWallpaperManager.Stub 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 @@ public class WallpaperManagerService extends IWallpaperManager.Stub 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 @@ public class WallpaperManagerService extends IWallpaperManager.Stub 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 @@ public class WallpaperManagerService extends IWallpaperManager.Stub 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 a9b809212124..d0311e398137 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 @@ package com.android.server.wallpaper; 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 e2ab2167e255..65127e4a3b31 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 @@ final class AccessibilityController { // 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 @@ final class AccessibilityController { // 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 @@ final class AccessibilityController { 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 4428be7458f9..d4895edbbaac 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.DESTROYING; 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 @@ class ActivityClientController extends IActivityClientController.Stub { 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 45ae3d8210c8..6abd3d7ef9ec 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A 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 b40aa3c2c974..1944b3f8e6d3 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 @@ class ActivityStartInterceptor { 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 407ffd055113..919bab8324a2 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 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { 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 d395f12f8a01..db88f0ff7cb1 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 static com.android.server.wm.utils.CoordinateTransforms.computeRotationMa 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 @@ class ScreenRotationAnimation { 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 @@ class ScreenRotationAnimation { 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 5e081d50cc54..e5385841a9a1 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 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { 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 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { 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 4e7613b66d73..0243a98ee51a 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 @@ public class WindowManagerService extends IWindowManager.Stub 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 @@ public class WindowManagerService extends IWindowManager.Stub 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 dd70fca0ab0c..0a5e0b7533ee 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.ActivityManager.isStartResultSuccessful; 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 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } 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 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub 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 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub 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 595d03d9845d..be60946dc655 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 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta 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 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta 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 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta } 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 82c235820f69..acfa491d72e1 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 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR 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 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR 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 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR 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 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR @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 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR 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 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR } 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 aefd300956d3..f76cf4993ebc 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 @@ public final class CredentialManagerService 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 @@ public final class CredentialManagerService // 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 @@ public final class CredentialManagerService // 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 c7fa72c27abb..f7c590565ae3 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 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest 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 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest 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 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest 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 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest 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 8796314b5454..c2b346fc2921 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 @@ public class PendingIntentResultHandler { 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 27eaa0b26773..7a24a225d8f8 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 @@ public final class ProviderCreateSession extends ProviderSession< 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 @@ public final class ProviderCreateSession extends ProviderSession< /** * 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 de93af41671d..95f231347ad1 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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential 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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential } Log.i(TAG, "No error or respond found in pending intent response"); - invokeCallbackOnInternalInvalidState(); + onUpdateEmptyResponse(); } private void onActionEntrySelected(ProviderPendingIntentResponse @@ -415,12 +420,16 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential } } + 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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential 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 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential /** * 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 7036dfb94163..678c752f8589 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 @@ public abstract class ProviderSession<T, R> 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 0c3c34e52d0f..8e44f0f198e5 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.CallingAppInfo; 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 @@ import java.util.Map; * 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 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan 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 f549797d42f8..e416718e13bc 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 @@ class AccessCheckingService(context: Context) : SystemService(context) { } } + 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 e0f94c7707a6..07a5e72fc182 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 @@ class AccessPolicy private constructor( } } + fun MutateStateScope.onSystemReady() { + newState.systemState.isSystemReady = true + forEachSchemePolicy { + with(it) { onSystemReady() } + } + } + fun BinaryXmlPullParser.parseSystemState(state: AccessState) { forEachTag { when (tagName) { @@ -362,6 +369,8 @@ abstract class SchemePolicy { 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 961619304531..5532311068ab 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 @@ class SystemState private constructor( 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 @@ class SystemState private constructor( IndexedListSet(), PermissionAllowlist(), IndexedMap(), + false, IntMap(), IndexedMap(), IndexedMap(), @@ -85,6 +88,7 @@ class SystemState private constructor( 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 7bfca1214b53..714480c526c7 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 @@ data class Permission( 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 903fad33055f..c7e937102ca0 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 @@ class PermissionService( 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 d0833bdda35d..694efbbf7cf9 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 @@ class UidPermissionPolicy : SchemePolicy() { IndexedListSet<OnPermissionFlagsChangedListener>() private val onPermissionFlagsChangedListenersLock = Any() + private val privilegedPermissionAllowlistViolations = IndexedSet<String>() + override val subjectScheme: String get() = UidUri.SCHEME @@ -734,7 +736,7 @@ class UidPermissionPolicy : SchemePolicy() { } 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 @@ class UidPermissionPolicy : SchemePolicy() { } !sourcePermission.isRuntime } ?: false - val shouldGrantByImplicit = isLeanBackNotificationsPermission || + val shouldGrantByImplicit = isLeanbackNotificationsPermission || (isImplicitPermission && isAnySourcePermissionNonRuntime) if (shouldGrantByImplicit) { newFlags = newFlags or PermissionFlags.IMPLICIT_GRANTED @@ -917,7 +919,21 @@ class UidPermissionPolicy : SchemePolicy() { 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 @@ class UidPermissionPolicy : SchemePolicy() { // 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 @@ class UidPermissionPolicy : SchemePolicy() { 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 000000000000..73d04c64bc63 --- /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 000000000000..8415fe120c51 --- /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 640bde330cee..804bb495bafe 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.anyBoolean; 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 @@ public class InputMethodManagerServiceTestBase { "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 @@ public class InputMethodManagerServiceTestBase { 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 83677c2a2242..47e7a37e9352 100644 --- a/services/tests/PackageManagerServiceTests/host/Android.bp +++ b/services/tests/PackageManagerServiceTests/host/Android.bp @@ -30,11 +30,16 @@ java_test_host { "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 000000000000..44b4e303565c --- /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 000000000000..aef365e9d5fc --- /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 000000000000..575b2bc74940 --- /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 000000000000..87fb5cc2c14a --- /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 000000000000..b89581d1e0ca --- /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 9234431accb9..c40017a18af0 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 @@ public class MockingOomAdjusterTests { 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 757d27b6b569..f6566a0d525a 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.CameraInjectionSession; 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 org.mockito.MockitoAnnotations; 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 2f909aa7b191..5b0e2f3800c3 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 @@ public class ApexManagerTest { 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 8b36da5bc36c..d5aa7fec996f 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 @@ public final class BackgroundDexOptServiceUnitTest { 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 bcba4a1878d8..c08f6bf0db34 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.doReturn; 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.SparseArray; 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.AfterClass; 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 @@ import java.nio.charset.StandardCharsets; * atest FrameworksMockingServicesTests:WallpaperManagerServiceTests */ @Presubmit -@FlakyTest(bugId = 129797242) @RunWith(AndroidJUnit4.class) public class WallpaperManagerServiceTests { @@ -273,8 +272,10 @@ public class WallpaperManagerServiceTests { /** * 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 @@ public class WallpaperManagerServiceTests { verifyLastWallpaperData(testUserId, sDefaultWallpaperComponent); verifyCurrentSystemData(testUserId); - mService.setWallpaperComponent(sImageWallpaperComponentName); + mService.setWallpaperComponent(sImageWallpaperComponentName, FLAG_SYSTEM, testUserId); verifyLastWallpaperData(testUserId, sImageWallpaperComponentName); verifyCurrentSystemData(testUserId); @@ -312,7 +313,7 @@ public class WallpaperManagerServiceTests { 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 class WallpaperManagerServiceTests { 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 000000000000..ea6bc980b7b6 --- /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 000000000000..b5a05fcaff17 --- /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 0780d219dc80..d996e37a2eb0 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 @@ public class FullScreenMagnificationControllerTest { } 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 @@ public class FullScreenMagnificationControllerTest { .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 @@ public class FullScreenMagnificationControllerTest { 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 @@ public class FullScreenMagnificationControllerTest { 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 @@ public class FullScreenMagnificationControllerTest { 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 @@ public class FullScreenMagnificationControllerTest { mMessageCapturingHandler.sendAllMessages(); callbacks.onUserContextChanged(); mMessageCapturingHandler.sendAllMessages(); - assertFalse(mFullScreenMagnificationController.isMagnifying(displayId)); + checkActivatedAndMagnifyingState(/* activated= */false, /* magnifying= */false, displayId); } @Test @@ -784,10 +785,10 @@ public class FullScreenMagnificationControllerTest { 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 @@ public class FullScreenMagnificationControllerTest { } @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 @@ public class FullScreenMagnificationControllerTest { 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 0fed89b831d6..5334e4cd04b8 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 @@ package com.android.server.accessibility.magnification; 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 @@ import java.util.function.IntConsumer; * {@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 class FullScreenMagnificationGestureHandlerTest { 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 class FullScreenMagnificationGestureHandlerTest { 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 @@ public class FullScreenMagnificationGestureHandlerTest { @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 @@ public class FullScreenMagnificationGestureHandlerTest { // 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 @@ public class FullScreenMagnificationGestureHandlerTest { // 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 @@ public class FullScreenMagnificationGestureHandlerTest { @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 @@ public class FullScreenMagnificationGestureHandlerTest { 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 @@ public class FullScreenMagnificationGestureHandlerTest { } @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 @@ public class FullScreenMagnificationGestureHandlerTest { } } - 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 @@ public class FullScreenMagnificationGestureHandlerTest { 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 @@ public class FullScreenMagnificationGestureHandlerTest { 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,12 +677,12 @@ public class FullScreenMagnificationGestureHandlerTest { case STATE_ZOOMED_2TAPS: { tap(); } break; - case STATE_DRAGGING: { + case STATE_NON_ACTIVATED_ZOOMED_TMP: { send(upEvent()); - returnToNormalFrom(STATE_ZOOMED); } break; - case STATE_DRAGGING_TMP: { + case STATE_ACTIVATED_ZOOMED_TMP: { send(upEvent()); + returnToNormalFrom(STATE_ZOOMED); } break; case STATE_SHORTCUT_TRIGGERED: { triggerShortcut(); @@ -682,8 +705,12 @@ public class FullScreenMagnificationGestureHandlerTest { } } + 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 @@ public class FullScreenMagnificationGestureHandlerTest { 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 @@ public class FullScreenMagnificationGestureHandlerTest { 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 2a80ce05d4e0..231b2f32864d 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 @@ public class MagnificationControllerTest { } @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 @@ public class MagnificationControllerTest { 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 3a27e3bcfdb6..798650dcc9ce 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.VirtualDeviceParams; 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 org.junit.runner.RunWith; 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 f47308637a9c..bb28a3689f68 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 static org.junit.Assert.assertThrows; 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 c27043565fcc..7b5af1e0fe98 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.MediaRecorder; 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 org.mockito.MockitoAnnotations; 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 @@ public class VirtualAudioControllerTest { } + @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 b5dad945ffac..1d23e12d0a10 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.os.test.TestLooper 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 @@ class KeyboardBacklightControllerTests { 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 @@ class KeyboardBacklightControllerTests { 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,94 +135,67 @@ class KeyboardBacklightControllerTests { `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() - 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 - ) - } - for (level in brightnessLevelsArray.size - 2 downTo 0) { - keyboardBacklightController.decrementKeyboardBacklight(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 ).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 @@ class KeyboardBacklightControllerTests { `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 @@ class KeyboardBacklightControllerTests { ) 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 @@ class KeyboardBacklightControllerTests { 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 @@ class KeyboardBacklightControllerTests { 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 @@ class KeyboardBacklightControllerTests { `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 @@ class KeyboardBacklightControllerTests { 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 @@ class KeyboardBacklightControllerTests { 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 @@ class KeyboardBacklightControllerTests { } } + 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 000000000000..34540c398deb --- /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 f5029ecae079..f2e03aa990a5 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 @@ public class BackgroundRestrictionsTest { 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 d80aa5711199..ccf530f98b4d 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 @@ public final class BackgroundInstallControlServiceTest { 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 @@ public final class BackgroundInstallControlServiceTest { 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 @@ public final class BackgroundInstallControlServiceTest { 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 @@ public final class BackgroundInstallControlServiceTest { 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 @@ public final class BackgroundInstallControlServiceTest { 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 2ebe21505bd9..cff4cc72de52 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 @@ package com.android.server.power.stats; 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 @@ public class BatteryExternalStatsWorkerTest { 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 @@ public class BatteryExternalStatsWorkerTest { 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 e4ab21b0e938..5fce32f0598a 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 @@ import org.junit.runner.RunWith; 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(calculator); + mStatsRule.apply(BatteryUsageStatsRule.POWER_PROFILE_MODEL_ONLY, 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)) + 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(1000); + .isEqualTo(3000); assertThat(deviceBatteryConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA)) - .isWithin(PRECISION).of(0.1); + .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 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.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(3000); + assertThat(deviceBatteryConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA)) + .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(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 558f39629d81..28f4799656b7 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 @@ public final class EnergyConsumerSnapshotTest { 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 08d08b65288a..10014221f235 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 @@ public class NetworkTimeUpdateServiceTest { // 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 @@ public class NetworkTimeUpdateServiceTest { 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 @@ public class NetworkTimeUpdateServiceTest { 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 @@ public class NetworkTimeUpdateServiceTest { 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 @@ public class NetworkTimeUpdateServiceTest { mMockNtpTrustedTime); NtpTrustedTime.TimeResult timeResult = createNtpTimeResult( - mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1); + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis()); NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult); { @@ -158,6 +162,9 @@ public class NetworkTimeUpdateServiceTest { 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 @@ public class NetworkTimeUpdateServiceTest { // 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,6 +214,161 @@ public class NetworkTimeUpdateServiceTest { verify(mockCallback, never()).submitSuggestion(any()); reset(mMockNtpTrustedTime); + + // Simulate the passage of time for realism. + mFakeElapsedRealtimeClock.incrementMillis(5000); + } + } + + @Test + public void engineImpl_refreshIfRequiredAndReschedule_successThenFail_tryAgainTimesZero() { + 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()); + 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 == 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); } } @@ -216,7 +377,6 @@ public class NetworkTimeUpdateServiceTest { mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); int normalPollingIntervalMillis = 7777777; - int maxTimeResultAgeMillis = normalPollingIntervalMillis; int shortPollingIntervalMillis = 3333; int tryAgainTimesMax = 5; NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( @@ -225,13 +385,16 @@ public class NetworkTimeUpdateServiceTest { mMockNtpTrustedTime); NtpTrustedTime.TimeResult timeResult1 = createNtpTimeResult( - mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1); + 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. @@ -241,10 +404,9 @@ public class NetworkTimeUpdateServiceTest { // initially. verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); - long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult( - timeResult1, normalPollingIntervalMillis); + long expectedDelayMillis = normalPollingIntervalMillis; verify(mockCallback).scheduleNextRefresh( - mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + timeResult1.getElapsedRealtimeMillis() + expectedDelayMillis); NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult1); verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion); reset(mMockNtpTrustedTime); @@ -253,7 +415,7 @@ public class NetworkTimeUpdateServiceTest { // 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 @@ public class NetworkTimeUpdateServiceTest { 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 @@ public class NetworkTimeUpdateServiceTest { 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 @@ public class NetworkTimeUpdateServiceTest { // 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 @@ public class NetworkTimeUpdateServiceTest { * 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 @@ public class NetworkTimeUpdateServiceTest { 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 @@ public class NetworkTimeUpdateServiceTest { // 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 @@ public class NetworkTimeUpdateServiceTest { /** * 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 @@ public class NetworkTimeUpdateServiceTest { 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 @@ public class NetworkTimeUpdateServiceTest { 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 a76b82babe08..fd1ca68c08db 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 @@ class TestPhoneWindowManager { 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 113f5ecbc9f5..3eb7fe3b0021 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 @@ public class SizeCompatTests extends WindowTestsBase { 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 a405e5a84360..035d73dc1c5c 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 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { 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 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // 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 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // 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 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { 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 964328271866..13c0f17dbfbb 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 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne @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 2d0135ae1c99..64b3c0a203e3 100644 --- a/telephony/java/android/telephony/ModemActivityInfo.java +++ b/telephony/java/android/telephony/ModemActivityInfo.java @@ -567,14 +567,14 @@ public final class ModemActivityInfo implements Parcelable { /** @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 a2a110d3f758..26b4bbc41b25 100644 --- a/telephony/java/android/telephony/SmsManager.java +++ b/telephony/java/android/telephony/SmsManager.java @@ -46,6 +46,7 @@ import android.util.Pair; 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 @@ public final class SmsManager { 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 0ad5ba0eaa47..481280634685 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -17715,37 +17715,6 @@ public class TelephonyManager { } /** - * 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 4fa7f43ddde4..3dfc81eafb77 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 @@ interface IPhoneSubInfo { 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 @@ interface IPhoneSubInfo { * @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 000000000000..d55dc78b8c0a --- /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 ef324e7c1377..6c89e49a0e6e 100644 --- a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java +++ b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java @@ -1156,12 +1156,12 @@ public class NotificationTestList extends TestActivity 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); } |