diff options
163 files changed, 10180 insertions, 1043 deletions
diff --git a/Android.bp b/Android.bp index c5f415d500be..ea6a6319718f 100644 --- a/Android.bp +++ b/Android.bp @@ -104,6 +104,11 @@ java_defaults { "core/java/android/app/backup/IRestoreObserver.aidl", "core/java/android/app/backup/IRestoreSession.aidl", "core/java/android/app/backup/ISelectBackupTransportCallback.aidl", + "core/java/android/app/contentsuggestions/IClassificationsCallback.aidl", + "core/java/android/app/contentsuggestions/IContentSuggestionsManager.aidl", + "core/java/android/app/contentsuggestions/ISelectionsCallback.aidl", + "core/java/android/app/prediction/IPredictionCallback.aidl", + "core/java/android/app/prediction/IPredictionManager.aidl", "core/java/android/app/role/IOnRoleHoldersChangedListener.aidl", "core/java/android/app/role/IRoleManager.aidl", "core/java/android/app/role/IRoleManagerCallback.aidl", @@ -273,6 +278,7 @@ java_defaults { "core/java/android/rolecontrollerservice/IRoleControllerService.aidl", ":keystore_aidl", "core/java/android/security/keymaster/IKeyAttestationApplicationIdProvider.aidl", + "core/java/android/service/appprediction/IPredictionService.aidl", "core/java/android/service/autofill/augmented/IAugmentedAutofillService.aidl", "core/java/android/service/autofill/augmented/IFillCallback.aidl", "core/java/android/service/autofill/IAutoFillService.aidl", @@ -282,6 +288,7 @@ java_defaults { "core/java/android/service/carrier/ICarrierService.aidl", "core/java/android/service/carrier/ICarrierMessagingCallback.aidl", "core/java/android/service/carrier/ICarrierMessagingService.aidl", + "core/java/android/service/contentsuggestions/IContentSuggestionsService.aidl", "core/java/android/service/euicc/IDeleteSubscriptionCallback.aidl", "core/java/android/service/euicc/IDownloadSubscriptionCallback.aidl", "core/java/android/service/euicc/IEraseSubscriptionsCallback.aidl", diff --git a/api/current.txt b/api/current.txt index 8dad532b1e71..0ff1743c2a39 100644 --- a/api/current.txt +++ b/api/current.txt @@ -29748,9 +29748,6 @@ package android.net.wifi { method public void setReferenceCounted(boolean); } - public static abstract class WifiManager.NetworkSuggestionsStatusCode implements java.lang.annotation.Annotation { - } - public class WifiManager.WifiLock { method public void acquire(); method public boolean isHeld(); @@ -34440,6 +34437,8 @@ package android.os { method public java.util.ArrayList<java.lang.String> createStringArrayList(); method public <T> T[] createTypedArray(android.os.Parcelable.Creator<T>); method public <T> java.util.ArrayList<T> createTypedArrayList(android.os.Parcelable.Creator<T>); + method public <T extends android.os.Parcelable> android.util.ArrayMap<java.lang.String, T> createTypedArrayMap(android.os.Parcelable.Creator<T>); + method public <T extends android.os.Parcelable> android.util.SparseArray<T> createTypedSparseArray(android.os.Parcelable.Creator<T>); method public int dataAvail(); method public int dataCapacity(); method public int dataPosition(); @@ -34481,7 +34480,7 @@ package android.os { method public java.io.Serializable readSerializable(); method public android.util.Size readSize(); method public android.util.SizeF readSizeF(); - method public android.util.SparseArray readSparseArray(java.lang.ClassLoader); + method public <T> android.util.SparseArray<T> readSparseArray(java.lang.ClassLoader); method public android.util.SparseBooleanArray readSparseBooleanArray(); method public java.lang.String readString(); method public void readStringArray(java.lang.String[]); @@ -34527,7 +34526,7 @@ package android.os { method public void writeSerializable(java.io.Serializable); method public void writeSize(android.util.Size); method public void writeSizeF(android.util.SizeF); - method public void writeSparseArray(android.util.SparseArray<java.lang.Object>); + method public <T> void writeSparseArray(android.util.SparseArray<T>); method public void writeSparseBooleanArray(android.util.SparseBooleanArray); method public void writeString(java.lang.String); method public void writeStringArray(java.lang.String[]); @@ -34535,8 +34534,10 @@ package android.os { method public void writeStrongBinder(android.os.IBinder); method public void writeStrongInterface(android.os.IInterface); method public <T extends android.os.Parcelable> void writeTypedArray(T[], int); + method public <T extends android.os.Parcelable> void writeTypedArrayMap(android.util.ArrayMap<java.lang.String, T>, int); method public <T extends android.os.Parcelable> void writeTypedList(java.util.List<T>); method public <T extends android.os.Parcelable> void writeTypedObject(T, int); + method public <T extends android.os.Parcelable> void writeTypedSparseArray(android.util.SparseArray<T>, int); method public void writeValue(java.lang.Object); field public static final android.os.Parcelable.Creator<java.lang.String> STRING_CREATOR; } diff --git a/api/system-current.txt b/api/system-current.txt index 320a794cffaf..b190e9f5971f 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -86,11 +86,13 @@ package android { field public static final java.lang.String MANAGE_ACCESSIBILITY = "android.permission.MANAGE_ACCESSIBILITY"; field public static final java.lang.String MANAGE_ACTIVITY_STACKS = "android.permission.MANAGE_ACTIVITY_STACKS"; field public static final java.lang.String MANAGE_APP_OPS_RESTRICTIONS = "android.permission.MANAGE_APP_OPS_RESTRICTIONS"; + field public static final java.lang.String MANAGE_APP_PREDICTIONS = "android.permission.MANAGE_APP_PREDICTIONS"; field public static final java.lang.String MANAGE_APP_TOKENS = "android.permission.MANAGE_APP_TOKENS"; field public static final java.lang.String MANAGE_AUTO_FILL = "android.permission.MANAGE_AUTO_FILL"; field public static final java.lang.String MANAGE_CARRIER_OEM_UNLOCK_STATE = "android.permission.MANAGE_CARRIER_OEM_UNLOCK_STATE"; field public static final java.lang.String MANAGE_CA_CERTIFICATES = "android.permission.MANAGE_CA_CERTIFICATES"; field public static final java.lang.String MANAGE_CONTENT_CAPTURE = "android.permission.MANAGE_CONTENT_CAPTURE"; + field public static final java.lang.String MANAGE_CONTENT_SUGGESTIONS = "android.permission.MANAGE_CONTENT_SUGGESTIONS"; field public static final java.lang.String MANAGE_DEBUGGING = "android.permission.MANAGE_DEBUGGING"; field public static final java.lang.String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS"; field public static final java.lang.String MANAGE_IPSEC_TUNNELS = "android.permission.MANAGE_IPSEC_TUNNELS"; @@ -284,10 +286,11 @@ package android.app { } public class AppOpsManager { - method public java.util.List<android.app.AppOpsManager.HistoricalPackageOps> getAllHistoricPackagesOps(java.lang.String[], long, long); - method public android.app.AppOpsManager.HistoricalPackageOps getHistoricalPackagesOps(int, java.lang.String, java.lang.String[], long, long); + method public void getHistoricalOps(int, java.lang.String, java.lang.String[], long, long, java.util.concurrent.Executor, java.util.function.Consumer<android.app.AppOpsManager.HistoricalOps>); method public static java.lang.String[] getOpStrs(); - method public java.util.List<android.app.AppOpsManager.PackageOps> getOpsForPackage(int, java.lang.String, int[]); + method public deprecated java.util.List<android.app.AppOpsManager.PackageOps> getOpsForPackage(int, java.lang.String, int[]); + method public java.util.List<android.app.AppOpsManager.PackageOps> getOpsForPackage(int, java.lang.String, java.lang.String...); + method public java.util.List<android.app.AppOpsManager.PackageOps> getPackagesForOps(java.lang.String[]); method public static int opToDefaultMode(java.lang.String); method public static java.lang.String opToPermission(java.lang.String); method public void setMode(java.lang.String, int, java.lang.String, int); @@ -343,7 +346,7 @@ package android.app { field public static final int UID_STATE_TOP = 1; // 0x1 } - public static final class AppOpsManager.HistoricalOpEntry implements android.os.Parcelable { + public static final class AppOpsManager.HistoricalOp implements android.os.Parcelable { method public int describeContents(); method public long getAccessCount(int); method public long getAccessDuration(int); @@ -353,23 +356,43 @@ package android.app { method public long getForegroundAccessCount(); method public long getForegroundAccessDuration(); method public long getForegroundRejectCount(); - method public java.lang.String getOp(); + method public java.lang.String getOpName(); method public long getRejectCount(int); method public void writeToParcel(android.os.Parcel, int); - field public static final android.os.Parcelable.Creator<android.app.AppOpsManager.HistoricalOpEntry> CREATOR; + field public static final android.os.Parcelable.Creator<android.app.AppOpsManager.HistoricalOp> CREATOR; + } + + public static final class AppOpsManager.HistoricalOps implements android.os.Parcelable { + method public int describeContents(); + method public long getBeginTimeMillis(); + method public long getEndTimeMillis(); + method public int getUidCount(); + method public android.app.AppOpsManager.HistoricalUidOps getUidOps(int); + method public android.app.AppOpsManager.HistoricalUidOps getUidOpsAt(int); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.AppOpsManager.HistoricalOps> CREATOR; } public static final class AppOpsManager.HistoricalPackageOps implements android.os.Parcelable { method public int describeContents(); - method public android.app.AppOpsManager.HistoricalOpEntry getEntry(java.lang.String); - method public android.app.AppOpsManager.HistoricalOpEntry getEntryAt(int); - method public int getEntryCount(); + method public android.app.AppOpsManager.HistoricalOp getOp(java.lang.String); + method public android.app.AppOpsManager.HistoricalOp getOpAt(int); + method public int getOpCount(); method public java.lang.String getPackageName(); - method public int getUid(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.app.AppOpsManager.HistoricalPackageOps> CREATOR; } + public static final class AppOpsManager.HistoricalUidOps implements android.os.Parcelable { + method public int describeContents(); + method public int getPackageCount(); + method public android.app.AppOpsManager.HistoricalPackageOps getPackageOps(java.lang.String); + method public android.app.AppOpsManager.HistoricalPackageOps getPackageOpsAt(int); + method public int getUid(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.AppOpsManager.HistoricalUidOps> CREATOR; + } + public static final class AppOpsManager.OpEntry implements android.os.Parcelable { method public int describeContents(); method public int getDuration(); @@ -848,6 +871,73 @@ package android.app.backup { } +package android.app.contentsuggestions { + + public final class ClassificationsRequest implements android.os.Parcelable { + method public int describeContents(); + method public android.os.Bundle getExtras(); + method public java.util.List<android.app.contentsuggestions.ContentSelection> getSelections(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.contentsuggestions.ClassificationsRequest> CREATOR; + } + + public static final class ClassificationsRequest.Builder { + ctor public ClassificationsRequest.Builder(java.util.List<android.app.contentsuggestions.ContentSelection>); + method public android.app.contentsuggestions.ClassificationsRequest build(); + method public android.app.contentsuggestions.ClassificationsRequest.Builder setExtras(android.os.Bundle); + } + + public final class ContentClassification implements android.os.Parcelable { + ctor public ContentClassification(java.lang.String, android.os.Bundle); + method public int describeContents(); + method public android.os.Bundle getExtras(); + method public java.lang.String getId(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.contentsuggestions.ContentClassification> CREATOR; + } + + public final class ContentSelection implements android.os.Parcelable { + ctor public ContentSelection(java.lang.String, android.os.Bundle); + method public int describeContents(); + method public android.os.Bundle getExtras(); + method public java.lang.String getId(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.contentsuggestions.ContentSelection> CREATOR; + } + + public final class ContentSuggestionsManager { + method public void classifyContentSelections(android.app.contentsuggestions.ClassificationsRequest, java.util.concurrent.Executor, android.app.contentsuggestions.ContentSuggestionsManager.ClassificationsCallback); + method public void notifyInteraction(java.lang.String, android.os.Bundle); + method public void provideContextImage(int, android.os.Bundle); + method public void suggestContentSelections(android.app.contentsuggestions.SelectionsRequest, java.util.concurrent.Executor, android.app.contentsuggestions.ContentSuggestionsManager.SelectionsCallback); + } + + public static abstract interface ContentSuggestionsManager.ClassificationsCallback { + method public abstract void onContentClassificationsAvailable(int, java.util.List<android.app.contentsuggestions.ContentClassification>); + } + + public static abstract interface ContentSuggestionsManager.SelectionsCallback { + method public abstract void onContentSelectionsAvailable(int, java.util.List<android.app.contentsuggestions.ContentSelection>); + } + + public final class SelectionsRequest implements android.os.Parcelable { + method public int describeContents(); + method public android.os.Bundle getExtras(); + method public android.graphics.Point getInterestPoint(); + method public int getTaskId(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.contentsuggestions.SelectionsRequest> CREATOR; + } + + public static final class SelectionsRequest.Builder { + ctor public SelectionsRequest.Builder(int); + method public android.app.contentsuggestions.SelectionsRequest build(); + method public android.app.contentsuggestions.SelectionsRequest.Builder setExtras(android.os.Bundle); + method public android.app.contentsuggestions.SelectionsRequest.Builder setInterestPoint(android.graphics.Point); + } + +} + package android.app.job { public abstract class JobScheduler { @@ -856,6 +946,87 @@ package android.app.job { } +package android.app.prediction { + + public final class AppPredictionContext implements android.os.Parcelable { + method public int describeContents(); + method public android.os.Bundle getExtras(); + method public java.lang.String getPackageName(); + method public int getPredictedTargetCount(); + method public java.lang.String getUiSurface(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.prediction.AppPredictionContext> CREATOR; + } + + public static final class AppPredictionContext.Builder { + method public android.app.prediction.AppPredictionContext build(); + method public android.app.prediction.AppPredictionContext.Builder setExtras(android.os.Bundle); + method public android.app.prediction.AppPredictionContext.Builder setPredictedTargetCount(int); + method public android.app.prediction.AppPredictionContext.Builder setUiSurface(java.lang.String); + } + + public final class AppPredictionManager { + method public android.app.prediction.AppPredictor createAppPredictionSession(android.app.prediction.AppPredictionContext); + } + + public final class AppPredictionSessionId implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.prediction.AppPredictionSessionId> CREATOR; + } + + public final class AppPredictor { + method public void destroy(); + method public void notifyAppTargetEvent(android.app.prediction.AppTargetEvent); + method public void notifyLocationShown(java.lang.String, java.util.List<android.app.prediction.AppTargetId>); + method public void registerPredictionUpdates(java.util.concurrent.Executor, android.app.prediction.AppPredictor.Callback); + method public void requestPredictionUpdate(); + method public void sortTargets(java.util.List<android.app.prediction.AppTarget>, java.util.concurrent.Executor, java.util.function.Consumer<java.util.List<android.app.prediction.AppTarget>>); + method public void unregisterPredictionUpdates(android.app.prediction.AppPredictor.Callback); + } + + public static abstract interface AppPredictor.Callback { + method public abstract void onTargetsAvailable(java.util.List<android.app.prediction.AppTarget>); + } + + public final class AppTarget implements android.os.Parcelable { + method public int describeContents(); + method public java.lang.String getClassName(); + method public android.app.prediction.AppTargetId getId(); + method public java.lang.String getPackageName(); + method public int getRank(); + method public android.content.pm.ShortcutInfo getShortcutInfo(); + method public android.os.UserHandle getUser(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.prediction.AppTarget> CREATOR; + } + + public final class AppTargetEvent implements android.os.Parcelable { + method public int describeContents(); + method public int getAction(); + method public java.lang.String getLaunchLocation(); + method public android.app.prediction.AppTarget getTarget(); + method public void writeToParcel(android.os.Parcel, int); + field public static final int ACTION_DISMISS = 2; // 0x2 + field public static final int ACTION_LAUNCH = 1; // 0x1 + field public static final int ACTION_PIN = 3; // 0x3 + field public static final android.os.Parcelable.Creator<android.app.prediction.AppTargetEvent> CREATOR; + } + + public static final class AppTargetEvent.Builder { + ctor public AppTargetEvent.Builder(android.app.prediction.AppTarget, int); + method public android.app.prediction.AppTargetEvent build(); + method public android.app.prediction.AppTargetEvent.Builder setLaunchLocation(java.lang.String); + } + + public final class AppTargetId implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.prediction.AppTargetId> CREATOR; + } + +} + package android.app.role { public abstract interface OnRoleHoldersChangedListener { @@ -1042,7 +1213,9 @@ package android.content { method public abstract void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle, java.lang.String, android.os.Bundle); method public abstract void sendOrderedBroadcast(android.content.Intent, java.lang.String, android.os.Bundle, android.content.BroadcastReceiver, android.os.Handler, int, java.lang.String, android.os.Bundle); method public void startActivityAsUser(android.content.Intent, android.os.UserHandle); + field public static final java.lang.String APP_PREDICTION_SERVICE = "app_prediction"; field public static final java.lang.String BACKUP_SERVICE = "backup"; + field public static final java.lang.String CONTENT_SUGGESTIONS_SERVICE = "content_suggestions"; field public static final java.lang.String CONTEXTHUB_SERVICE = "contexthub"; field public static final java.lang.String EUICC_CARD_SERVICE = "euicc_card"; field public static final java.lang.String HDMI_CONTROL_SERVICE = "hdmi_control"; @@ -1128,6 +1301,7 @@ package android.content { package android.content.pm { public class ApplicationInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable { + method public boolean isEncryptionAware(); method public boolean isInstantApp(); field public java.lang.String credentialProtectedDataDir; field public int targetSandboxVersion; @@ -1548,6 +1722,10 @@ package android.hardware.display { field public static final android.os.Parcelable.Creator<android.hardware.display.BrightnessCorrection> CREATOR; } + public final class ColorDisplayManager { + method public boolean setSaturationLevel(int); + } + public final class DisplayManager { method public java.util.List<android.hardware.display.AmbientBrightnessDayStats> getAmbientBrightnessStats(); method public android.hardware.display.BrightnessConfiguration getBrightnessConfiguration(); @@ -1556,7 +1734,7 @@ package android.hardware.display { method public android.util.Pair<float[], float[]> getMinimumBrightnessCurve(); method public android.graphics.Point getStableDisplaySize(); method public void setBrightnessConfiguration(android.hardware.display.BrightnessConfiguration); - method public void setSaturationLevel(float); + method public deprecated void setSaturationLevel(float); } } @@ -5162,6 +5340,24 @@ package android.security.keystore.recovery { } +package android.service.appprediction { + + public abstract class AppPredictionService extends android.app.Service { + ctor public AppPredictionService(); + method public abstract void onAppTargetEvent(android.app.prediction.AppPredictionSessionId, android.app.prediction.AppTargetEvent); + method public final android.os.IBinder onBind(android.content.Intent); + method public void onCreatePredictionSession(android.app.prediction.AppPredictionContext, android.app.prediction.AppPredictionSessionId); + method public void onDestroyPredictionSession(android.app.prediction.AppPredictionSessionId); + method public abstract void onLocationShown(android.app.prediction.AppPredictionSessionId, java.lang.String, java.util.List<android.app.prediction.AppTargetId>); + method public abstract void onRequestPredictionUpdate(android.app.prediction.AppPredictionSessionId); + method public abstract void onSortAppTargets(android.app.prediction.AppPredictionSessionId, java.util.List<android.app.prediction.AppTarget>, android.os.CancellationSignal, java.util.function.Consumer<java.util.List<android.app.prediction.AppTarget>>); + method public void onStartPredictionUpdates(); + method public void onStopPredictionUpdates(); + method public final void updatePredictions(android.app.prediction.AppPredictionSessionId, java.util.List<android.app.prediction.AppTarget>); + } + +} + package android.service.autofill { public abstract class AutofillFieldClassificationService extends android.app.Service { @@ -5290,6 +5486,19 @@ package android.service.contentcapture { } +package android.service.contentsuggestions { + + public abstract class ContentSuggestionsService extends android.app.Service { + ctor public ContentSuggestionsService(); + method public abstract void classifyContentSelections(android.app.contentsuggestions.ClassificationsRequest, android.app.contentsuggestions.ContentSuggestionsManager.ClassificationsCallback); + method public abstract void notifyInteraction(java.lang.String, android.os.Bundle); + method public abstract void processContextImage(int, android.graphics.Bitmap, android.os.Bundle); + method public abstract void suggestContentSelections(android.app.contentsuggestions.SelectionsRequest, android.app.contentsuggestions.ContentSuggestionsManager.SelectionsCallback); + field public static final java.lang.String SERVICE_INTERFACE = "android.service.contentsuggestions.ContentSuggestionsService"; + } + +} + package android.service.euicc { public final class DownloadSubscriptionResult implements android.os.Parcelable { diff --git a/api/test-current.txt b/api/test-current.txt index ae3c1e027f51..575875dddad7 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -89,18 +89,26 @@ package android.app { } public class AppOpsManager { - method public java.util.List<android.app.AppOpsManager.HistoricalPackageOps> getAllHistoricPackagesOps(java.lang.String[], long, long); - method public android.app.AppOpsManager.HistoricalPackageOps getHistoricalPackagesOps(int, java.lang.String, java.lang.String[], long, long); + method public void addHistoricalOps(android.app.AppOpsManager.HistoricalOps); + method public void clearHistory(); + method public void getHistoricalOps(int, java.lang.String, java.lang.String[], long, long, java.util.concurrent.Executor, java.util.function.Consumer<android.app.AppOpsManager.HistoricalOps>); + method public void getHistoricalOpsFromDiskRaw(int, java.lang.String, java.lang.String[], long, long, java.util.concurrent.Executor, java.util.function.Consumer<android.app.AppOpsManager.HistoricalOps>); method public static int getNumOps(); method public static java.lang.String[] getOpStrs(); method public boolean isOperationActive(int, int, java.lang.String); + method public void offsetHistory(long); method public static java.lang.String opToPermission(int); method public static int permissionToOpCode(java.lang.String); + method public void resetHistoryParameters(); + method public void setHistoryParameters(int, long, int); method public void setMode(int, int, java.lang.String, int); method public void setUidMode(java.lang.String, int, int); method public void startWatchingActive(int[], android.app.AppOpsManager.OnOpActiveChangedListener); method public void stopWatchingActive(android.app.AppOpsManager.OnOpActiveChangedListener); method public static int strOpToOp(java.lang.String); + field public static final int HISTORICAL_MODE_DISABLED = 0; // 0x0 + field public static final int HISTORICAL_MODE_ENABLED_ACTIVE = 1; // 0x1 + field public static final int HISTORICAL_MODE_ENABLED_PASSIVE = 2; // 0x2 field public static final java.lang.String OPSTR_ACCEPT_HANDOVER = "android:accept_handover"; field public static final java.lang.String OPSTR_ACCESS_NOTIFICATIONS = "android:access_notifications"; field public static final java.lang.String OPSTR_ACTIVATE_VPN = "android:activate_vpn"; @@ -144,11 +152,18 @@ package android.app { field public static final java.lang.String OPSTR_WRITE_ICC_SMS = "android:write_icc_sms"; field public static final java.lang.String OPSTR_WRITE_SMS = "android:write_sms"; field public static final java.lang.String OPSTR_WRITE_WALLPAPER = "android:write_wallpaper"; + field public static final int OP_COARSE_LOCATION = 0; // 0x0 field public static final int OP_RECORD_AUDIO = 27; // 0x1b field public static final int OP_SYSTEM_ALERT_WINDOW = 24; // 0x18 + field public static final int UID_STATE_BACKGROUND = 4; // 0x4 + field public static final int UID_STATE_CACHED = 5; // 0x5 + field public static final int UID_STATE_FOREGROUND = 3; // 0x3 + field public static final int UID_STATE_FOREGROUND_SERVICE = 2; // 0x2 + field public static final int UID_STATE_PERSISTENT = 0; // 0x0 + field public static final int UID_STATE_TOP = 1; // 0x1 } - public static final class AppOpsManager.HistoricalOpEntry implements android.os.Parcelable { + public static final class AppOpsManager.HistoricalOp implements android.os.Parcelable { method public int describeContents(); method public long getAccessCount(int); method public long getAccessDuration(int); @@ -158,23 +173,48 @@ package android.app { method public long getForegroundAccessCount(); method public long getForegroundAccessDuration(); method public long getForegroundRejectCount(); - method public java.lang.String getOp(); + method public java.lang.String getOpName(); method public long getRejectCount(int); method public void writeToParcel(android.os.Parcel, int); - field public static final android.os.Parcelable.Creator<android.app.AppOpsManager.HistoricalOpEntry> CREATOR; + field public static final android.os.Parcelable.Creator<android.app.AppOpsManager.HistoricalOp> CREATOR; + } + + public static final class AppOpsManager.HistoricalOps implements android.os.Parcelable { + ctor public AppOpsManager.HistoricalOps(long, long); + method public int describeContents(); + method public long getBeginTimeMillis(); + method public long getEndTimeMillis(); + method public int getUidCount(); + method public android.app.AppOpsManager.HistoricalUidOps getUidOps(int); + method public android.app.AppOpsManager.HistoricalUidOps getUidOpsAt(int); + method public void increaseAccessCount(int, int, java.lang.String, int, long); + method public void increaseAccessDuration(int, int, java.lang.String, int, long); + method public void increaseRejectCount(int, int, java.lang.String, int, long); + method public void offsetBeginAndEndTime(long); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.AppOpsManager.HistoricalOps> CREATOR; } public static final class AppOpsManager.HistoricalPackageOps implements android.os.Parcelable { method public int describeContents(); - method public android.app.AppOpsManager.HistoricalOpEntry getEntry(java.lang.String); - method public android.app.AppOpsManager.HistoricalOpEntry getEntryAt(int); - method public int getEntryCount(); + method public android.app.AppOpsManager.HistoricalOp getOp(java.lang.String); + method public android.app.AppOpsManager.HistoricalOp getOpAt(int); + method public int getOpCount(); method public java.lang.String getPackageName(); - method public int getUid(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.app.AppOpsManager.HistoricalPackageOps> CREATOR; } + public static final class AppOpsManager.HistoricalUidOps implements android.os.Parcelable { + method public int describeContents(); + method public int getPackageCount(); + method public android.app.AppOpsManager.HistoricalPackageOps getPackageOps(java.lang.String); + method public android.app.AppOpsManager.HistoricalPackageOps getPackageOpsAt(int); + method public int getUid(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.AppOpsManager.HistoricalUidOps> CREATOR; + } + public static abstract interface AppOpsManager.OnOpActiveChangedListener { method public abstract void onOpActiveChanged(int, int, java.lang.String, boolean); } diff --git a/cmds/idmap2/idmap2/Scan.cpp b/cmds/idmap2/idmap2/Scan.cpp index 49187475ccdb..4f88127f8af1 100644 --- a/cmds/idmap2/idmap2/Scan.cpp +++ b/cmds/idmap2/idmap2/Scan.cpp @@ -39,6 +39,17 @@ using android::idmap2::ZipFile; using android::idmap2::utils::FindFiles; namespace { + +struct InputOverlay { + bool operator<(InputOverlay const& rhs) const { + return priority < rhs.priority || (priority == rhs.priority && apk_path < rhs.apk_path); + } + + std::string apk_path; // NOLINT(misc-non-private-member-variables-in-classes) + std::string idmap_path; // NOLINT(misc-non-private-member-variables-in-classes) + int priority; // NOLINT(misc-non-private-member-variables-in-classes) +}; + std::unique_ptr<std::vector<std::string>> FindApkFiles(const std::vector<std::string>& dirs, bool recursive, std::ostream& out_error) { const auto predicate = [](unsigned char type, const std::string& path) -> bool { @@ -58,6 +69,7 @@ std::unique_ptr<std::vector<std::string>> FindApkFiles(const std::vector<std::st } return std::make_unique<std::vector<std::string>>(paths.cbegin(), paths.cend()); } + } // namespace bool Scan(const std::vector<std::string>& args, std::ostream& out_error) { @@ -87,7 +99,7 @@ bool Scan(const std::vector<std::string>& args, std::ostream& out_error) { return false; } - std::vector<std::string> interesting_apks; + std::vector<InputOverlay> interesting_apks; for (const std::string& path : *apk_paths) { std::unique_ptr<const ZipFile> zip = ZipFile::Open(path); if (!zip) { @@ -132,27 +144,29 @@ bool Scan(const std::vector<std::string>& args, std::ostream& out_error) { continue; } + // Sort the static overlays in ascending priority order + std::string idmap_path = Idmap::CanonicalIdmapPathFor(output_directory, path); + InputOverlay input{path, idmap_path, priority}; interesting_apks.insert( - std::lower_bound(interesting_apks.begin(), interesting_apks.end(), path), path); + std::lower_bound(interesting_apks.begin(), interesting_apks.end(), input), input); } std::stringstream stream; - for (const auto& apk : interesting_apks) { - const std::string idmap_path = Idmap::CanonicalIdmapPathFor(output_directory, apk); + for (const auto& overlay : interesting_apks) { std::stringstream dev_null; - if (!Verify(std::vector<std::string>({"--idmap-path", idmap_path}), dev_null) && + if (!Verify(std::vector<std::string>({"--idmap-path", overlay.idmap_path}), dev_null) && !Create(std::vector<std::string>({ "--target-apk-path", target_apk_path, "--overlay-apk-path", - apk, + overlay.apk_path, "--idmap-path", - idmap_path, + overlay.idmap_path, }), out_error)) { return false; } - stream << idmap_path << std::endl; + stream << overlay.idmap_path << std::endl; } std::cout << stream.str(); diff --git a/cmds/idmap2/static-checks.sh b/cmds/idmap2/static-checks.sh index 560ccb692bb1..ad9830b8a096 100755 --- a/cmds/idmap2/static-checks.sh +++ b/cmds/idmap2/static-checks.sh @@ -70,7 +70,15 @@ function _bpfmt() function _cpplint() { local cpplint="${ANDROID_BUILD_TOP}/tools/repohooks/tools/cpplint.py" - $cpplint --quiet $cpp_files + local output="$($cpplint --quiet $cpp_files 2>&1 >/dev/null | grep -v \ + -e 'Found C system header after C++ system header.' \ + -e 'Unknown NOLINT error category: misc-non-private-member-variables-in-classes' \ + )" + if [[ "$output" ]]; then + echo "$output" + return 1 + fi + return 0 } function _parse_args() diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 8faf08a77355..7767f0491a16 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -271,6 +271,13 @@ public final class ActivityThread extends ClientTransactionHandler { @UnsupportedAppUsage final H mH = new H(); final Executor mExecutor = new HandlerExecutor(mH); + /** + * Maps from activity token to local record of running activities in this process. + * + * This variable is readable if the code is running in activity thread or holding {@link + * #mResourcesManager}. It's only writable if the code is running in activity thread and holding + * {@link #mResourcesManager}. + */ @UnsupportedAppUsage final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>(); /** The activities to be truly destroyed (not include relaunch). */ @@ -434,6 +441,10 @@ public final class ActivityThread extends ClientTransactionHandler { Configuration newConfig; Configuration createdConfig; Configuration overrideConfig; + // Used to save the last reported configuration from server side so that activity + // configuration transactions can always use the latest configuration. + @GuardedBy("this") + private Configuration mPendingOverrideConfig; // Used for consolidating configs before sending on to Activity. private Configuration tmpConfig = new Configuration(); // Callback used for updating activity override config. @@ -3064,7 +3075,12 @@ public final class ActivityThread extends ClientTransactionHandler { } r.setState(ON_CREATE); - mActivities.put(r.token, r); + // updatePendingActivityConfiguration() reads from mActivities to update + // ActivityClientRecord which runs in a different thread. Protect modifications to + // mActivities to avoid race. + synchronized (mResourcesManager) { + mActivities.put(r.token, r); + } } catch (SuperNotCalledException e) { throw e; @@ -4639,7 +4655,12 @@ public final class ActivityThread extends ClientTransactionHandler { r.setState(ON_DESTROY); } schedulePurgeIdler(); - mActivities.remove(token); + // updatePendingActivityConfiguration() reads from mActivities to update + // ActivityClientRecord which runs in a different thread. Protect modifications to + // mActivities to avoid race. + synchronized (mResourcesManager) { + mActivities.remove(token); + } StrictMode.decrementExpectedActivityCount(activityClass); return r; } @@ -5382,6 +5403,26 @@ public final class ActivityThread extends ClientTransactionHandler { } } + @Override + public void updatePendingActivityConfiguration(IBinder activityToken, + Configuration overrideConfig) { + final ActivityClientRecord r; + synchronized (mResourcesManager) { + r = mActivities.get(activityToken); + } + + if (r == null) { + if (DEBUG_CONFIGURATION) { + Slog.w(TAG, "Not found target activity to update its pending config."); + } + return; + } + + synchronized (r) { + r.mPendingOverrideConfig = overrideConfig; + } + } + /** * Handle new activity configuration and/or move to a different display. * @param activityToken Target activity token. @@ -5401,6 +5442,24 @@ public final class ActivityThread extends ClientTransactionHandler { final boolean movedToDifferentDisplay = displayId != INVALID_DISPLAY && displayId != r.activity.getDisplayId(); + synchronized (r) { + if (r.mPendingOverrideConfig != null + && !r.mPendingOverrideConfig.isOtherSeqNewer(overrideConfig)) { + overrideConfig = r.mPendingOverrideConfig; + } + r.mPendingOverrideConfig = null; + } + + if (r.overrideConfig != null && !r.overrideConfig.isOtherSeqNewer(overrideConfig) + && !movedToDifferentDisplay) { + if (DEBUG_CONFIGURATION) { + Slog.v(TAG, "Activity already handled newer configuration so drop this" + + " transaction. overrideConfig=" + overrideConfig + " r.overrideConfig=" + + r.overrideConfig); + } + return; + } + // Perform updates. r.overrideConfig = overrideConfig; final ViewRootImpl viewRoot = r.activity.mDecor != null diff --git a/core/java/android/app/AppOpsManager.aidl b/core/java/android/app/AppOpsManager.aidl index 9329fbc83376..224030202e08 100644 --- a/core/java/android/app/AppOpsManager.aidl +++ b/core/java/android/app/AppOpsManager.aidl @@ -19,5 +19,7 @@ package android.app; parcelable AppOpsManager.PackageOps; parcelable AppOpsManager.OpEntry; +parcelable AppOpsManager.HistoricalOp; +parcelable AppOpsManager.HistoricalOps; parcelable AppOpsManager.HistoricalPackageOps; -parcelable AppOpsManager.HistoricalOpEntry; +parcelable AppOpsManager.HistoricalUidOps; diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 78fe0024b0b0..e155fe201f98 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -36,26 +36,33 @@ import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.Process; +import android.os.RemoteCallback; import android.os.RemoteException; import android.os.UserManager; import android.provider.Settings; import android.util.ArrayMap; import com.android.internal.annotations.GuardedBy; +import android.util.SparseArray; import com.android.internal.app.IAppOpsActiveCallback; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsNotedCallback; import com.android.internal.app.IAppOpsService; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * API for interacting with "application operation" tracking. @@ -106,6 +113,51 @@ public class AppOpsManager { static IBinder sToken; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = { "HISTORICAL_MODE_" }, value = { + HISTORICAL_MODE_DISABLED, + HISTORICAL_MODE_ENABLED_ACTIVE, + HISTORICAL_MODE_ENABLED_PASSIVE + }) + public @interface HistoricalMode {} + + /** + * Mode in which app op history is completely disabled. + * @hide + */ + @TestApi + public static final int HISTORICAL_MODE_DISABLED = 0; + + /** + * Mode in which app op history is enabled and app ops performed by apps would + * be tracked. This is the mode in which the feature is completely enabled. + * @hide + */ + @TestApi + public static final int HISTORICAL_MODE_ENABLED_ACTIVE = 1; + + /** + * Mode in which app op history is enabled but app ops performed by apps would + * not be tracked and the only way to add ops to the history is via explicit calls + * to dedicated APIs. This mode is useful for testing to allow full control of + * the historical content. + * @hide + */ + @TestApi + public static final int HISTORICAL_MODE_ENABLED_PASSIVE = 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = { "MODE_" }, value = { + MODE_ALLOWED, + MODE_IGNORED, + MODE_ERRORED, + MODE_DEFAULT, + MODE_FOREGROUND + }) + public @interface Mode {} + /** * Result from {@link #checkOp}, {@link #noteOp}, {@link #startOp}: the given caller is * allowed to perform the given operation. @@ -159,7 +211,6 @@ public class AppOpsManager { "foreground", // MODE_FOREGROUND }; - /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, prefix = { "UID_STATE_" }, value = { @@ -173,9 +224,16 @@ public class AppOpsManager { public @interface UidState {} /** + * Invalid UID state. + * @hide + */ + public static final int UID_STATE_INVALID = -1; + + /** * Metrics about an op when its uid is persistent. * @hide */ + @TestApi @SystemApi public static final int UID_STATE_PERSISTENT = 0; @@ -183,6 +241,7 @@ public class AppOpsManager { * Metrics about an op when its uid is at the top. * @hide */ + @TestApi @SystemApi public static final int UID_STATE_TOP = 1; @@ -190,6 +249,7 @@ public class AppOpsManager { * Metrics about an op when its uid is running a foreground service. * @hide */ + @TestApi @SystemApi public static final int UID_STATE_FOREGROUND_SERVICE = 2; @@ -203,6 +263,7 @@ public class AppOpsManager { * Metrics about an op when its uid is in the foreground for any other reasons. * @hide */ + @TestApi @SystemApi public static final int UID_STATE_FOREGROUND = 3; @@ -210,6 +271,7 @@ public class AppOpsManager { * Metrics about an op when its uid is in the background for any reason. * @hide */ + @TestApi @SystemApi public static final int UID_STATE_BACKGROUND = 4; @@ -217,6 +279,7 @@ public class AppOpsManager { * Metrics about an op when its uid is cached. * @hide */ + @TestApi @SystemApi public static final int UID_STATE_CACHED = 5; @@ -237,7 +300,7 @@ public class AppOpsManager { @UnsupportedAppUsage public static final int OP_NONE = -1; /** @hide Access to coarse location information. */ - @UnsupportedAppUsage + @TestApi public static final int OP_COARSE_LOCATION = 0; /** @hide Access to fine location information. */ @UnsupportedAppUsage @@ -1645,6 +1708,9 @@ public class AppOpsManager { } } + /** @hide */ + public static final String KEY_HISTORICAL_OPS = "historical_ops"; + /** * Retrieve the op switch that controls the given operation. * @hide @@ -1731,7 +1797,7 @@ public class AppOpsManager { * Retrieve the default mode for the operation. * @hide */ - public static int opToDefaultMode(int op) { + public static @Mode int opToDefaultMode(int op) { // STOPSHIP b/118520006: Hardcode the default values once the feature is stable. switch (op) { // SMS permissions @@ -1795,7 +1861,7 @@ public class AppOpsManager { * Retrieve the human readable mode. * @hide */ - public static String modeToName(int mode) { + public static String modeToName(@Mode int mode) { if (mode >= 0 && mode < MODE_NAMES.length) { return MODE_NAMES[mode]; } @@ -1885,7 +1951,7 @@ public class AppOpsManager { @SystemApi public static final class OpEntry implements Parcelable { private final int mOp; - private final int mMode; + private final @Mode int mMode; private final long[] mTimes; private final long[] mRejectTimes; private final int mDuration; @@ -1896,7 +1962,7 @@ public class AppOpsManager { /** * @hide */ - public OpEntry(int op, int mode, long time, long rejectTime, int duration, + public OpEntry(int op, @Mode int mode, long time, long rejectTime, int duration, int proxyUid, String proxyPackage) { mOp = op; mMode = mode; @@ -1913,7 +1979,7 @@ public class AppOpsManager { /** * @hide */ - public OpEntry(int op, int mode, long[] times, long[] rejectTimes, int duration, + public OpEntry(int op, @Mode int mode, long[] times, long[] rejectTimes, int duration, boolean running, int proxyUid, String proxyPackage) { mOp = op; mMode = mode; @@ -1930,7 +1996,7 @@ public class AppOpsManager { /** * @hide */ - public OpEntry(int op, int mode, long[] times, long[] rejectTimes, int duration, + public OpEntry(int op, @Mode int mode, long[] times, long[] rejectTimes, int duration, int proxyUid, String proxyPackage) { this(op, mode, times, rejectTimes, duration, duration == -1, proxyUid, proxyPackage); } @@ -1953,7 +2019,7 @@ public class AppOpsManager { /** * Return this entry's current mode, such as {@link #MODE_ALLOWED}. */ - public int getMode() { + public @Mode int getMode() { return mMode; } @@ -2086,83 +2152,813 @@ public class AppOpsManager { }; } + /** @hide */ + public interface HistoricalOpsVisitor { + void visitHistoricalOps(@NonNull HistoricalOps ops); + void visitHistoricalUidOps(@NonNull HistoricalUidOps ops); + void visitHistoricalPackageOps(@NonNull HistoricalPackageOps ops); + void visitHistoricalOp(@NonNull HistoricalOp ops); + } + /** - * This class represents historical app op information about a package. The history - * is aggregated information about ops for a certain amount of time such - * as the times the op was accessed, the times the op was rejected, the total - * duration the app op has been accessed. + * This class represents historical app op state of all UIDs for a given time interval. * * @hide */ @TestApi @SystemApi - public static final class HistoricalPackageOps implements Parcelable { - private final int mUid; - private final @NonNull String mPackageName; - private final @NonNull List<HistoricalOpEntry> mEntries; + public static final class HistoricalOps implements Parcelable { + private long mBeginTimeMillis; + private long mEndTimeMillis; + private @Nullable SparseArray<HistoricalUidOps> mHistoricalUidOps; + + /** @hide */ + @TestApi + public HistoricalOps(long beginTimeMillis, long endTimeMillis) { + Preconditions.checkState(beginTimeMillis <= endTimeMillis); + mBeginTimeMillis = beginTimeMillis; + mEndTimeMillis = endTimeMillis; + } + + /** @hide */ + public HistoricalOps(@NonNull HistoricalOps other) { + mBeginTimeMillis = other.mBeginTimeMillis; + mEndTimeMillis = other.mEndTimeMillis; + Preconditions.checkState(mBeginTimeMillis <= mEndTimeMillis); + if (other.mHistoricalUidOps != null) { + final int opCount = other.getUidCount(); + for (int i = 0; i < opCount; i++) { + final HistoricalUidOps origOps = other.getUidOpsAt(i); + final HistoricalUidOps clonedOps = new HistoricalUidOps(origOps); + if (mHistoricalUidOps == null) { + mHistoricalUidOps = new SparseArray<>(opCount); + } + mHistoricalUidOps.put(clonedOps.getUid(), clonedOps); + } + } + } + + private HistoricalOps(Parcel parcel) { + mBeginTimeMillis = parcel.readLong(); + mEndTimeMillis = parcel.readLong(); + final int[] uids = parcel.createIntArray(); + if (!ArrayUtils.isEmpty(uids)) { + final ParceledListSlice<HistoricalUidOps> listSlice = parcel.readParcelable( + HistoricalOps.class.getClassLoader()); + final List<HistoricalUidOps> uidOps = (listSlice != null) + ? listSlice.getList() : null; + if (uidOps == null) { + return; + } + for (int i = 0; i < uids.length; i++) { + if (mHistoricalUidOps == null) { + mHistoricalUidOps = new SparseArray<>(); + } + mHistoricalUidOps.put(uids[i], uidOps.get(i)); + } + } + } /** + * Splice a piece from the beginning of these ops. + * + * @param splicePoint The fraction of the data to be spliced off. + * * @hide */ - public HistoricalPackageOps(int uid, @NonNull String packageName) { - mUid = uid; - mPackageName = packageName; - mEntries = new ArrayList<>(); + public @NonNull HistoricalOps spliceFromBeginning(double splicePoint) { + return splice(splicePoint, true); } - HistoricalPackageOps(@NonNull Parcel parcel) { - mUid = parcel.readInt(); - mPackageName = parcel.readString(); - mEntries = parcel.createTypedArrayList(HistoricalOpEntry.CREATOR); + /** + * Splice a piece from the end of these ops. + * + * @param fractionToRemove The fraction of the data to be spliced off. + * + * @hide + */ + public @NonNull HistoricalOps spliceFromEnd(double fractionToRemove) { + return splice(fractionToRemove, false); } /** + * Splice a piece from the beginning or end of these ops. + * + * @param fractionToRemove The fraction of the data to be spliced off. + * @param beginning Whether to splice off the beginning or the end. + * + * @return The spliced off part. + * * @hide */ - public void addEntry(@NonNull HistoricalOpEntry entry) { - mEntries.add(entry); + private @Nullable HistoricalOps splice(double fractionToRemove, boolean beginning) { + final long spliceBeginTimeMills; + final long spliceEndTimeMills; + if (beginning) { + spliceBeginTimeMills = mBeginTimeMillis; + spliceEndTimeMills = (long) (mBeginTimeMillis + + getDurationMillis() * fractionToRemove); + mBeginTimeMillis = spliceEndTimeMills; + } else { + spliceBeginTimeMills = (long) (mEndTimeMillis + - getDurationMillis() * fractionToRemove); + spliceEndTimeMills = mEndTimeMillis; + mEndTimeMillis = spliceBeginTimeMills; + } + + HistoricalOps splice = null; + final int uidCount = getUidCount(); + for (int i = 0; i < uidCount; i++) { + final HistoricalUidOps origOps = getUidOpsAt(i); + final HistoricalUidOps spliceOps = origOps.splice(fractionToRemove); + if (spliceOps != null) { + if (splice == null) { + splice = new HistoricalOps(spliceBeginTimeMills, spliceEndTimeMills); + } + if (splice.mHistoricalUidOps == null) { + splice.mHistoricalUidOps = new SparseArray<>(); + } + splice.mHistoricalUidOps.put(spliceOps.getUid(), spliceOps); + } + } + return splice; } /** - * Gets the package name which the data represents. + * Merge the passed ops into the current ones. The time interval is a + * union of the current and passed in one and the passed in data is + * folded into the data of this instance. * - * @return The package name which the data represents. + * @hide */ - public @NonNull String getPackageName() { - return mPackageName; + public void merge(@NonNull HistoricalOps other) { + mBeginTimeMillis = Math.min(mBeginTimeMillis, other.mBeginTimeMillis); + mEndTimeMillis = Math.max(mEndTimeMillis, other.mEndTimeMillis); + final int uidCount = other.getUidCount(); + for (int i = 0; i < uidCount; i++) { + final HistoricalUidOps otherUidOps = other.getUidOpsAt(i); + final HistoricalUidOps thisUidOps = getUidOps(otherUidOps.getUid()); + if (thisUidOps != null) { + thisUidOps.merge(otherUidOps); + } else { + if (mHistoricalUidOps == null) { + mHistoricalUidOps = new SparseArray<>(); + } + mHistoricalUidOps.put(otherUidOps.getUid(), otherUidOps); + } + } + } + + /** + * AppPermissionUsage the ops to leave only the data we filter for. + * + * @param uid Uid to filter for or {@link android.os.Process#INCIDENTD_UID} for all. + * @param packageName Package to filter for or null for all. + * @param opNames Ops to filter for or null for all. + * @param beginTimeMillis The begin time to filter for or {@link Long#MIN_VALUE} for all. + * @param endTimeMillis The end time to filter for or {@link Long#MAX_VALUE} for all. + * + * @hide + */ + public void filter(int uid, @Nullable String packageName, @Nullable String[] opNames, + long beginTimeMillis, long endTimeMillis) { + final long durationMillis = getDurationMillis(); + mBeginTimeMillis = Math.max(mBeginTimeMillis, beginTimeMillis); + mEndTimeMillis = Math.min(mEndTimeMillis, endTimeMillis); + final double scaleFactor = Math.min((double) (endTimeMillis - beginTimeMillis) + / (double) durationMillis, 1); + final int uidCount = getUidCount(); + for (int i = uidCount - 1; i >= 0; i--) { + final HistoricalUidOps uidOp = mHistoricalUidOps.valueAt(i); + if (uid != Process.INVALID_UID && uid != uidOp.getUid()) { + mHistoricalUidOps.removeAt(i); + } else { + uidOp.filter(packageName, opNames, scaleFactor); + } + } + } + + /** @hide */ + public boolean isEmpty() { + if (getBeginTimeMillis() >= getEndTimeMillis()) { + return true; + } + final int uidCount = getUidCount(); + for (int i = uidCount - 1; i >= 0; i--) { + final HistoricalUidOps uidOp = mHistoricalUidOps.valueAt(i); + if (!uidOp.isEmpty()) { + return false; + } + } + return true; + } + + /** @hide */ + public long getDurationMillis() { + return mEndTimeMillis - mBeginTimeMillis; + } + + /** @hide */ + @TestApi + public void increaseAccessCount(int opCode, int uid, @NonNull String packageName, + @UidState int uidState, long increment) { + getOrCreateHistoricalUidOps(uid).increaseAccessCount(opCode, + packageName, uidState, increment); + } + + /** @hide */ + @TestApi + public void increaseRejectCount(int opCode, int uid, @NonNull String packageName, + @UidState int uidState, long increment) { + getOrCreateHistoricalUidOps(uid).increaseRejectCount(opCode, + packageName, uidState, increment); + } + + /** @hide */ + @TestApi + public void increaseAccessDuration(int opCode, int uid, @NonNull String packageName, + @UidState int uidState, long increment) { + getOrCreateHistoricalUidOps(uid).increaseAccessDuration(opCode, + packageName, uidState, increment); + } + + /** @hide */ + @TestApi + public void offsetBeginAndEndTime(long offsetMillis) { + mBeginTimeMillis += offsetMillis; + mEndTimeMillis += offsetMillis; + } + + /** @hide */ + public void setBeginAndEndTime(long beginTimeMillis, long endTimeMillis) { + mBeginTimeMillis = beginTimeMillis; + mEndTimeMillis = endTimeMillis; + } + + /** @hide */ + public void setBeginTime(long beginTimeMillis) { + mBeginTimeMillis = beginTimeMillis; + } + + /** @hide */ + public void setEndTime(long endTimeMillis) { + mEndTimeMillis = endTimeMillis; + } + + /** + * @return The beginning of the interval in milliseconds since + * epoch start (January 1, 1970 00:00:00.000 GMT - Gregorian). + */ + public long getBeginTimeMillis() { + return mBeginTimeMillis; + } + + /** + * @return The end of the interval in milliseconds since + * epoch start (January 1, 1970 00:00:00.000 GMT - Gregorian). + */ + public long getEndTimeMillis() { + return mEndTimeMillis; + } + + /** + * Gets number of UIDs with historical ops. + * + * @return The number of UIDs with historical ops. + * + * @see #getUidOpsAt(int) + */ + public int getUidCount() { + if (mHistoricalUidOps == null) { + return 0; + } + return mHistoricalUidOps.size(); + } + + /** + * Gets the historical UID ops at a given index. + * + * @param index The index. + * + * @return The historical UID ops at the given index. + * + * @see #getUidCount() + */ + public @NonNull HistoricalUidOps getUidOpsAt(int index) { + if (mHistoricalUidOps == null) { + throw new IndexOutOfBoundsException(); + } + return mHistoricalUidOps.valueAt(index); + } + + /** + * Gets the historical UID ops for a given UID. + * + * @param uid The UID. + * + * @return The historical ops for the UID. + */ + public @Nullable HistoricalUidOps getUidOps(int uid) { + if (mHistoricalUidOps == null) { + return null; + } + return mHistoricalUidOps.get(uid); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeLong(mBeginTimeMillis); + parcel.writeLong(mEndTimeMillis); + if (mHistoricalUidOps != null) { + final int uidCount = mHistoricalUidOps.size(); + parcel.writeInt(uidCount); + for (int i = 0; i < uidCount; i++) { + parcel.writeInt(mHistoricalUidOps.keyAt(i)); + } + final List<HistoricalUidOps> opsList = new ArrayList<>(uidCount); + for (int i = 0; i < uidCount; i++) { + opsList.add(mHistoricalUidOps.valueAt(i)); + } + parcel.writeParcelable(new ParceledListSlice<>(opsList), flags); + } else { + parcel.writeInt(-1); + } + } + + /** + * Accepts a visitor to traverse the ops tree. + * + * @param visitor The visitor. + * + * @hide + */ + public void accept(@NonNull HistoricalOpsVisitor visitor) { + visitor.visitHistoricalOps(this); + final int uidCount = getUidCount(); + for (int i = 0; i < uidCount; i++) { + getUidOpsAt(i).accept(visitor); + } + } + + private @NonNull HistoricalUidOps getOrCreateHistoricalUidOps(int uid) { + if (mHistoricalUidOps == null) { + mHistoricalUidOps = new SparseArray<>(); + } + HistoricalUidOps historicalUidOp = mHistoricalUidOps.get(uid); + if (historicalUidOp == null) { + historicalUidOp = new HistoricalUidOps(uid); + mHistoricalUidOps.put(uid, historicalUidOp); + } + return historicalUidOp; } /** - * Gets the UID which the data represents. + * @return Rounded value up at the 0.5 boundary. * - * @return The UID which the data represents. + * @hide + */ + public static double round(double value) { + final BigDecimal decimalScale = new BigDecimal(value); + return decimalScale.setScale(0, RoundingMode.HALF_UP).doubleValue(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final HistoricalOps other = (HistoricalOps) obj; + if (mBeginTimeMillis != other.mBeginTimeMillis) { + return false; + } + if (mEndTimeMillis != other.mEndTimeMillis) { + return false; + } + if (mHistoricalUidOps == null) { + if (other.mHistoricalUidOps != null) { + return false; + } + } else if (!mHistoricalUidOps.equals(other.mHistoricalUidOps)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = (int) (mBeginTimeMillis ^ (mBeginTimeMillis >>> 32)); + result = 31 * result + mHistoricalUidOps.hashCode(); + return result; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[from:" + + mBeginTimeMillis + " to:" + mEndTimeMillis + "]"; + } + + public static final Creator<HistoricalOps> CREATOR = new Creator<HistoricalOps>() { + @Override + public @NonNull HistoricalOps createFromParcel(@NonNull Parcel parcel) { + return new HistoricalOps(parcel); + } + + @Override + public @NonNull HistoricalOps[] newArray(int size) { + return new HistoricalOps[size]; + } + }; + } + + /** + * This class represents historical app op state for a UID. + * + * @hide + */ + @TestApi + @SystemApi + public static final class HistoricalUidOps implements Parcelable { + private final int mUid; + private @Nullable ArrayMap<String, HistoricalPackageOps> mHistoricalPackageOps; + + /** @hide */ + public HistoricalUidOps(int uid) { + mUid = uid; + } + + private HistoricalUidOps(@NonNull HistoricalUidOps other) { + mUid = other.mUid; + final int opCount = other.getPackageCount(); + for (int i = 0; i < opCount; i++) { + final HistoricalPackageOps origOps = other.getPackageOpsAt(i); + final HistoricalPackageOps cloneOps = new HistoricalPackageOps(origOps); + if (mHistoricalPackageOps == null) { + mHistoricalPackageOps = new ArrayMap<>(opCount); + } + mHistoricalPackageOps.put(cloneOps.getPackageName(), cloneOps); + } + } + + private HistoricalUidOps(@NonNull Parcel parcel) { + // No arg check since we always read from a trusted source. + mUid = parcel.readInt(); + mHistoricalPackageOps = parcel.createTypedArrayMap(HistoricalPackageOps.CREATOR); + } + + private @Nullable HistoricalUidOps splice(double fractionToRemove) { + HistoricalUidOps splice = null; + final int packageCount = getPackageCount(); + for (int i = 0; i < packageCount; i++) { + final HistoricalPackageOps origOps = getPackageOpsAt(i); + final HistoricalPackageOps spliceOps = origOps.splice(fractionToRemove); + if (spliceOps != null) { + if (splice == null) { + splice = new HistoricalUidOps(mUid); + } + if (splice.mHistoricalPackageOps == null) { + splice.mHistoricalPackageOps = new ArrayMap<>(); + } + splice.mHistoricalPackageOps.put(spliceOps.getPackageName(), spliceOps); + } + } + return splice; + } + + private void merge(@NonNull HistoricalUidOps other) { + final int packageCount = other.getPackageCount(); + for (int i = 0; i < packageCount; i++) { + final HistoricalPackageOps otherPackageOps = other.getPackageOpsAt(i); + final HistoricalPackageOps thisPackageOps = getPackageOps( + otherPackageOps.getPackageName()); + if (thisPackageOps != null) { + thisPackageOps.merge(otherPackageOps); + } else { + if (mHistoricalPackageOps == null) { + mHistoricalPackageOps = new ArrayMap<>(); + } + mHistoricalPackageOps.put(otherPackageOps.getPackageName(), otherPackageOps); + } + } + } + + private void filter(@Nullable String packageName, @Nullable String[] opNames, + double fractionToRemove) { + final int packageCount = getPackageCount(); + for (int i = packageCount - 1; i >= 0; i--) { + final HistoricalPackageOps packageOps = getPackageOpsAt(i); + if (packageName != null && !packageName.equals(packageOps.getPackageName())) { + mHistoricalPackageOps.removeAt(i); + } else { + packageOps.filter(opNames, fractionToRemove); + } + } + } + + private boolean isEmpty() { + final int packageCount = getPackageCount(); + for (int i = packageCount - 1; i >= 0; i--) { + final HistoricalPackageOps packageOps = mHistoricalPackageOps.valueAt(i); + if (!packageOps.isEmpty()) { + return false; + } + } + return true; + } + + private void increaseAccessCount(int opCode, @NonNull String packageName, + @UidState int uidState, long increment) { + getOrCreateHistoricalPackageOps(packageName).increaseAccessCount( + opCode, uidState, increment); + } + + private void increaseRejectCount(int opCode, @NonNull String packageName, + @UidState int uidState, long increment) { + getOrCreateHistoricalPackageOps(packageName).increaseRejectCount( + opCode, uidState, increment); + } + + private void increaseAccessDuration(int opCode, @NonNull String packageName, + @UidState int uidState, long increment) { + getOrCreateHistoricalPackageOps(packageName).increaseAccessDuration( + opCode, uidState, increment); + } + + /** + * @return The UID for which the data is related. */ public int getUid() { return mUid; } /** - * Gets number historical app op entries. + * Gets number of packages with historical ops. + * + * @return The number of packages with historical ops. + * + * @see #getPackageOpsAt(int) + */ + public int getPackageCount() { + if (mHistoricalPackageOps == null) { + return 0; + } + return mHistoricalPackageOps.size(); + } + + /** + * Gets the historical package ops at a given index. + * + * @param index The index. * - * @return The number historical app op entries. + * @return The historical package ops at the given index. * - * @see #getEntryAt(int) + * @see #getPackageCount() */ - public int getEntryCount() { - return mEntries.size(); + public @NonNull HistoricalPackageOps getPackageOpsAt(int index) { + if (mHistoricalPackageOps == null) { + throw new IndexOutOfBoundsException(); + } + return mHistoricalPackageOps.valueAt(index); } /** - * Gets the historical at a given index. + * Gets the historical package ops for a given package. + * + * @param packageName The package. + * + * @return The historical ops for the package. + */ + public @Nullable HistoricalPackageOps getPackageOps(@NonNull String packageName) { + if (mHistoricalPackageOps == null) { + return null; + } + return mHistoricalPackageOps.get(packageName); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mUid); + parcel.writeTypedArrayMap(mHistoricalPackageOps, flags); + } + + private void accept(@NonNull HistoricalOpsVisitor visitor) { + visitor.visitHistoricalUidOps(this); + final int packageCount = getPackageCount(); + for (int i = 0; i < packageCount; i++) { + getPackageOpsAt(i).accept(visitor); + } + } + + private @NonNull HistoricalPackageOps getOrCreateHistoricalPackageOps( + @NonNull String packageName) { + if (mHistoricalPackageOps == null) { + mHistoricalPackageOps = new ArrayMap<>(); + } + HistoricalPackageOps historicalPackageOp = mHistoricalPackageOps.get(packageName); + if (historicalPackageOp == null) { + historicalPackageOp = new HistoricalPackageOps(packageName); + mHistoricalPackageOps.put(packageName, historicalPackageOp); + } + return historicalPackageOp; + } + + + public static final Creator<HistoricalUidOps> CREATOR = new Creator<HistoricalUidOps>() { + @Override + public @NonNull HistoricalUidOps createFromParcel(@NonNull Parcel parcel) { + return new HistoricalUidOps(parcel); + } + + @Override + public @NonNull HistoricalUidOps[] newArray(int size) { + return new HistoricalUidOps[size]; + } + }; + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final HistoricalUidOps other = (HistoricalUidOps) obj; + if (mUid != other.mUid) { + return false; + } + if (mHistoricalPackageOps == null) { + if (other.mHistoricalPackageOps != null) { + return false; + } + } else if (!mHistoricalPackageOps.equals(other.mHistoricalPackageOps)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = mUid; + result = 31 * result + (mHistoricalPackageOps != null + ? mHistoricalPackageOps.hashCode() : 0); + return result; + } + } + + /** + * This class represents historical app op information about a package. + * + * @hide + */ + @TestApi + @SystemApi + public static final class HistoricalPackageOps implements Parcelable { + private final @NonNull String mPackageName; + private @Nullable ArrayMap<String, HistoricalOp> mHistoricalOps; + + /** @hide */ + public HistoricalPackageOps(@NonNull String packageName) { + mPackageName = packageName; + } + + private HistoricalPackageOps(@NonNull HistoricalPackageOps other) { + mPackageName = other.mPackageName; + final int opCount = other.getOpCount(); + for (int i = 0; i < opCount; i++) { + final HistoricalOp origOp = other.getOpAt(i); + final HistoricalOp cloneOp = new HistoricalOp(origOp); + if (mHistoricalOps == null) { + mHistoricalOps = new ArrayMap<>(opCount); + } + mHistoricalOps.put(cloneOp.getOpName(), cloneOp); + } + } + + private HistoricalPackageOps(@NonNull Parcel parcel) { + mPackageName = parcel.readString(); + mHistoricalOps = parcel.createTypedArrayMap(HistoricalOp.CREATOR); + } + + private @Nullable HistoricalPackageOps splice(double fractionToRemove) { + HistoricalPackageOps splice = null; + final int opCount = getOpCount(); + for (int i = 0; i < opCount; i++) { + final HistoricalOp origOps = getOpAt(i); + final HistoricalOp spliceOps = origOps.splice(fractionToRemove); + if (spliceOps != null) { + if (splice == null) { + splice = new HistoricalPackageOps(mPackageName); + } + if (splice.mHistoricalOps == null) { + splice.mHistoricalOps = new ArrayMap<>(); + } + splice.mHistoricalOps.put(spliceOps.getOpName(), spliceOps); + } + } + return splice; + } + + private void merge(@NonNull HistoricalPackageOps other) { + final int opCount = other.getOpCount(); + for (int i = 0; i < opCount; i++) { + final HistoricalOp otherOp = other.getOpAt(i); + final HistoricalOp thisOp = getOp(otherOp.getOpName()); + if (thisOp != null) { + thisOp.merge(otherOp); + } else { + if (mHistoricalOps == null) { + mHistoricalOps = new ArrayMap<>(); + } + mHistoricalOps.put(otherOp.getOpName(), otherOp); + } + } + } + + private void filter(@Nullable String[] opNames, double scaleFactor) { + final int opCount = getOpCount(); + for (int i = opCount - 1; i >= 0; i--) { + final HistoricalOp op = mHistoricalOps.valueAt(i); + if (opNames != null && !ArrayUtils.contains(opNames, op.getOpName())) { + mHistoricalOps.removeAt(i); + } else { + op.filter(scaleFactor); + } + } + } + + private boolean isEmpty() { + final int opCount = getOpCount(); + for (int i = opCount - 1; i >= 0; i--) { + final HistoricalOp op = mHistoricalOps.valueAt(i); + if (!op.isEmpty()) { + return false; + } + } + return true; + } + + private void increaseAccessCount(int opCode, @UidState int uidState, long increment) { + getOrCreateHistoricalOp(opCode).increaseAccessCount(uidState, increment); + } + + private void increaseRejectCount(int opCode, @UidState int uidState, long increment) { + getOrCreateHistoricalOp(opCode).increaseRejectCount(uidState, increment); + } + + private void increaseAccessDuration(int opCode, @UidState int uidState, long increment) { + getOrCreateHistoricalOp(opCode).increaseAccessDuration(uidState, increment); + } + + /** + * Gets the package name which the data represents. + * + * @return The package name which the data represents. + */ + public @NonNull String getPackageName() { + return mPackageName; + } + + /** + * Gets number historical app ops. + * + * @return The number historical app ops. + * + * @see #getOpAt(int) + */ + public int getOpCount() { + if (mHistoricalOps == null) { + return 0; + } + return mHistoricalOps.size(); + } + + /** + * Gets the historical op at a given index. * * @param index The index to lookup. * - * @return The entry at the given index. + * @return The op at the given index. * - * @see #getEntryCount() + * @see #getOpCount() */ - public @NonNull HistoricalOpEntry getEntryAt(int index) { - return mEntries.get(index); + public @NonNull HistoricalOp getOpAt(int index) { + if (mHistoricalOps == null) { + throw new IndexOutOfBoundsException(); + } + return mHistoricalOps.valueAt(index); } /** @@ -2172,15 +2968,11 @@ public class AppOpsManager { * * @return The historical entry for that op name. */ - public @Nullable HistoricalOpEntry getEntry(@NonNull String opName) { - final int entryCount = mEntries.size(); - for (int i = 0; i < entryCount; i++) { - final HistoricalOpEntry entry = mEntries.get(i); - if (entry.getOp().equals(opName)) { - return entry; - } + public @Nullable HistoricalOp getOp(@NonNull String opName) { + if (mHistoricalOps == null) { + return null; } - return null; + return mHistoricalOps.get(opName); } @Override @@ -2190,9 +2982,29 @@ public class AppOpsManager { @Override public void writeToParcel(@NonNull Parcel parcel, int flags) { - parcel.writeInt(mUid); parcel.writeString(mPackageName); - parcel.writeTypedList(mEntries, flags); + parcel.writeTypedArrayMap(mHistoricalOps, flags); + } + + private void accept(@NonNull HistoricalOpsVisitor visitor) { + visitor.visitHistoricalPackageOps(this); + final int opCount = getOpCount(); + for (int i = 0; i < opCount; i++) { + getOpAt(i).accept(visitor); + } + } + + private @NonNull HistoricalOp getOrCreateHistoricalOp(int opCode) { + if (mHistoricalOps == null) { + mHistoricalOps = new ArrayMap<>(); + } + final String opStr = sOpToString[opCode]; + HistoricalOp op = mHistoricalOps.get(opStr); + if (op == null) { + op = new HistoricalOp(opCode); + mHistoricalOps.put(opStr, op); + } + return op; } public static final Creator<HistoricalPackageOps> CREATOR = @@ -2207,49 +3019,177 @@ public class AppOpsManager { return new HistoricalPackageOps[size]; } }; + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final HistoricalPackageOps other = (HistoricalPackageOps) obj; + if (!mPackageName.equals(other.mPackageName)) { + return false; + } + if (mHistoricalOps == null) { + if (other.mHistoricalOps != null) { + return false; + } + } else if (!mHistoricalOps.equals(other.mHistoricalOps)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = mPackageName != null ? mPackageName.hashCode() : 0; + result = 31 * result + (mHistoricalOps != null ? mHistoricalOps.hashCode() : 0); + return result; + } } /** - * This class represents historical information about an app op. The history - * is aggregated information about the op for a certain amount of time such - * as the times the op was accessed, the times the op was rejected, the total - * duration the app op has been accessed. + * This class represents historical information about an app op. * * @hide */ @TestApi @SystemApi - public static final class HistoricalOpEntry implements Parcelable { + public static final class HistoricalOp implements Parcelable { private final int mOp; - private final long[] mAccessCount; - private final long[] mRejectCount; - private final long[] mAccessDuration; + private @Nullable long[] mAccessCount; + private @Nullable long[] mRejectCount; + private @Nullable long[] mAccessDuration; - /** - * @hide - */ - public HistoricalOpEntry(int op) { + /** @hide */ + public HistoricalOp(int op) { mOp = op; mAccessCount = new long[_NUM_UID_STATE]; mRejectCount = new long[_NUM_UID_STATE]; mAccessDuration = new long[_NUM_UID_STATE]; } - HistoricalOpEntry(@NonNull Parcel parcel) { + private HistoricalOp(@NonNull HistoricalOp other) { + mOp = other.mOp; + if (other.mAccessCount != null) { + System.arraycopy(other.mAccessCount, 0, getOrCreateAccessCount(), + 0, other.mAccessCount.length); + } + if (other.mRejectCount != null) { + System.arraycopy(other.mRejectCount, 0, getOrCreateRejectCount(), + 0, other.mRejectCount.length); + } + if (other.mAccessDuration != null) { + System.arraycopy(other.mAccessDuration, 0, getOrCreateAccessDuration(), + 0, other.mAccessDuration.length); + } + } + + private HistoricalOp(@NonNull Parcel parcel) { mOp = parcel.readInt(); mAccessCount = parcel.createLongArray(); mRejectCount = parcel.createLongArray(); mAccessDuration = parcel.createLongArray(); } - /** - * @hide - */ - public void addEntry(@UidState int uidState, long accessCount, - long rejectCount, long accessDuration) { - mAccessCount[uidState] = accessCount; - mRejectCount[uidState] = rejectCount; - mAccessDuration[uidState] = accessDuration; + private void filter(double scaleFactor) { + scale(mAccessCount, scaleFactor); + scale(mRejectCount, scaleFactor); + scale(mAccessDuration, scaleFactor); + } + + private boolean isEmpty() { + return !hasData(mAccessCount) + && !hasData(mRejectCount) + && !hasData(mAccessDuration); + } + + private boolean hasData(@NonNull long[] array) { + for (long value : array) { + if (value != 0) { + return true; + } + } + return false; + } + + private @Nullable HistoricalOp splice(double fractionToRemove) { + HistoricalOp splice = null; + if (mAccessCount != null) { + for (int i = 0; i < _NUM_UID_STATE; i++) { + final long spliceAccessCount = Math.round( + mAccessCount[i] * fractionToRemove); + if (spliceAccessCount > 0) { + if (splice == null) { + splice = new HistoricalOp(mOp); + } + splice.getOrCreateAccessCount()[i] = spliceAccessCount; + mAccessCount[i] -= spliceAccessCount; + } + } + } + + if (mRejectCount != null) { + for (int i = 0; i < _NUM_UID_STATE; i++) { + final long spliceRejectCount = Math.round( + mRejectCount[i] * fractionToRemove); + + if (spliceRejectCount > 0) { + if (splice == null) { + splice = new HistoricalOp(mOp); + } + splice.getOrCreateRejectCount()[i] = spliceRejectCount; + mRejectCount[i] -= spliceRejectCount; + } + } + } + + if (mAccessDuration != null) { + for (int i = 0; i < _NUM_UID_STATE; i++) { + final long spliceAccessDuration = Math.round( + mAccessDuration[i] * fractionToRemove); + if (spliceAccessDuration > 0) { + if (splice == null) { + splice = new HistoricalOp(mOp); + } + splice.getOrCreateAccessDuration()[i] = spliceAccessDuration; + mAccessDuration[i] -= spliceAccessDuration; + } + } + } + return splice; + } + + private void merge(@NonNull HistoricalOp other) { + if (other.mAccessCount != null) { + for (int i = 0; i < _NUM_UID_STATE; i++) { + getOrCreateAccessCount()[i] += other.mAccessCount[i]; + } + } + if (other.mRejectCount != null) { + for (int i = 0; i < _NUM_UID_STATE; i++) { + getOrCreateRejectCount()[i] += other.mRejectCount[i]; + } + } + if (other.mAccessDuration != null) { + for (int i = 0; i < _NUM_UID_STATE; i++) { + getOrCreateAccessDuration()[i] += other.mAccessDuration[i]; + } + } + } + + private void increaseAccessCount(@UidState int uidState, long increment) { + getOrCreateAccessCount()[uidState] += increment; + } + + private void increaseRejectCount(@UidState int uidState, long increment) { + getOrCreateRejectCount()[uidState] += increment; + } + + private void increaseAccessDuration(@UidState int uidState, long increment) { + getOrCreateAccessDuration()[uidState] += increment; } /** @@ -2257,10 +3197,15 @@ public class AppOpsManager { * * @return The op name. */ - public @NonNull String getOp() { + public @NonNull String getOpName() { return sOpToString[mOp]; } + /** @hide */ + public int getOpCode() { + return mOp; + } + /** * Gets the number times the op was accessed (performed) in the foreground. * @@ -2270,6 +3215,9 @@ public class AppOpsManager { * @see #getAccessCount(int) */ public long getForegroundAccessCount() { + if (mAccessCount == null) { + return 0; + } return sum(mAccessCount, UID_STATE_PERSISTENT, UID_STATE_LAST_NON_RESTRICTED + 1); } @@ -2282,6 +3230,9 @@ public class AppOpsManager { * @see #getAccessCount(int) */ public long getBackgroundAccessCount() { + if (mAccessCount == null) { + return 0; + } return sum(mAccessCount, UID_STATE_LAST_NON_RESTRICTED + 1, _NUM_UID_STATE); } @@ -2299,6 +3250,9 @@ public class AppOpsManager { * @see #getBackgroundAccessCount() */ public long getAccessCount(@UidState int uidState) { + if (mAccessCount == null) { + return 0; + } return mAccessCount[uidState]; } @@ -2311,6 +3265,9 @@ public class AppOpsManager { * @see #getRejectCount(int) */ public long getForegroundRejectCount() { + if (mRejectCount == null) { + return 0; + } return sum(mRejectCount, UID_STATE_PERSISTENT, UID_STATE_LAST_NON_RESTRICTED + 1); } @@ -2323,6 +3280,9 @@ public class AppOpsManager { * @see #getRejectCount(int) */ public long getBackgroundRejectCount() { + if (mRejectCount == null) { + return 0; + } return sum(mRejectCount, UID_STATE_LAST_NON_RESTRICTED + 1, _NUM_UID_STATE); } @@ -2340,6 +3300,9 @@ public class AppOpsManager { * @see #getBackgroundRejectCount() */ public long getRejectCount(@UidState int uidState) { + if (mRejectCount == null) { + return 0; + } return mRejectCount[uidState]; } @@ -2352,6 +3315,9 @@ public class AppOpsManager { * @see #getAccessDuration(int) */ public long getForegroundAccessDuration() { + if (mAccessDuration == null) { + return 0; + } return sum(mAccessDuration, UID_STATE_PERSISTENT, UID_STATE_LAST_NON_RESTRICTED + 1); } @@ -2364,6 +3330,9 @@ public class AppOpsManager { * @see #getAccessDuration(int) */ public long getBackgroundAccessDuration() { + if (mAccessDuration == null) { + return 0; + } return sum(mAccessDuration, UID_STATE_LAST_NON_RESTRICTED + 1, _NUM_UID_STATE); } @@ -2381,6 +3350,9 @@ public class AppOpsManager { * @see #getBackgroundAccessDuration() */ public long getAccessDuration(@UidState int uidState) { + if (mAccessDuration == null) { + return 0; + } return mAccessDuration[uidState]; } @@ -2397,34 +3369,29 @@ public class AppOpsManager { parcel.writeLongArray(mAccessDuration); } - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - final HistoricalOpEntry otherInstance = (HistoricalOpEntry) other; - if (mOp != otherInstance.mOp) { - return false; - } - if (!Arrays.equals(mAccessCount, otherInstance.mAccessCount)) { - return false; + private void accept(@NonNull HistoricalOpsVisitor visitor) { + visitor.visitHistoricalOp(this); + } + + private @NonNull long[] getOrCreateAccessCount() { + if (mAccessCount == null) { + mAccessCount = new long[_NUM_UID_STATE]; } - if (!Arrays.equals(mRejectCount, otherInstance.mRejectCount)) { - return false; + return mAccessCount; + } + + private @NonNull long[] getOrCreateRejectCount() { + if (mRejectCount == null) { + mRejectCount = new long[_NUM_UID_STATE]; } - return Arrays.equals(mAccessDuration, otherInstance.mAccessDuration); + return mRejectCount; } - @Override - public int hashCode() { - int result = mOp; - result = 31 * result + Arrays.hashCode(mAccessCount); - result = 31 * result + Arrays.hashCode(mRejectCount); - result = 31 * result + Arrays.hashCode(mAccessDuration); - return result; + private @NonNull long[] getOrCreateAccessDuration() { + if (mAccessDuration == null) { + mAccessDuration = new long[_NUM_UID_STATE]; + } + return mAccessDuration; } /** @@ -2444,17 +3411,62 @@ public class AppOpsManager { return totalCount; } - public static final Creator<HistoricalOpEntry> CREATOR = new Creator<HistoricalOpEntry>() { + /** + * Multiplies the entries in the array with the passed in scale factor and + * rounds the result at up 0.5 boundary. + * + * @param data The data to scale. + * @param scaleFactor The scale factor. + */ + private static void scale(@NonNull long[] data, double scaleFactor) { + if (data != null) { + for (int i = 0; i < _NUM_UID_STATE; i++) { + data[i] = (long) HistoricalOps.round((double) data[i] * scaleFactor); + } + } + } + + public static final Creator<HistoricalOp> CREATOR = new Creator<HistoricalOp>() { @Override - public @NonNull HistoricalOpEntry createFromParcel(@NonNull Parcel source) { - return new HistoricalOpEntry(source); + public @NonNull HistoricalOp createFromParcel(@NonNull Parcel source) { + return new HistoricalOp(source); } @Override - public @NonNull HistoricalOpEntry[] newArray(int size) { - return new HistoricalOpEntry[size]; + public @NonNull HistoricalOp[] newArray(int size) { + return new HistoricalOp[size]; } }; + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final HistoricalOp other = (HistoricalOp) obj; + if (mOp != other.mOp) { + return false; + } + if (!Arrays.equals(mAccessCount, other.mAccessCount)) { + return false; + } + if (!Arrays.equals(mRejectCount, other.mRejectCount)) { + return false; + } + return Arrays.equals(mAccessDuration, other.mAccessDuration); + } + + @Override + public int hashCode() { + int result = mOp; + result = 31 * result + Arrays.hashCode(mAccessCount); + result = 31 * result + Arrays.hashCode(mRejectCount); + result = 31 * result + Arrays.hashCode(mAccessDuration); + return result; + } } /** @@ -2520,6 +3532,24 @@ public class AppOpsManager { * @param ops The set of operations you are interested in, or null if you want all of them. * @hide */ + @SystemApi + @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) + public @NonNull List<AppOpsManager.PackageOps> getPackagesForOps(@Nullable String[] ops) { + final int opCount = ops.length; + final int[] opCodes = new int[opCount]; + for (int i = 0; i < opCount; i++) { + opCodes[i] = sOpStrToOp.get(ops[i]); + } + final List<AppOpsManager.PackageOps> result = getPackagesForOps(opCodes); + return (result != null) ? result : Collections.emptyList(); + } + + /** + * Retrieve current operation state for all applications. + * + * @param ops The set of operations you are interested in, or null if you want all of them. + * @hide + */ @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) @UnsupportedAppUsage public List<AppOpsManager.PackageOps> getPackagesForOps(int[] ops) { @@ -2536,11 +3566,17 @@ public class AppOpsManager { * @param uid The uid of the application of interest. * @param packageName The name of the application of interest. * @param ops The set of operations you are interested in, or null if you want all of them. + * + * @deprecated The int op codes are not stable and you should use the string based op + * names which are stable and namespaced. Use + * {@link #getOpsForPackage(int, String, String...)})}. + * * @hide */ + @Deprecated @SystemApi @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) - public List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName, int[] ops) { + public List<PackageOps> getOpsForPackage(int uid, String packageName, int[] ops) { try { return mService.getOpsForPackage(uid, packageName, ops); } catch (RemoteException e) { @@ -2549,7 +3585,49 @@ public class AppOpsManager { } /** - * Retrieve historical app op stats for a package. + * Retrieve current operation state for one application. The UID and the + * package must match. + * + * @param uid The uid of the application of interest. + * @param packageName The name of the application of interest. + * @param ops The set of operations you are interested in, or null if you want all of them. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) + public @NonNull List<AppOpsManager.PackageOps> getOpsForPackage(int uid, + @NonNull String packageName, @Nullable String... ops) { + int[] opCodes = null; + if (ops != null) { + opCodes = new int[ops.length]; + for (int i = 0; i < ops.length; i++) { + opCodes[i] = strOpToOp(ops[i]); + } + } + try { + final List<PackageOps> result = mService.getOpsForPackage(uid, packageName, opCodes); + if (result == null) { + return Collections.emptyList(); + } + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Retrieve historical app op stats for a period. + * + * <p>Historical data can be obtained + * for a specific package by specifying the <code>packageName</code> argument, + * for a specific UID if specifying the <code>uid</code> argument, for a + * specific package in a UID by specifying the <code>packageName</code> + * and the <code>uid</code> arguments, for all packages by passing + * {@link android.os.Process#INVALID_UID} and <code>null</code> for the + * <code>uid</code> and <code>packageName</code> arguments, respectively. + * Similarly, you can specify the <code>opNames</code> argument to get + * data only for these ops or <code>null</code> for all ops. * * @param uid The UID to query for. * @param packageName The package to query for. @@ -2558,10 +3636,12 @@ public class AppOpsManager { * negative. * @param endTimeMillis The end of the interval in milliseconds since * epoch start (January 1, 1970 00:00:00.000 GMT - Gregorian). Must be after - * {@code beginTimeMillis}. + * {@code beginTimeMillis}. Pass {@link Long#MAX_VALUE} to get the most recent + * history including ops that happen while this call is in flight. * @param opNames The ops to query for. Pass {@code null} for all ops. - * - * @return The historical ops or {@code null} if there are no ops for this package. + * @param executor Executor on which to run the callback. If <code>null</code> + * the callback is executed on the default executor running on the main thread. + * @param callback Callback on which to deliver the result. * * @throws IllegalArgumentException If any of the argument contracts is violated. * @@ -2570,47 +3650,78 @@ public class AppOpsManager { @TestApi @SystemApi @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) - public @Nullable HistoricalPackageOps getHistoricalPackagesOps( - int uid, @NonNull String packageName, @Nullable String[] opNames, - long beginTimeMillis, long endTimeMillis) { + public void getHistoricalOps(int uid, @Nullable String packageName, + @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis, + @NonNull Executor executor, @NonNull Consumer<HistoricalOps> callback) { + Preconditions.checkNotNull(executor, "executor cannot be null"); + Preconditions.checkNotNull(callback, "callback cannot be null"); try { - return mService.getHistoricalPackagesOps(uid, packageName, opNames, - beginTimeMillis, endTimeMillis); + mService.getHistoricalOps(uid, packageName, opNames, beginTimeMillis, endTimeMillis, + new RemoteCallback((result) -> { + final HistoricalOps ops = result.getParcelable(KEY_HISTORICAL_OPS); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> callback.accept(ops)); + } finally { + Binder.restoreCallingIdentity(identity); + } + })); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** - * Retrieve historical app op stats for all packages. + * Retrieve historical app op stats for a period. + * + * <p>Historical data can be obtained + * for a specific package by specifying the <code>packageName</code> argument, + * for a specific UID if specifying the <code>uid</code> argument, for a + * specific package in a UID by specifying the <code>packageName</code> + * and the <code>uid</code> arguments, for all packages by passing + * {@link android.os.Process#INVALID_UID} and <code>null</code> for the + * <code>uid</code> and <code>packageName</code> arguments, respectively. + * Similarly, you can specify the <code>opNames</code> argument to get + * data only for these ops or <code>null</code> for all ops. + * <p> + * This method queries only the on disk state and the returned ops are raw, + * which is their times are relative to the history start as opposed to the + * epoch start. * + * @param uid The UID to query for. + * @param packageName The package to query for. * @param beginTimeMillis The beginning of the interval in milliseconds since - * epoch start (January 1, 1970 00:00:00.000 GMT - Gregorian). Must be non - * negative. + * history start. History time grows as one goes into the past. * @param endTimeMillis The end of the interval in milliseconds since - * epoch start (January 1, 1970 00:00:00.000 GMT - Gregorian). Must be after + * history start. History time grows as one goes into the past. Must be after * {@code beginTimeMillis}. * @param opNames The ops to query for. Pass {@code null} for all ops. - * - * @return The historical ops or an empty list if there are no ops for any package. + * @param executor Executor on which to run the callback. If <code>null</code> + * the callback is executed on the default executor running on the main thread. + * @param callback Callback on which to deliver the result. * * @throws IllegalArgumentException If any of the argument contracts is violated. * * @hide */ @TestApi - @SystemApi @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) - public @NonNull List<HistoricalPackageOps> getAllHistoricPackagesOps( - @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis) { + public void getHistoricalOpsFromDiskRaw(int uid, @Nullable String packageName, + @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis, + @Nullable Executor executor, @NonNull Consumer<HistoricalOps> callback) { + Preconditions.checkNotNull(executor, "executor cannot be null"); + Preconditions.checkNotNull(callback, "callback cannot be null"); try { - @SuppressWarnings("unchecked") - final ParceledListSlice<HistoricalPackageOps> payload = - mService.getAllHistoricalPackagesOps(opNames, beginTimeMillis, endTimeMillis); - if (payload != null) { - return payload.getList(); - } - return Collections.emptyList(); + mService.getHistoricalOpsFromDiskRaw(uid, packageName, opNames, beginTimeMillis, + endTimeMillis, new RemoteCallback((result) -> { + final HistoricalOps ops = result.getParcelable(KEY_HISTORICAL_OPS); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> callback.accept(ops)); + } finally { + Binder.restoreCallingIdentity(identity); + } + })); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2627,7 +3738,7 @@ public class AppOpsManager { * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) - public void setUidMode(int code, int uid, int mode) { + public void setUidMode(int code, int uid, @Mode int mode) { try { mService.setUidMode(code, uid, mode); } catch (RemoteException e) { @@ -2648,7 +3759,7 @@ public class AppOpsManager { @SystemApi @TestApi @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) - public void setUidMode(String appOp, int uid, int mode) { + public void setUidMode(String appOp, int uid, @Mode int mode) { try { mService.setUidMode(AppOpsManager.strOpToOp(appOp), uid, mode); } catch (RemoteException e) { @@ -2680,7 +3791,7 @@ public class AppOpsManager { /** @hide */ @TestApi @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) - public void setMode(int code, int uid, String packageName, int mode) { + public void setMode(int code, int uid, String packageName, @Mode int mode) { try { mService.setMode(code, uid, packageName, mode); } catch (RemoteException e) { @@ -2701,7 +3812,7 @@ public class AppOpsManager { */ @SystemApi @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) - public void setMode(String op, int uid, String packageName, int mode) { + public void setMode(String op, int uid, String packageName, @Mode int mode) { try { mService.setMode(strOpToOp(op), uid, packageName, mode); } catch (RemoteException e) { @@ -2722,7 +3833,7 @@ public class AppOpsManager { */ @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) @UnsupportedAppUsage - public void setRestriction(int code, @AttributeUsage int usage, int mode, + public void setRestriction(int code, @AttributeUsage int usage, @Mode int mode, String[] exceptionPackages) { try { final int uid = Binder.getCallingUid(); @@ -3507,6 +4618,104 @@ public class AppOpsManager { } /** + * Configures the app ops persistence for testing. + * + * @param mode The mode in which the historical registry operates. + * @param baseSnapshotInterval The base interval on which we would be persisting a snapshot of + * the historical data. The history is recursive where every subsequent step encompasses + * {@code compressionStep} longer interval with {@code compressionStep} distance between + * snapshots. + * @param compressionStep The compression step in every iteration. + * + * @see #HISTORICAL_MODE_DISABLED + * @see #HISTORICAL_MODE_ENABLED_ACTIVE + * @see #HISTORICAL_MODE_ENABLED_PASSIVE + * + * @hide + */ + @TestApi + @RequiresPermission(Manifest.permission.MANAGE_APPOPS) + public void setHistoryParameters(@HistoricalMode int mode, long baseSnapshotInterval, + int compressionStep) { + try { + mService.setHistoryParameters(mode, baseSnapshotInterval, compressionStep); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Offsets the history by the given duration. + * + * @param offsetMillis The offset duration. + * + * @hide + */ + @TestApi + @RequiresPermission(Manifest.permission.MANAGE_APPOPS) + public void offsetHistory(long offsetMillis) { + try { + mService.offsetHistory(offsetMillis); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Adds ops to the history directly. This could be useful for testing especially + * when the historical registry operates in {@link #HISTORICAL_MODE_ENABLED_PASSIVE} + * mode. + * + * @param ops The ops to add to the history. + * + * @see #setHistoryParameters(int, long, int) + * @see #HISTORICAL_MODE_ENABLED_PASSIVE + * + * @hide + */ + @TestApi + @RequiresPermission(Manifest.permission.MANAGE_APPOPS) + public void addHistoricalOps(@NonNull HistoricalOps ops) { + try { + mService.addHistoricalOps(ops); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Resets the app ops persistence for testing. + * + * @see #setHistoryParameters(int, long, int) + * + * @hide + */ + @TestApi + @RequiresPermission(Manifest.permission.MANAGE_APPOPS) + public void resetHistoryParameters() { + try { + mService.resetHistoryParameters(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Clears all app ops history. + * + * @hide + */ + @TestApi + @RequiresPermission(Manifest.permission.MANAGE_APPOPS) + public void clearHistory() { + try { + mService.clearHistory(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Returns all supported operation names. * @hide */ @@ -3538,4 +4747,64 @@ public class AppOpsManager { } return time; } + + /** @hide */ + public static String uidStateToString(@UidState int uidState) { + switch (uidState) { + case UID_STATE_PERSISTENT: { + return "UID_STATE_PERSISTENT"; + } + case UID_STATE_TOP: { + return "UID_STATE_TOP"; + } + case UID_STATE_FOREGROUND_SERVICE: { + return "UID_STATE_FOREGROUND_SERVICE"; + } + case UID_STATE_FOREGROUND: { + return "UID_STATE_FOREGROUND"; + } + case UID_STATE_BACKGROUND: { + return "UID_STATE_BACKGROUND"; + } + case UID_STATE_CACHED: { + return "UID_STATE_CACHED"; + } + default: { + return "UNKNOWN"; + } + } + } + + /** @hide */ + public static int parseHistoricalMode(@NonNull String mode) { + switch (mode) { + case "HISTORICAL_MODE_ENABLED_ACTIVE": { + return HISTORICAL_MODE_ENABLED_ACTIVE; + } + case "HISTORICAL_MODE_ENABLED_PASSIVE": { + return HISTORICAL_MODE_ENABLED_PASSIVE; + } + default: { + return HISTORICAL_MODE_DISABLED; + } + } + } + + /** @hide */ + public static String historicalModeToString(@HistoricalMode int mode) { + switch (mode) { + case HISTORICAL_MODE_DISABLED: { + return "HISTORICAL_MODE_DISABLED"; + } + case HISTORICAL_MODE_ENABLED_ACTIVE: { + return "HISTORICAL_MODE_ENABLED_ACTIVE"; + } + case HISTORICAL_MODE_ENABLED_PASSIVE: { + return "HISTORICAL_MODE_ENABLED_PASSIVE"; + } + default: { + return "UNKNOWN"; + } + } + } } diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java index b5295d15ee8d..07dbb6bee9fd 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -124,6 +124,10 @@ public abstract class ClientTransactionHandler { /** Restart the activity after it was stopped. */ public abstract void performRestartActivity(IBinder token, boolean start); + /** Set pending activity configuration in case it will be updated by other transaction item. */ + public abstract void updatePendingActivityConfiguration(IBinder activityToken, + Configuration overrideConfig); + /** Deliver activity (override) configuration change. */ public abstract void handleActivityConfigurationChanged(IBinder activityToken, Configuration overrideConfig, int displayId); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 83c6fac6158e..d827f6cb9654 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -21,8 +21,11 @@ import android.accounts.IAccountManager; import android.app.ContextImpl.ServiceInitializationState; import android.app.admin.DevicePolicyManager; import android.app.admin.IDevicePolicyManager; +import android.app.contentsuggestions.ContentSuggestionsManager; +import android.app.contentsuggestions.IContentSuggestionsManager; import android.app.job.IJobScheduler; import android.app.job.JobScheduler; +import android.app.prediction.AppPredictionManager; import android.app.role.RoleManager; import android.app.slice.SliceManager; import android.app.timedetector.TimeDetector; @@ -1120,6 +1123,29 @@ final class SystemServiceRegistry { return null; }}); + registerService(Context.APP_PREDICTION_SERVICE, AppPredictionManager.class, + new CachedServiceFetcher<AppPredictionManager>() { + @Override + public AppPredictionManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + return new AppPredictionManager(ctx); + } + }); + + registerService(Context.CONTENT_SUGGESTIONS_SERVICE, + ContentSuggestionsManager.class, + new CachedServiceFetcher<ContentSuggestionsManager>() { + @Override + public ContentSuggestionsManager createService(ContextImpl ctx) { + // No throw as this is an optional service + IBinder b = ServiceManager.getService( + Context.CONTENT_SUGGESTIONS_SERVICE); + IContentSuggestionsManager service = + IContentSuggestionsManager.Stub.asInterface(b); + return new ContentSuggestionsManager(service); + } + }); + registerService(Context.VR_SERVICE, VrManager.class, new CachedServiceFetcher<VrManager>() { @Override public VrManager createService(ContextImpl ctx) throws ServiceNotFoundException { diff --git a/core/java/android/app/contentsuggestions/ClassificationsRequest.aidl b/core/java/android/app/contentsuggestions/ClassificationsRequest.aidl new file mode 100644 index 000000000000..a66413becc29 --- /dev/null +++ b/core/java/android/app/contentsuggestions/ClassificationsRequest.aidl @@ -0,0 +1,19 @@ +/** + * 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. + */ + +package android.app.contentsuggestions; + +parcelable ClassificationsRequest; diff --git a/core/java/android/app/contentsuggestions/ClassificationsRequest.java b/core/java/android/app/contentsuggestions/ClassificationsRequest.java new file mode 100644 index 000000000000..3f715186abfb --- /dev/null +++ b/core/java/android/app/contentsuggestions/ClassificationsRequest.java @@ -0,0 +1,113 @@ +/* + * 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. + */ + +package android.app.contentsuggestions; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.List; + +/** + * @hide + */ +@SystemApi +public final class ClassificationsRequest implements Parcelable { + @NonNull + private final List<ContentSelection> mSelections; + @Nullable + private final Bundle mExtras; + + private ClassificationsRequest(@NonNull List<ContentSelection> selections, + @Nullable Bundle extras) { + mSelections = selections; + mExtras = extras; + } + + /** + * Return request selections. + */ + public List<ContentSelection> getSelections() { + return mSelections; + } + + /** + * Return the request extras. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedList(mSelections); + dest.writeBundle(mExtras); + } + + public static final Creator<ClassificationsRequest> CREATOR = + new Creator<ClassificationsRequest>() { + @Override + public ClassificationsRequest createFromParcel(Parcel source) { + return new ClassificationsRequest( + source.createTypedArrayList(ContentSelection.CREATOR), + source.readBundle()); + } + + @Override + public ClassificationsRequest[] newArray(int size) { + return new ClassificationsRequest[size]; + } + }; + + /** + * A builder for classifications request events. + * @hide + */ + @SystemApi + public static final class Builder { + + private final List<ContentSelection> mSelections; + private Bundle mExtras; + + public Builder(@NonNull List<ContentSelection> selections) { + mSelections = selections; + } + + /** + * Sets the request extras. + */ + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds a new request instance. + */ + public ClassificationsRequest build() { + return new ClassificationsRequest(mSelections, mExtras); + } + } +} diff --git a/core/java/android/app/contentsuggestions/ContentClassification.aidl b/core/java/android/app/contentsuggestions/ContentClassification.aidl new file mode 100644 index 000000000000..f67b2c8cea36 --- /dev/null +++ b/core/java/android/app/contentsuggestions/ContentClassification.aidl @@ -0,0 +1,19 @@ +/** + * 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. + */ + +package android.app.contentsuggestions; + +parcelable ContentClassification; diff --git a/core/java/android/app/contentsuggestions/ContentClassification.java b/core/java/android/app/contentsuggestions/ContentClassification.java new file mode 100644 index 000000000000..f18520f1053c --- /dev/null +++ b/core/java/android/app/contentsuggestions/ContentClassification.java @@ -0,0 +1,79 @@ +/* + * 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. + */ + +package android.app.contentsuggestions; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +@SystemApi +public final class ContentClassification implements Parcelable { + @NonNull + private final String mClassificationId; + @NonNull + private final Bundle mExtras; + + public ContentClassification(@NonNull String classificationId, @NonNull Bundle extras) { + mClassificationId = classificationId; + mExtras = extras; + } + + /** + * Return the classification id. + */ + public String getId() { + return mClassificationId; + } + + /** + * Return the classification extras. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mClassificationId); + dest.writeBundle(mExtras); + } + + public static final Creator<ContentClassification> CREATOR = + new Creator<ContentClassification>() { + @Override + public ContentClassification createFromParcel(Parcel source) { + return new ContentClassification( + source.readString(), + source.readBundle()); + } + + @Override + public ContentClassification[] newArray(int size) { + return new ContentClassification[size]; + } + }; +} diff --git a/core/java/android/app/contentsuggestions/ContentSelection.aidl b/core/java/android/app/contentsuggestions/ContentSelection.aidl new file mode 100644 index 000000000000..e626d69278a4 --- /dev/null +++ b/core/java/android/app/contentsuggestions/ContentSelection.aidl @@ -0,0 +1,19 @@ +/** + * 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. + */ + +package android.app.contentsuggestions; + +parcelable ContentSelection; diff --git a/core/java/android/app/contentsuggestions/ContentSelection.java b/core/java/android/app/contentsuggestions/ContentSelection.java new file mode 100644 index 000000000000..a8917f782e83 --- /dev/null +++ b/core/java/android/app/contentsuggestions/ContentSelection.java @@ -0,0 +1,79 @@ +/* + * 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. + */ + +package android.app.contentsuggestions; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +@SystemApi +public final class ContentSelection implements Parcelable { + @NonNull + private final String mSelectionId; + @NonNull + private final Bundle mExtras; + + public ContentSelection(@NonNull String selectionId, @NonNull Bundle extras) { + mSelectionId = selectionId; + mExtras = extras; + } + + /** + * Return the selection id. + */ + public String getId() { + return mSelectionId; + } + + /** + * Return the selection extras. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mSelectionId); + dest.writeBundle(mExtras); + } + + public static final Creator<ContentSelection> CREATOR = + new Creator<ContentSelection>() { + @Override + public ContentSelection createFromParcel(Parcel source) { + return new ContentSelection( + source.readString(), + source.readBundle()); + } + + @Override + public ContentSelection[] newArray(int size) { + return new ContentSelection[size]; + } + }; +} diff --git a/core/java/android/app/contentsuggestions/ContentSuggestionsManager.java b/core/java/android/app/contentsuggestions/ContentSuggestionsManager.java new file mode 100644 index 000000000000..b4d89774cc3b --- /dev/null +++ b/core/java/android/app/contentsuggestions/ContentSuggestionsManager.java @@ -0,0 +1,230 @@ +/* + * 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. + */ + +package android.app.contentsuggestions; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Binder; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * When provided with content from an app, can suggest selections and classifications of that + * content. + * + * <p>The content is mainly a snapshot of a running task, the selections will be text and image + * selections with that image content. These mSelections can then be classified to find actions and + * entities on those selections. + * + * <p>Only accessible to blessed components such as Overview. + * + * @hide + */ +@SystemApi +public final class ContentSuggestionsManager { + private static final String TAG = ContentSuggestionsManager.class.getSimpleName(); + + @Nullable + private final IContentSuggestionsManager mService; + + /** @hide */ + public ContentSuggestionsManager(@Nullable IContentSuggestionsManager service) { + mService = service; + } + + /** + * Hints to the system that a new context image for the provided task should be sent to the + * system content suggestions service. + * + * @param taskId of the task to snapshot. + * @param imageContextRequestExtras sent with with request to provide implementation specific + * extra information. + */ + public void provideContextImage(int taskId, @NonNull Bundle imageContextRequestExtras) { + if (mService == null) { + Log.e(TAG, "provideContextImage called, but no ContentSuggestionsManager configured"); + return; + } + + try { + mService.provideContextImage(taskId, imageContextRequestExtras); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Suggest content selections, based on the provided task id and optional + * location on screen provided in the request. Called after provideContextImage(). + * The result can be passed to + * {@link #classifyContentSelections(ClassificationsRequest, Executor, ClassificationsCallback)} + * to classify actions and entities on these selections. + * + * @param request containing the task and point location. + * @param callbackExecutor to execute the provided callback on. + * @param callback to receive the selections. + */ + public void suggestContentSelections( + @NonNull SelectionsRequest request, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull SelectionsCallback callback) { + if (mService == null) { + Log.e(TAG, + "suggestContentSelections called, but no ContentSuggestionsManager configured"); + return; + } + + try { + mService.suggestContentSelections( + request, new SelectionsCallbackWrapper(callback, callbackExecutor)); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Classify actions and entities in content selections, as returned from + * suggestContentSelections. Note these selections may be modified by the + * caller before being passed here. + * + * @param request containing the selections to classify. + * @param callbackExecutor to execute the provided callback on. + * @param callback to receive the classifications. + */ + public void classifyContentSelections( + @NonNull ClassificationsRequest request, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull ClassificationsCallback callback) { + if (mService == null) { + Log.e(TAG, "classifyContentSelections called, " + + "but no ContentSuggestionsManager configured"); + return; + } + + try { + mService.classifyContentSelections( + request, new ClassificationsCallbackWrapper(callback, callbackExecutor)); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Report telemetry for interaction with suggestions / classifications. + * + * @param requestId the id for the associated interaction + * @param interaction to report back to the system content suggestions service. + */ + public void notifyInteraction(@NonNull String requestId, @NonNull Bundle interaction) { + if (mService == null) { + Log.e(TAG, "notifyInteraction called, but no ContentSuggestionsManager configured"); + return; + } + + try { + mService.notifyInteraction(requestId, interaction); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Callback to receive content selections from + * {@link #suggestContentSelections(SelectionsRequest, Executor, SelectionsCallback)}. + */ + public interface SelectionsCallback { + /** + * Async callback called when the content suggestions service has selections available. + * These can be modified and sent back to the manager for classification. The contents of + * the selection is implementation dependent. + * + * @param statusCode as defined by the implementation of content suggestions service. + * @param selections not {@code null}, but can be size {@code 0}. + */ + void onContentSelectionsAvailable( + int statusCode, @NonNull List<ContentSelection> selections); + } + + /** + * Callback to receive classifications from + * {@link #classifyContentSelections(ClassificationsRequest, Executor, ClassificationsCallback)} + */ + public interface ClassificationsCallback { + /** + * Async callback called when the content suggestions service has classified selections. The + * contents of the classification is implementation dependent. + * + * @param statusCode as defined by the implementation of content suggestions service. + * @param classifications not {@code null}, but can be size {@code 0}. + */ + void onContentClassificationsAvailable(int statusCode, + @NonNull List<ContentClassification> classifications); + } + + private static class SelectionsCallbackWrapper extends ISelectionsCallback.Stub { + private final SelectionsCallback mCallback; + private final Executor mExecutor; + + SelectionsCallbackWrapper( + @NonNull SelectionsCallback callback, @NonNull Executor executor) { + mCallback = callback; + mExecutor = executor; + } + + @Override + public void onContentSelectionsAvailable( + int statusCode, List<ContentSelection> selections) { + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> + mCallback.onContentSelectionsAvailable(statusCode, selections)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + private static final class ClassificationsCallbackWrapper extends + IClassificationsCallback.Stub { + private final ClassificationsCallback mCallback; + private final Executor mExecutor; + + ClassificationsCallbackWrapper(@NonNull ClassificationsCallback callback, + @NonNull Executor executor) { + mCallback = callback; + mExecutor = executor; + } + + @Override + public void onContentClassificationsAvailable( + int statusCode, List<ContentClassification> classifications) { + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> + mCallback.onContentClassificationsAvailable(statusCode, classifications)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } +} diff --git a/core/java/android/app/contentsuggestions/IClassificationsCallback.aidl b/core/java/android/app/contentsuggestions/IClassificationsCallback.aidl new file mode 100644 index 000000000000..69f5d5595986 --- /dev/null +++ b/core/java/android/app/contentsuggestions/IClassificationsCallback.aidl @@ -0,0 +1,25 @@ +/** + * 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. + */ + +package android.app.contentsuggestions; + +import android.app.contentsuggestions.ContentClassification; + +/** @hide */ +oneway interface IClassificationsCallback { + void onContentClassificationsAvailable( + int statusCode, in List<ContentClassification> classifications); +} diff --git a/core/java/android/app/contentsuggestions/IContentSuggestionsManager.aidl b/core/java/android/app/contentsuggestions/IContentSuggestionsManager.aidl new file mode 100644 index 000000000000..24f5ad866635 --- /dev/null +++ b/core/java/android/app/contentsuggestions/IContentSuggestionsManager.aidl @@ -0,0 +1,37 @@ +/* + * 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. + */ + +package android.app.contentsuggestions; + +import android.app.contentsuggestions.IClassificationsCallback; +import android.app.contentsuggestions.ISelectionsCallback; +import android.app.contentsuggestions.ClassificationsRequest; +import android.app.contentsuggestions.SelectionsRequest; +import android.os.Bundle; + +/** @hide */ +oneway interface IContentSuggestionsManager { + void provideContextImage( + int taskId, + in Bundle imageContextRequestExtras); + void suggestContentSelections( + in SelectionsRequest request, + in ISelectionsCallback callback); + void classifyContentSelections( + in ClassificationsRequest request, + in IClassificationsCallback callback); + void notifyInteraction(in String requestId, in Bundle interaction); +} diff --git a/core/java/android/app/contentsuggestions/ISelectionsCallback.aidl b/core/java/android/app/contentsuggestions/ISelectionsCallback.aidl new file mode 100644 index 000000000000..19525832a4cd --- /dev/null +++ b/core/java/android/app/contentsuggestions/ISelectionsCallback.aidl @@ -0,0 +1,25 @@ +/** + * 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. + */ + +package android.app.contentsuggestions; + +import android.app.contentsuggestions.ContentSelection; +import android.os.Bundle; + +/** @hide */ +oneway interface ISelectionsCallback { + void onContentSelectionsAvailable(int statusCode, in List<ContentSelection> selections); +} diff --git a/core/java/android/app/contentsuggestions/SelectionsRequest.aidl b/core/java/android/app/contentsuggestions/SelectionsRequest.aidl new file mode 100644 index 000000000000..f5ce7c31bd94 --- /dev/null +++ b/core/java/android/app/contentsuggestions/SelectionsRequest.aidl @@ -0,0 +1,19 @@ +/** + * 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. + */ + +package android.app.contentsuggestions; + +parcelable SelectionsRequest; diff --git a/core/java/android/app/contentsuggestions/SelectionsRequest.java b/core/java/android/app/contentsuggestions/SelectionsRequest.java new file mode 100644 index 000000000000..16f3e6b35aa4 --- /dev/null +++ b/core/java/android/app/contentsuggestions/SelectionsRequest.java @@ -0,0 +1,129 @@ +/* + * 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. + */ + +package android.app.contentsuggestions; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.graphics.Point; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +@SystemApi +public final class SelectionsRequest implements Parcelable { + private final int mTaskId; + @Nullable + private final Point mInterestPoint; + @Nullable + private final Bundle mExtras; + + private SelectionsRequest(int taskId, @Nullable Point interestPoint, @Nullable Bundle extras) { + mTaskId = taskId; + mInterestPoint = interestPoint; + mExtras = extras; + } + + /** + * Return the request task id. + */ + public int getTaskId() { + return mTaskId; + } + + /** + * Return the request point of interest. + */ + public Point getInterestPoint() { + return mInterestPoint; + } + + /** + * Return the request extras. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mTaskId); + dest.writeTypedObject(mInterestPoint, flags); + dest.writeBundle(mExtras); + } + + public static final Creator<SelectionsRequest> CREATOR = + new Creator<SelectionsRequest>() { + @Override + public SelectionsRequest createFromParcel(Parcel source) { + return new SelectionsRequest( + source.readInt(), source.readTypedObject(Point.CREATOR), source.readBundle()); + } + + @Override + public SelectionsRequest[] newArray(int size) { + return new SelectionsRequest[size]; + } + }; + + /** + * A builder for selections requests events. + * @hide + */ + @SystemApi + public static final class Builder { + + private final int mTaskId; + private Point mInterestPoint; + private Bundle mExtras; + + public Builder(int taskId) { + mTaskId = taskId; + } + + /** + * Sets the request extras. + */ + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Sets the request interest point. + */ + public Builder setInterestPoint(@NonNull Point interestPoint) { + mInterestPoint = interestPoint; + return this; + } + + /** + * Builds a new request instance. + */ + public SelectionsRequest build() { + return new SelectionsRequest(mTaskId, mInterestPoint, mExtras); + } + } +} diff --git a/core/java/android/app/prediction/AppPredictionContext.aidl b/core/java/android/app/prediction/AppPredictionContext.aidl new file mode 100644 index 000000000000..5767bf4dbc9a --- /dev/null +++ b/core/java/android/app/prediction/AppPredictionContext.aidl @@ -0,0 +1,19 @@ +/** + * 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. + */ + +package android.app.prediction; + +parcelable AppPredictionContext; diff --git a/core/java/android/app/prediction/AppPredictionContext.java b/core/java/android/app/prediction/AppPredictionContext.java new file mode 100644 index 000000000000..87ccb660e5bd --- /dev/null +++ b/core/java/android/app/prediction/AppPredictionContext.java @@ -0,0 +1,158 @@ +/* + * 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. + */ +package android.app.prediction; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * TODO(b/111701043): Add java docs + * + * @hide + */ +@SystemApi +public final class AppPredictionContext implements Parcelable { + + private final int mPredictedTargetCount; + @NonNull + private final String mUiSurface; + @NonNull + private final String mPackageName; + @Nullable + private final Bundle mExtras; + + private AppPredictionContext(@NonNull String uiSurface, int numPredictedTargets, + @NonNull String packageName, @Nullable Bundle extras) { + mUiSurface = uiSurface; + mPredictedTargetCount = numPredictedTargets; + mPackageName = packageName; + mExtras = extras; + } + + private AppPredictionContext(Parcel parcel) { + mUiSurface = parcel.readString(); + mPredictedTargetCount = parcel.readInt(); + mPackageName = parcel.readString(); + mExtras = parcel.readBundle(); + } + + public String getUiSurface() { + return mUiSurface; + } + + public int getPredictedTargetCount() { + return mPredictedTargetCount; + } + + @NonNull + public String getPackageName() { + return mPackageName; + } + + @Nullable + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mUiSurface); + dest.writeInt(mPredictedTargetCount); + dest.writeString(mPackageName); + dest.writeBundle(mExtras); + } + + /** + * @see Parcelable.Creator + */ + public static final Parcelable.Creator<AppPredictionContext> CREATOR = + new Parcelable.Creator<AppPredictionContext>() { + public AppPredictionContext createFromParcel(Parcel parcel) { + return new AppPredictionContext(parcel); + } + + public AppPredictionContext[] newArray(int size) { + return new AppPredictionContext[size]; + } + }; + + /** + * A builder for app prediction contexts. + * @hide + */ + @SystemApi + public static final class Builder { + + @NonNull + private final String mPackageName; + + private int mPredictedTargetCount; + @Nullable + private String mUiSurface; + @Nullable + private Bundle mExtras; + + /** + * @hide + */ + public Builder(@NonNull Context context) { + mPackageName = context.getPackageName(); + } + + + /** + * Sets the number of prediction targets as a hint. + */ + public Builder setPredictedTargetCount(int predictedTargetCount) { + mPredictedTargetCount = predictedTargetCount; + return this; + } + + /** + * Sets the UI surface. + */ + public Builder setUiSurface(@Nullable String uiSurface) { + mUiSurface = uiSurface; + return this; + } + + /** + * Sets the extras. + */ + public Builder setExtras(@Nullable Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds a new context instance. + */ + public AppPredictionContext build() { + return new AppPredictionContext(mUiSurface, mPredictedTargetCount, mPackageName, + mExtras); + } + } +} diff --git a/core/java/android/app/prediction/AppPredictionManager.java b/core/java/android/app/prediction/AppPredictionManager.java new file mode 100644 index 000000000000..f8578d4533ef --- /dev/null +++ b/core/java/android/app/prediction/AppPredictionManager.java @@ -0,0 +1,47 @@ +/* + * 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. + */ +package android.app.prediction; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Context; + +import com.android.internal.util.Preconditions; + +/** + * TODO (b/111701043) : Add java doc + * @hide + */ +@SystemApi +public final class AppPredictionManager { + + private final Context mContext; + + /** + * @hide + */ + public AppPredictionManager(Context context) { + mContext = Preconditions.checkNotNull(context); + } + + /** + * Creates a new app prediction session. + */ + public AppPredictor createAppPredictionSession( + @NonNull AppPredictionContext predictionContext) { + return new AppPredictor(mContext, predictionContext); + } +} diff --git a/core/java/android/app/prediction/AppPredictionSessionId.aidl b/core/java/android/app/prediction/AppPredictionSessionId.aidl new file mode 100644 index 000000000000..e8295261ae2a --- /dev/null +++ b/core/java/android/app/prediction/AppPredictionSessionId.aidl @@ -0,0 +1,19 @@ +/** + * 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. + */ + +package android.app.prediction; + +parcelable AppPredictionSessionId; diff --git a/core/java/android/app/prediction/AppPredictionSessionId.java b/core/java/android/app/prediction/AppPredictionSessionId.java new file mode 100644 index 000000000000..1d7308ec2404 --- /dev/null +++ b/core/java/android/app/prediction/AppPredictionSessionId.java @@ -0,0 +1,86 @@ +/* + * 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. + */ +package android.app.prediction; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * TODO (b/111701043) : Add java doc + * + * @hide + */ +@SystemApi +public final class AppPredictionSessionId implements Parcelable { + + private final String mId; + + /** + * @hide + */ + public AppPredictionSessionId(@NonNull String id) { + mId = id; + } + + private AppPredictionSessionId(Parcel p) { + mId = p.readString(); + } + + @Override + public boolean equals(Object o) { + if (!getClass().equals(o != null ? o.getClass() : null)) return false; + + AppPredictionSessionId other = (AppPredictionSessionId) o; + return mId.equals(other.mId); + } + + @Override + public @NonNull String toString() { + return mId; + } + + @Override + public int hashCode() { + // Ensure that the id has a consistent hash + return mId.hashCode(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + } + + /** + * @see Parcelable.Creator + */ + public static final Parcelable.Creator<AppPredictionSessionId> CREATOR = + new Parcelable.Creator<AppPredictionSessionId>() { + public AppPredictionSessionId createFromParcel(Parcel parcel) { + return new AppPredictionSessionId(parcel); + } + + public AppPredictionSessionId[] newArray(int size) { + return new AppPredictionSessionId[size]; + } + }; +} diff --git a/core/java/android/app/prediction/AppPredictor.java b/core/java/android/app/prediction/AppPredictor.java new file mode 100644 index 000000000000..2ddbd08c7477 --- /dev/null +++ b/core/java/android/app/prediction/AppPredictor.java @@ -0,0 +1,257 @@ +/* + * 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. + */ +package android.app.prediction; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.prediction.IPredictionCallback.Stub; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.ArrayMap; +import android.util.Log; + +import dalvik.system.CloseGuard; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +/** + * TODO (b/111701043) : Add java doc + * + * <p> + * Usage: <pre> {@code + * + * class MyActivity { + * private AppPredictor mClient + * + * void onCreate() { + * mClient = new AppPredictor(...) + * } + * + * void onStart() { + * mClient.requestPredictionUpdate(); + * } + * + * void onDestroy() { + * mClient.close(); + * } + * + * }</pre> + * + * @hide + */ +@SystemApi +public final class AppPredictor { + + private static final String TAG = AppPredictor.class.getSimpleName(); + + + private final IPredictionManager mPredictionManager; + private final CloseGuard mCloseGuard = CloseGuard.get(); + private final AtomicBoolean mIsClosed = new AtomicBoolean(false); + + private final AppPredictionSessionId mSessionId; + private final ArrayMap<Callback, CallbackWrapper> mRegisteredCallbacks = new ArrayMap<>(); + + /** + * Creates a new Prediction client. + * <p> + * The caller should call {@link AppPredictor#destroy()} to dispose the client once it + * no longer used. + * + * @param predictionContext The prediction context + */ + AppPredictor(@NonNull Context context, @NonNull AppPredictionContext predictionContext) { + IBinder b = ServiceManager.getService(Context.APP_PREDICTION_SERVICE); + mPredictionManager = IPredictionManager.Stub.asInterface(b); + mSessionId = new AppPredictionSessionId( + context.getPackageName() + ":" + UUID.randomUUID().toString()); + try { + mPredictionManager.createPredictionSession(predictionContext, mSessionId); + } catch (RemoteException e) { + Log.e(TAG, "Failed to create predictor", e); + return; + } + + mCloseGuard.open("close"); + } + + /** + * Notifies the prediction service of an app target event. + */ + public void notifyAppTargetEvent(@NonNull AppTargetEvent event) { + try { + mPredictionManager.notifyAppTargetEvent(mSessionId, event); + } catch (RemoteException e) { + Log.e(TAG, "Failed to notify app target event", e); + } + } + + /** + * Notifies the prediction service when the targets in a launch location are shown to the user. + */ + public void notifyLocationShown(@NonNull String launchLocation, + @NonNull List<AppTargetId> targetIds) { + try { + mPredictionManager.notifyLocationShown(mSessionId, launchLocation, + new ParceledListSlice<>(targetIds)); + } catch (RemoteException e) { + Log.e(TAG, "Failed to notify location shown event", e); + } + } + + /** + * Requests the prediction service provide continuous updates of App predictions via the + * provided callback, until the given callback is unregistered. + * + * @see Callback#onTargetsAvailable(List) + */ + public void registerPredictionUpdates(@NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull AppPredictor.Callback callback) { + if (mRegisteredCallbacks.containsKey(callback)) { + // Skip if this callback is already registered + return; + } + try { + final CallbackWrapper callbackWrapper = new CallbackWrapper(callbackExecutor, + callback::onTargetsAvailable); + mPredictionManager.registerPredictionUpdates(mSessionId, callbackWrapper); + mRegisteredCallbacks.put(callback, callbackWrapper); + } catch (RemoteException e) { + Log.e(TAG, "Failed to register for prediction updates", e); + } + } + + /** + * Requests the prediction service to stop providing continuous updates to the provided + * callback until the callback is re-registered. + */ + public void unregisterPredictionUpdates(@NonNull AppPredictor.Callback callback) { + if (!mRegisteredCallbacks.containsKey(callback)) { + // Skip if this callback was never registered + return; + } + try { + final CallbackWrapper callbackWrapper = mRegisteredCallbacks.remove(callback); + mPredictionManager.unregisterPredictionUpdates(mSessionId, callbackWrapper); + } catch (RemoteException e) { + Log.e(TAG, "Failed to unregister for prediction updates", e); + } + } + + /** + * Requests the prediction service to dispatch a new set of App predictions via the provided + * callback. + * + * @see Callback#onTargetsAvailable(List) + */ + public void requestPredictionUpdate() { + try { + mPredictionManager.requestPredictionUpdate(mSessionId); + } catch (RemoteException e) { + Log.e(TAG, "Failed to request prediction update", e); + } + } + + /** + * Returns a new list of AppTargets sorted based on prediction rank or {@code null} if the + * ranker is not available. + */ + @Nullable + public void sortTargets(@NonNull List<AppTarget> targets, + @NonNull Executor callbackExecutor, @NonNull Consumer<List<AppTarget>> callback) { + try { + mPredictionManager.sortAppTargets(mSessionId, new ParceledListSlice(targets), + new CallbackWrapper(callbackExecutor, callback)); + } catch (RemoteException e) { + Log.e(TAG, "Failed to sort targets", e); + } + } + + /** + * Destroys the client and unregisters the callback. Any method on this class after this call + * with throw {@link IllegalStateException}. + * + * TODO(b/111701043): Add state check in other methods. + */ + public void destroy() { + if (!mIsClosed.getAndSet(true)) { + mCloseGuard.close(); + + // Do destroy; + try { + mPredictionManager.onDestroyPredictionSession(mSessionId); + } catch (RemoteException e) { + Log.e(TAG, "Failed to notify app target event", e); + } + } + } + + @Override + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + destroy(); + } finally { + super.finalize(); + } + } + + /** + * Callback for receiving prediction updates. + */ + public interface Callback { + + /** + * Called when a new set of predicted app targets are available. + * @param targets Sorted list of predicted targets + */ + void onTargetsAvailable(@NonNull List<AppTarget> targets); + } + + static class CallbackWrapper extends Stub { + + private final Consumer<List<AppTarget>> mCallback; + private final Executor mExecutor; + + CallbackWrapper(@NonNull Executor callbackExecutor, + @NonNull Consumer<List<AppTarget>> callback) { + mCallback = callback; + mExecutor = callbackExecutor; + } + + @Override + public void onResult(ParceledListSlice result) { + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.accept(result.getList())); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } +} diff --git a/core/java/android/app/prediction/AppTarget.aidl b/core/java/android/app/prediction/AppTarget.aidl new file mode 100644 index 000000000000..e4e2bc2aaaf5 --- /dev/null +++ b/core/java/android/app/prediction/AppTarget.aidl @@ -0,0 +1,19 @@ +/** + * 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. + */ + +package android.app.prediction; + +parcelable AppTarget; diff --git a/core/java/android/app/prediction/AppTarget.java b/core/java/android/app/prediction/AppTarget.java new file mode 100644 index 000000000000..99c1c44866fe --- /dev/null +++ b/core/java/android/app/prediction/AppTarget.java @@ -0,0 +1,166 @@ +/* + * 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. + */ +package android.app.prediction; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.pm.ShortcutInfo; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.UserHandle; + +import com.android.internal.util.Preconditions; + +/** + * A representation of a launchable target. + * @hide + */ +@SystemApi +public final class AppTarget implements Parcelable { + + private final AppTargetId mId; + private final String mPackageName; + private final String mClassName; + private final UserHandle mUser; + + private final ShortcutInfo mShortcutInfo; + + private int mRank; + + /** + * @hide + */ + public AppTarget(@NonNull AppTargetId id, @NonNull String packageName, + @Nullable String className, @NonNull UserHandle user) { + mId = id; + mShortcutInfo = null; + + mPackageName = Preconditions.checkNotNull(packageName); + mClassName = className; + mUser = Preconditions.checkNotNull(user); + } + + /** + * @hide + */ + public AppTarget(@NonNull AppTargetId id, @NonNull ShortcutInfo shortcutInfo) { + mId = id; + mShortcutInfo = Preconditions.checkNotNull(shortcutInfo); + + mPackageName = mShortcutInfo.getPackage(); + mUser = mShortcutInfo.getUserHandle(); + mClassName = null; + } + + private AppTarget(Parcel parcel) { + mId = parcel.readTypedObject(AppTargetId.CREATOR); + mShortcutInfo = parcel.readTypedObject(ShortcutInfo.CREATOR); + if (mShortcutInfo == null) { + mPackageName = parcel.readString(); + mClassName = parcel.readString(); + mUser = UserHandle.of(parcel.readInt()); + } else { + mPackageName = mShortcutInfo.getPackage(); + mUser = mShortcutInfo.getUserHandle(); + mClassName = null; + } + mRank = parcel.readInt(); + } + + /** + * Returns the target id. + */ + @NonNull + public AppTargetId getId() { + return mId; + } + + /** + * Returns the class name for the app target. + */ + @Nullable + public String getClassName() { + return mClassName; + } + + /** + * Returns the package name for the app target. + */ + @NonNull + public String getPackageName() { + return mPackageName; + } + + /** + * Returns the user for the app target. + */ + @NonNull + public UserHandle getUser() { + return mUser; + } + + /** + * Returns the shortcut info for the target. + */ + @Nullable + public ShortcutInfo getShortcutInfo() { + return mShortcutInfo; + } + + /** + * Sets the rank of the for the target. + * @hide + */ + public void setRank(int rank) { + mRank = rank; + } + + public int getRank() { + return mRank; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedObject(mId, flags); + dest.writeTypedObject(mShortcutInfo, flags); + if (mShortcutInfo == null) { + dest.writeString(mPackageName); + dest.writeString(mClassName); + dest.writeInt(mUser.getIdentifier()); + } + dest.writeInt(mRank); + } + + /** + * @see Parcelable.Creator + */ + public static final Parcelable.Creator<AppTarget> CREATOR = + new Parcelable.Creator<AppTarget>() { + public AppTarget createFromParcel(Parcel parcel) { + return new AppTarget(parcel); + } + + public AppTarget[] newArray(int size) { + return new AppTarget[size]; + } + }; +} diff --git a/core/java/android/app/prediction/AppTargetEvent.aidl b/core/java/android/app/prediction/AppTargetEvent.aidl new file mode 100644 index 000000000000..ebed2dadbee6 --- /dev/null +++ b/core/java/android/app/prediction/AppTargetEvent.aidl @@ -0,0 +1,19 @@ +/** + * 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. + */ + +package android.app.prediction; + +parcelable AppTargetEvent; diff --git a/core/java/android/app/prediction/AppTargetEvent.java b/core/java/android/app/prediction/AppTargetEvent.java new file mode 100644 index 000000000000..18317e1bf99d --- /dev/null +++ b/core/java/android/app/prediction/AppTargetEvent.java @@ -0,0 +1,154 @@ +/* + * 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. + */ +package android.app.prediction; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A representation of an app target event. + * @hide + */ +@SystemApi +public final class AppTargetEvent implements Parcelable { + + /** + * @hide + */ + @IntDef({ACTION_LAUNCH, ACTION_DISMISS, ACTION_PIN}) + @Retention(RetentionPolicy.SOURCE) + public @interface ActionType {} + + /** + * Event type constant indicating an app target has been launched. + */ + public static final int ACTION_LAUNCH = 1; + + /** + * Event type constant indicating an app target has been dismissed. + */ + public static final int ACTION_DISMISS = 2; + + /** + * Event type constant indicating an app target has been pinned. + */ + public static final int ACTION_PIN = 3; + + private final AppTarget mTarget; + private final String mLocation; + private final int mAction; + + private AppTargetEvent(@Nullable AppTarget target, @Nullable String location, + @ActionType int actionType) { + mTarget = target; + mLocation = location; + mAction = actionType; + } + + private AppTargetEvent(Parcel parcel) { + mTarget = parcel.readParcelable(null); + mLocation = parcel.readString(); + mAction = parcel.readInt(); + } + + /** + * Returns the app target. + */ + @Nullable + public AppTarget getTarget() { + return mTarget; + } + + /** + * Returns the launch location. + */ + @NonNull + public String getLaunchLocation() { + return mLocation; + } + + /** + * Returns the action type. + */ + @NonNull + public int getAction() { + return mAction; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mTarget, 0); + dest.writeString(mLocation); + dest.writeInt(mAction); + } + + /** + * @see Creator + */ + public static final Creator<AppTargetEvent> CREATOR = + new Creator<AppTargetEvent>() { + public AppTargetEvent createFromParcel(Parcel parcel) { + return new AppTargetEvent(parcel); + } + + public AppTargetEvent[] newArray(int size) { + return new AppTargetEvent[size]; + } + }; + + /** + * A builder for app target events. + * @hide + */ + @SystemApi + public static final class Builder { + private AppTarget mTarget; + private String mLocation; + private @ActionType int mAction; + + public Builder(@Nullable AppTarget target, @ActionType int actionType) { + mTarget = target; + mAction = actionType; + } + + /** + * Sets the launch location. + */ + public Builder setLaunchLocation(String location) { + mLocation = location; + return this; + } + + /** + * Builds a new event instance. + */ + public AppTargetEvent build() { + return new AppTargetEvent(mTarget, mLocation, mAction); + } + } +} diff --git a/core/java/android/app/prediction/AppTargetId.aidl b/core/java/android/app/prediction/AppTargetId.aidl new file mode 100644 index 000000000000..bf69eeaca691 --- /dev/null +++ b/core/java/android/app/prediction/AppTargetId.aidl @@ -0,0 +1,19 @@ +/** + * 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. + */ + +package android.app.prediction; + +parcelable AppTargetId; diff --git a/core/java/android/app/prediction/AppTargetId.java b/core/java/android/app/prediction/AppTargetId.java new file mode 100644 index 000000000000..0b8fb47377d2 --- /dev/null +++ b/core/java/android/app/prediction/AppTargetId.java @@ -0,0 +1,90 @@ +/* + * 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. + */ +package android.app.prediction; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * The id for a prediction target. + * @hide + */ +@SystemApi +public final class AppTargetId implements Parcelable { + + @NonNull + private final String mId; + + /** + * @hide + */ + public AppTargetId(@NonNull String id) { + mId = id; + } + + private AppTargetId(Parcel parcel) { + mId = parcel.readString(); + } + + /** + * Returns the id. + * @hide + */ + @NonNull + public String getId() { + return mId; + } + + @Override + public boolean equals(Object o) { + if (!getClass().equals(o != null ? o.getClass() : null)) return false; + + AppTargetId other = (AppTargetId) o; + return mId.equals(other.mId); + } + + @Override + public int hashCode() { + // Ensure that the id has a consistent hash + return mId.hashCode(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + } + + /** + * @see Creator + */ + public static final Creator<AppTargetId> CREATOR = + new Creator<AppTargetId>() { + public AppTargetId createFromParcel(Parcel parcel) { + return new AppTargetId(parcel); + } + + public AppTargetId[] newArray(int size) { + return new AppTargetId[size]; + } + }; +} diff --git a/core/java/android/app/prediction/IPredictionCallback.aidl b/core/java/android/app/prediction/IPredictionCallback.aidl new file mode 100644 index 000000000000..f6f241e5560d --- /dev/null +++ b/core/java/android/app/prediction/IPredictionCallback.aidl @@ -0,0 +1,27 @@ +/* + * 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. + */ + +package android.app.prediction; + +import android.content.pm.ParceledListSlice; + +/** + * @hide + */ +oneway interface IPredictionCallback { + + void onResult(in ParceledListSlice result); +} diff --git a/core/java/android/app/prediction/IPredictionManager.aidl b/core/java/android/app/prediction/IPredictionManager.aidl new file mode 100644 index 000000000000..114a1ffb0eb2 --- /dev/null +++ b/core/java/android/app/prediction/IPredictionManager.aidl @@ -0,0 +1,51 @@ +/* + * 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. + */ + +package android.app.prediction; + +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.IPredictionCallback; +import android.content.pm.ParceledListSlice; + +/** + * @hide + */ +interface IPredictionManager { + + void createPredictionSession(in AppPredictionContext context, + in AppPredictionSessionId sessionId); + + void notifyAppTargetEvent(in AppPredictionSessionId sessionId, in AppTargetEvent event); + + void notifyLocationShown(in AppPredictionSessionId sessionId, in String launchLocation, + in ParceledListSlice targetIds); + + void sortAppTargets(in AppPredictionSessionId sessionId, in ParceledListSlice targets, + in IPredictionCallback callback); + + void registerPredictionUpdates(in AppPredictionSessionId sessionId, + in IPredictionCallback callback); + + void unregisterPredictionUpdates(in AppPredictionSessionId sessionId, + in IPredictionCallback callback); + + void requestPredictionUpdate(in AppPredictionSessionId sessionId); + + void onDestroyPredictionSession(in AppPredictionSessionId sessionId); +} diff --git a/core/java/android/app/role/IRoleManager.aidl b/core/java/android/app/role/IRoleManager.aidl index 0c9b41bf8d68..2964fbc0084b 100644 --- a/core/java/android/app/role/IRoleManager.aidl +++ b/core/java/android/app/role/IRoleManager.aidl @@ -50,4 +50,6 @@ interface IRoleManager { boolean removeRoleHolderFromController(in String roleName, in String packageName); List<String> getHeldRolesFromController(in String packageName); + + String getDefaultSmsPackage(int userId); } diff --git a/core/java/android/app/role/RoleManager.java b/core/java/android/app/role/RoleManager.java index d73e73f442d6..f14c56892807 100644 --- a/core/java/android/app/role/RoleManager.java +++ b/core/java/android/app/role/RoleManager.java @@ -19,6 +19,7 @@ package android.app.role; import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; @@ -592,7 +593,6 @@ public final class RoleManager { } } - /** * Returns the list of all roles that the given package is currently holding * @@ -613,6 +613,22 @@ public final class RoleManager { } } + /** + * Allows getting the role holder for {@link #ROLE_SMS} without + * {@link Manifest.permission#OBSERVE_ROLE_HOLDERS}, as required by + * {@link android.provider.Telephony.Sms#getDefaultSmsPackage(Context)} + * + * @hide + */ + @Nullable + public String getDefaultSmsPackage(@UserIdInt int userId) { + try { + return mService.getDefaultSmsPackage(userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + private static class RoleManagerCallbackDelegate extends IRoleManagerCallback.Stub { @NonNull diff --git a/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java b/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java index a2b7d5809c4d..8ee9e5381b86 100644 --- a/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java +++ b/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java @@ -36,6 +36,11 @@ public class ActivityConfigurationChangeItem extends ClientTransactionItem { private Configuration mConfiguration; @Override + public void preExecute(android.app.ClientTransactionHandler client, IBinder token) { + client.updatePendingActivityConfiguration(token, mConfiguration); + } + + @Override public void execute(ClientTransactionHandler client, IBinder token, PendingTransactionActions pendingActions) { // TODO(lifecycler): detect if PIP or multi-window mode changed and report it here. diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 6f866eab56ea..cefc700d372a 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -337,6 +337,15 @@ public abstract class Context { public static final int BIND_ADJUST_BELOW_PERCEPTIBLE = 0x0100; /** + * @hide Flag for {@link #bindService}: the service being bound to represents a + * protected system component, so must have association restrictions applied to it. + * That is, a system config must have one or more allow-association tags limiting + * which packages it can interact with. If it does not have any such association + * restrictions, a default empty set will be created. + */ + public static final int BIND_RESTRICT_ASSOCIATIONS = 0x00200000; + + /** * @hide Flag for {@link #bindService}: allows binding to a service provided * by an instant app. Note that the caller may not have access to the instant * app providing the service which is a violation of the instant app sandbox. @@ -3984,6 +3993,24 @@ public abstract class Context { public static final String CONTENT_CAPTURE_MANAGER_SERVICE = "content_capture"; /** + * Used for getting content selections and classifications for task snapshots. + * + * @hide + * @see #getSystemService(String) + */ + @SystemApi + public static final String CONTENT_SUGGESTIONS_SERVICE = "content_suggestions"; + + /** + * Official published name of the app prediction service. + * + * @hide + * @see #getSystemService(String) + */ + @SystemApi + public static final String APP_PREDICTION_SERVICE = "app_prediction"; + + /** * Use with {@link #getSystemService(String)} to access the * {@link com.android.server.voiceinteraction.SoundTriggerService}. * diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 2978058b2848..576466f21bcb 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -1866,7 +1866,15 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { return (privateFlags & ApplicationInfo.PRIVATE_FLAG_DIRECT_BOOT_AWARE) != 0; } - /** @hide */ + /** + * Check whether the application is encryption aware. + * + * @see #isDirectBootAware() + * @see #isPartiallyDirectBootAware() + * + * @hide + */ + @SystemApi public boolean isEncryptionAware() { return isDirectBootAware() || isPartiallyDirectBootAware(); } diff --git a/core/java/android/content/pm/PackageUserState.java b/core/java/android/content/pm/PackageUserState.java index be6ed51e3c89..74dd08fc1d6b 100644 --- a/core/java/android/content/pm/PackageUserState.java +++ b/core/java/android/content/pm/PackageUserState.java @@ -29,8 +29,11 @@ import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; import android.annotation.UnsupportedAppUsage; import android.os.BaseBundle; +import android.os.Debug; import android.os.PersistableBundle; import android.util.ArraySet; +import android.util.DebugUtils; +import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; @@ -43,6 +46,9 @@ import java.util.Objects; * @hide */ public class PackageUserState { + private static final boolean DEBUG = false; + private static final String LOG_TAG = "PackageUserState"; + public long ceDataInode; public boolean installed; public boolean stopped; @@ -132,12 +138,12 @@ public class PackageUserState { final boolean isSystemApp = componentInfo.applicationInfo.isSystemApp(); final boolean matchUninstalled = (flags & PackageManager.MATCH_KNOWN_PACKAGES) != 0; if (!isAvailable(flags) - && !(isSystemApp && matchUninstalled)) return false; - if (!isEnabled(componentInfo, flags)) return false; + && !(isSystemApp && matchUninstalled)) return reportIfDebug(false, flags); + if (!isEnabled(componentInfo, flags)) return reportIfDebug(false, flags); if ((flags & MATCH_SYSTEM_ONLY) != 0) { if (!isSystemApp) { - return false; + return reportIfDebug(false, flags); } } @@ -145,7 +151,16 @@ public class PackageUserState { && !componentInfo.directBootAware; final boolean matchesAware = ((flags & MATCH_DIRECT_BOOT_AWARE) != 0) && componentInfo.directBootAware; - return matchesUnaware || matchesAware; + return reportIfDebug(matchesUnaware || matchesAware, flags); + } + + private boolean reportIfDebug(boolean result, int flags) { + if (DEBUG && !result) { + Slog.i(LOG_TAG, "No match!; flags: " + + DebugUtils.flagsToString(PackageManager.class, "MATCH_", flags) + " " + + Debug.getCaller()); + } + return result; } /** diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java index a8c3b889421b..6e519c1863e8 100644 --- a/core/java/android/content/pm/RegisteredServicesCache.java +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -475,6 +475,14 @@ public abstract class RegisteredServicesCache<V> { final List<ResolveInfo> resolveInfos = queryIntentServices(userId); for (ResolveInfo resolveInfo : resolveInfos) { try { + // if this package is not one of those changedUids, we don't need to scan it, + // since nothing in it changed, so save a call to parseServiceInfo, which + // can cause a large amount of the package apk to be loaded into memory. + // if this is the initial scan, changedUids will be null, and containsUid will + // trivially return true, and will call parseServiceInfo + if (!containsUid(changedUids, resolveInfo.serviceInfo.applicationInfo.uid)) { + continue; + } ServiceInfo<V> info = parseServiceInfo(resolveInfo); if (info == null) { Log.w(TAG, "Unable to load service info " + resolveInfo.toString()); diff --git a/core/java/android/hardware/display/ColorDisplayManager.java b/core/java/android/hardware/display/ColorDisplayManager.java index dd782ec3a42f..0cf2d18e5c04 100644 --- a/core/java/android/hardware/display/ColorDisplayManager.java +++ b/core/java/android/hardware/display/ColorDisplayManager.java @@ -16,7 +16,9 @@ package android.hardware.display; +import android.Manifest; import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.annotation.SystemService; import android.content.Context; import android.os.IBinder; @@ -31,6 +33,7 @@ import com.android.internal.R; * * @hide */ +@SystemApi @SystemService(Context.COLOR_DISPLAY_SERVICE) public final class ColorDisplayManager { @@ -48,13 +51,29 @@ public final class ColorDisplayManager { * * @hide */ - @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) + @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) public boolean isDeviceColorManaged() { return mManager.isDeviceColorManaged(); } /** + * Set the level of color saturation to apply to the display. + * + * @param saturationLevel 0-100 (inclusive), where 100 is full saturation + * @return whether the saturation level change was applied successfully + * + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) + public boolean setSaturationLevel(int saturationLevel) { + return mManager.setSaturationLevel(saturationLevel); + } + + /** * Returns {@code true} if Night Display is supported by the device. + * + * @hide */ public static boolean isNightDisplayAvailable(Context context) { return context.getResources().getBoolean(R.bool.config_nightDisplayAvailable); @@ -62,6 +81,8 @@ public final class ColorDisplayManager { /** * Returns {@code true} if display white balance is supported by the device. + * + * @hide */ public static boolean isDisplayWhiteBalanceAvailable(Context context) { return context.getResources().getBoolean(R.bool.config_displayWhiteBalanceAvailable); @@ -99,5 +120,13 @@ public final class ColorDisplayManager { throw e.rethrowFromSystemServer(); } } + + boolean setSaturationLevel(int saturationLevel) { + try { + return mCdm.setSaturationLevel(saturationLevel); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } } diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 82e765dee447..44b653c314ad 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -563,11 +563,16 @@ public final class DisplayManager { * 0 produces a grayscale image, 1 is normal. * * @hide + * @deprecated use {@link ColorDisplayManager#setSaturationLevel(int)}. */ @SystemApi @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_SATURATION) public void setSaturationLevel(float level) { - mGlobal.setSaturationLevel(level); + if (level < 0f || level > 1f) { + throw new IllegalArgumentException("Saturation level must be between 0 and 1"); + } + final ColorDisplayManager cdm = mContext.getSystemService(ColorDisplayManager.class); + cdm.setSaturationLevel(Math.round(level * 100f)); } /** diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index 7304ab4aa1df..cda8498c4f01 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -394,17 +394,6 @@ public final class DisplayManagerGlobal { } } - /** - * Set the level of color saturation to apply to the display. - */ - public void setSaturationLevel(float level) { - try { - mDm.setSaturationLevel(level); - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } - } - public VirtualDisplay createVirtualDisplay(Context context, MediaProjection projection, String name, int width, int height, int densityDpi, Surface surface, int flags, VirtualDisplay.Callback callback, Handler handler, String uniqueId) { diff --git a/core/java/android/hardware/display/IColorDisplayManager.aidl b/core/java/android/hardware/display/IColorDisplayManager.aidl index f7865899812a..81b82c64461f 100644 --- a/core/java/android/hardware/display/IColorDisplayManager.aidl +++ b/core/java/android/hardware/display/IColorDisplayManager.aidl @@ -19,4 +19,6 @@ package android.hardware.display; /** @hide */ interface IColorDisplayManager { boolean isDeviceColorManaged(); + + boolean setSaturationLevel(int saturationLevel); }
\ No newline at end of file diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl index b57599724ad5..2d81cdfec179 100644 --- a/core/java/android/hardware/display/IDisplayManager.aidl +++ b/core/java/android/hardware/display/IDisplayManager.aidl @@ -66,9 +66,6 @@ interface IDisplayManager { // Requires CONFIGURE_DISPLAY_COLOR_MODE void requestColorMode(int displayId, int colorMode); - // Requires CONTROL_DISPLAY_SATURATION - void setSaturationLevel(float level); - // Requires CAPTURE_VIDEO_OUTPUT, CAPTURE_SECURE_VIDEO_OUTPUT, or an appropriate // MediaProjection token for certain combinations of flags. int createVirtualDisplay(in IVirtualDisplayCallback callback, diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index cc6bb12a0894..b9cdcc096962 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -899,6 +899,33 @@ public final class Parcel { } /** + * Flatten an {@link ArrayMap} with string keys containing a particular object + * type into the parcel at the current dataPosition() and growing dataCapacity() + * if needed. The type of the objects in the array must be one that implements + * Parcelable. Only the raw data of the objects is written and not their type, + * so you must use the corresponding {@link #createTypedArrayMap(Parcelable.Creator)} + * + * @param val The map of objects to be written. + * @param parcelableFlags The parcelable flags to use. + * + * @see #createTypedArrayMap(Parcelable.Creator) + * @see Parcelable + */ + public <T extends Parcelable> void writeTypedArrayMap(@Nullable ArrayMap<String, T> val, + int parcelableFlags) { + if (val == null) { + writeInt(-1); + return; + } + final int count = val.size(); + writeInt(count); + for (int i = 0; i < count; i++) { + writeString(val.keyAt(i)); + writeTypedObject(val.valueAt(i), parcelableFlags); + } + } + + /** * Write an array set to the parcel. * * @param val The array set to write. @@ -1001,7 +1028,7 @@ public final class Parcel { * values are written using {@link #writeValue} and must follow the * specification there. */ - public final void writeSparseArray(@Nullable SparseArray<Object> val) { + public final <T> void writeSparseArray(@Nullable SparseArray<T> val) { if (val == null) { writeInt(-1); return; @@ -1400,6 +1427,34 @@ public final class Parcel { } /** + * Flatten a {@link SparseArray} containing a particular object type into the parcel + * at the current dataPosition() and growing dataCapacity() if needed. The + * type of the objects in the array must be one that implements Parcelable. + * Unlike the generic {@link #writeSparseArray(SparseArray)} method, however, only + * the raw data of the objects is written and not their type, so you must use the + * corresponding {@link #createTypedSparseArray(Parcelable.Creator)}. + * + * @param val The list of objects to be written. + * @param parcelableFlags The parcelable flags to use. + * + * @see #createTypedSparseArray(Parcelable.Creator) + * @see Parcelable + */ + public final <T extends Parcelable> void writeTypedSparseArray(@Nullable SparseArray<T> val, + int parcelableFlags) { + if (val == null) { + writeInt(-1); + return; + } + final int count = val.size(); + writeInt(count); + for (int i = 0; i < count; i++) { + writeInt(val.keyAt(i)); + writeTypedObject(val.valueAt(i), parcelableFlags); + } + } + + /** * @hide */ public <T extends Parcelable> void writeTypedList(@Nullable List<T> val, int parcelableFlags) { @@ -2369,7 +2424,7 @@ public final class Parcel { * Parcelables. */ @Nullable - public final SparseArray readSparseArray(@Nullable ClassLoader loader) { + public final <T> SparseArray<T> readSparseArray(@Nullable ClassLoader loader) { int N = readInt(); if (N < 0) { return null; @@ -2466,6 +2521,62 @@ public final class Parcel { } /** + * Read into a new {@link SparseArray} items containing a particular object type + * that were written with {@link #writeTypedSparseArray(SparseArray, int)} at the + * current dataPosition(). The list <em>must</em> have previously been written + * via {@link #writeTypedSparseArray(SparseArray, int)} with the same object type. + * + * @param creator The creator to use when for instantiation. + * + * @return A newly created {@link SparseArray} containing objects with the same data + * as those that were previously written. + * + * @see #writeTypedSparseArray(SparseArray, int) + */ + public final @Nullable <T extends Parcelable> SparseArray<T> createTypedSparseArray( + @NonNull Parcelable.Creator<T> creator) { + final int count = readInt(); + if (count < 0) { + return null; + } + final SparseArray<T> array = new SparseArray<>(count); + for (int i = 0; i < count; i++) { + final int index = readInt(); + final T value = readTypedObject(creator); + array.append(index, value); + } + return array; + } + + /** + * Read into a new {@link ArrayMap} with string keys items containing a particular + * object type that were written with {@link #writeTypedArrayMap(ArrayMap, int)} at the + * current dataPosition(). The list <em>must</em> have previously been written + * via {@link #writeTypedArrayMap(ArrayMap, int)} with the same object type. + * + * @param creator The creator to use when for instantiation. + * + * @return A newly created {@link ArrayMap} containing objects with the same data + * as those that were previously written. + * + * @see #writeTypedArrayMap(ArrayMap, int) + */ + public final @Nullable <T extends Parcelable> ArrayMap<String, T> createTypedArrayMap( + @NonNull Parcelable.Creator<T> creator) { + final int count = readInt(); + if (count < 0) { + return null; + } + final ArrayMap<String, T> map = new ArrayMap<>(count); + for (int i = 0; i < count; i++) { + final String key = readString(); + final T value = readTypedObject(creator); + map.append(key, value); + } + return map; + } + + /** * Read and return a new ArrayList containing String objects from * the parcel that was written with {@link #writeStringList} at the * current dataPosition(). Returns null if the diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index c3e04894c6c9..0942d97df6c7 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -1326,7 +1326,7 @@ public final class PowerManager { * @return Battery saver state data. * * @hide - * @see com.android.server.power.BatterySaverPolicy + * @see com.android.server.power.batterysaver.BatterySaverPolicy * @see PowerSaveState */ public PowerSaveState getPowerSaveState(@ServiceType int serviceType) { diff --git a/core/java/android/os/PowerSaveState.java b/core/java/android/os/PowerSaveState.java index de1128dfdef5..4918ad107763 100644 --- a/core/java/android/os/PowerSaveState.java +++ b/core/java/android/os/PowerSaveState.java @@ -27,7 +27,7 @@ public class PowerSaveState implements Parcelable { /** * Whether we should enable battery saver for this service. * - * @see com.android.server.power.BatterySaverPolicy + * @see com.android.server.power.batterysaver.BatterySaverPolicy */ public final boolean batterySaverEnabled; /** diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 8d3de842f720..4649776b3d50 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6016,6 +6016,15 @@ public final class Settings { "lock_screen_allow_remote_input"; /** + * Indicates which clock face to show on lock screen and AOD. + * @hide + */ + public static final String LOCK_SCREEN_CUSTOM_CLOCK_FACE = "lock_screen_custom_clock_face"; + + private static final Validator LOCK_SCREEN_CUSTOM_CLOCK_FACE_VALIDATOR = + ANY_STRING_VALIDATOR; + + /** * Set by the system to track if the user needs to see the call to action for * the lockscreen notification policy. * @hide @@ -8459,6 +8468,7 @@ public final class Settings { HUSH_GESTURE_USED, IN_CALL_NOTIFICATION_ENABLED, LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + LOCK_SCREEN_CUSTOM_CLOCK_FACE, LOCK_SCREEN_SHOW_NOTIFICATIONS, ZEN_DURATION, SHOW_ZEN_UPGRADE_NOTIFICATION, @@ -8637,6 +8647,7 @@ public final class Settings { VALIDATORS.put(ASSIST_GESTURE_SETUP_COMPLETE, BOOLEAN_VALIDATOR); VALIDATORS.put(NOTIFICATION_NEW_INTERRUPTION_MODEL, BOOLEAN_VALIDATOR); VALIDATORS.put(TRUST_AGENTS_EXTEND_UNLOCK, TRUST_AGENTS_EXTEND_UNLOCK_VALIDATOR); + VALIDATORS.put(LOCK_SCREEN_CUSTOM_CLOCK_FACE, LOCK_SCREEN_CUSTOM_CLOCK_FACE_VALIDATOR); VALIDATORS.put(LOCK_SCREEN_WHEN_TRUST_LOST, LOCK_SCREEN_WHEN_TRUST_LOST_VALIDATOR); } @@ -11207,14 +11218,14 @@ public final class Settings { * quick_doze_enabled (boolean) * </pre> * @hide - * @see com.android.server.power.BatterySaverPolicy + * @see com.android.server.power.batterysaver.BatterySaverPolicy */ public static final String BATTERY_SAVER_CONSTANTS = "battery_saver_constants"; /** * Battery Saver device specific settings * This is encoded as a key=value list, separated by commas. - * See {@link com.android.server.power.BatterySaverPolicy} for the details. + * See {@link com.android.server.power.batterysaver.BatterySaverPolicy} for the details. * * @hide */ @@ -13955,6 +13966,53 @@ public final class Settings { */ public static final String NATIVE_FLAGS_HEALTH_CHECK_ENABLED = "native_flags_health_check_enabled"; + + /** + * Parameter for {@link #APPOP_HISTORY_PARAMETERS} that controls the mode + * in which the historical registry operates. + * + * @hide + */ + public static final String APPOP_HISTORY_MODE = "mode"; + + /** + * Parameter for {@link #APPOP_HISTORY_PARAMETERS} that controls how long + * is the interval between snapshots in the base case i.e. the most recent + * part of the history. + * + * @hide + */ + public static final String APPOP_HISTORY_BASE_INTERVAL_MILLIS = "baseIntervalMillis"; + + /** + * Parameter for {@link #APPOP_HISTORY_PARAMETERS} that controls the base + * for the logarithmic step when building app op history. + * + * @hide + */ + public static final String APPOP_HISTORY_INTERVAL_MULTIPLIER = "intervalMultiplier"; + + /** + * Appop history parameters. These parameters are represented by + * a comma-delimited key-value list. + * + * The following strings are supported as keys: + * <pre> + * mode (int) + * baseIntervalMillis (long) + * intervalMultiplier (int) + * </pre> + * + * Ex: "enabled=true,baseIntervalMillis=1000,intervalMultiplier=10" + * + * @see #APPOP_HISTORY_MODE + * @see #APPOP_HISTORY_BASE_INTERVAL_MILLIS + * @see #APPOP_HISTORY_INTERVAL_MULTIPLIER + * + * @hide + */ + public static final String APPOP_HISTORY_PARAMETERS = + "appop_history_parameters"; } /** diff --git a/core/java/android/service/appprediction/AppPredictionService.java b/core/java/android/service/appprediction/AppPredictionService.java new file mode 100644 index 000000000000..b77405affaec --- /dev/null +++ b/core/java/android/service/appprediction/AppPredictionService.java @@ -0,0 +1,322 @@ +/* + * 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. + */ +package android.service.appprediction; + +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.annotation.CallSuper; +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.app.Service; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.AppTargetId; +import android.app.prediction.IPredictionCallback; +import android.content.Intent; +import android.content.pm.ParceledListSlice; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.service.appprediction.IPredictionService.Stub; +import android.util.ArrayMap; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * TODO(b/111701043): Add java docs + * + * @hide + */ +@SystemApi +public abstract class AppPredictionService extends Service { + + private static final String TAG = "AppPredictionService"; + + /** + * The {@link Intent} that must be declared as handled by the service. + * TODO(b/111701043): Add any docs about permissions the service must hold + * + * @hide + */ + public static final String SERVICE_INTERFACE = + "android.service.appprediction.AppPredictionService"; + + private final ArrayMap<AppPredictionSessionId, ArrayList<CallbackWrapper>> mSessionCallbacks = + new ArrayMap<>(); + private Handler mHandler; + + private final IPredictionService mInterface = new Stub() { + + @Override + public void onCreatePredictionSession(AppPredictionContext context, + AppPredictionSessionId sessionId) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::doCreatePredictionSession, + AppPredictionService.this, context, sessionId)); + } + + @Override + public void notifyAppTargetEvent(AppPredictionSessionId sessionId, AppTargetEvent event) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::onAppTargetEvent, + AppPredictionService.this, sessionId, event)); + } + + @Override + public void notifyLocationShown(AppPredictionSessionId sessionId, String launchLocation, + ParceledListSlice targetIds) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::onLocationShown, AppPredictionService.this, + sessionId, launchLocation, targetIds.getList())); + } + + @Override + public void sortAppTargets(AppPredictionSessionId sessionId, ParceledListSlice targets, + IPredictionCallback callback) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::onSortAppTargets, + AppPredictionService.this, sessionId, targets.getList(), null, + new CallbackWrapper(callback))); + } + + @Override + public void registerPredictionUpdates(AppPredictionSessionId sessionId, + IPredictionCallback callback) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::doRegisterPredictionUpdates, + AppPredictionService.this, sessionId, callback)); + } + + @Override + public void unregisterPredictionUpdates(AppPredictionSessionId sessionId, + IPredictionCallback callback) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::doUnregisterPredictionUpdates, + AppPredictionService.this, sessionId, callback)); + } + + @Override + public void requestPredictionUpdate(AppPredictionSessionId sessionId) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::doRequestPredictionUpdate, + AppPredictionService.this, sessionId)); + } + + @Override + public void onDestroyPredictionSession(AppPredictionSessionId sessionId) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::doDestroyPredictionSession, + AppPredictionService.this, sessionId)); + } + }; + + @CallSuper + @Override + public void onCreate() { + super.onCreate(); + mHandler = new Handler(Looper.getMainLooper(), null, true); + } + + @Override + public final IBinder onBind(Intent intent) { + return mInterface.asBinder(); + } + + /** + * Called by a client app to indicate a target launch + */ + @MainThread + public abstract void onAppTargetEvent(@NonNull AppPredictionSessionId sessionId, + @NonNull AppTargetEvent event); + + /** + * Called by a client app to indication a particular location has been shown to the user. + */ + @MainThread + public abstract void onLocationShown(@NonNull AppPredictionSessionId sessionId, + @NonNull String launchLocation, @NonNull List<AppTargetId> targetIds); + + private void doCreatePredictionSession(@NonNull AppPredictionContext context, + @NonNull AppPredictionSessionId sessionId) { + mSessionCallbacks.put(sessionId, new ArrayList<>()); + onCreatePredictionSession(context, sessionId); + } + + /** + * Creates a new interaction session. + * + * @param context interaction context + * @param sessionId the session's Id + */ + public void onCreatePredictionSession(@NonNull AppPredictionContext context, + @NonNull AppPredictionSessionId sessionId) {} + + /** + * Called by the client app to request sorting of targets based on prediction rank. + * TODO(b/111701043): Implement CancellationSignal so caller can cancel a long running request + */ + @MainThread + public abstract void onSortAppTargets(@NonNull AppPredictionSessionId sessionId, + @NonNull List<AppTarget> targets, @NonNull CancellationSignal cancellationSignal, + @NonNull Consumer<List<AppTarget>> callback); + + private void doRegisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId); + if (callbacks == null) { + Slog.e(TAG, "Failed to register for updates for unknown session: " + sessionId); + return; + } + + final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback); + if (wrapper == null) { + callbacks.add(new CallbackWrapper(callback)); + if (callbacks.size() == 1) { + onStartPredictionUpdates(); + } + } + } + + /** + * Called when any continuous prediction callback is registered. + */ + @MainThread + public void onStartPredictionUpdates() {} + + private void doUnregisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId); + if (callbacks == null) { + Slog.e(TAG, "Failed to unregister for updates for unknown session: " + sessionId); + return; + } + + final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback); + if (wrapper != null) { + callbacks.remove(wrapper); + if (callbacks.isEmpty()) { + onStopPredictionUpdates(); + } + } + } + + /** + * Called when there are no longer any continuous prediction callbacks registered. + */ + @MainThread + public void onStopPredictionUpdates() {} + + private void doRequestPredictionUpdate(@NonNull AppPredictionSessionId sessionId) { + final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId); + if (callbacks != null && !callbacks.isEmpty()) { + onRequestPredictionUpdate(sessionId); + } + } + + /** + * Called by the client app to request target predictions. This method is only called if there + * are one or more prediction callbacks registered. + * TODO(b/111701043): Add java docs + * + * @see #updatePredictions(AppPredictionSessionId, List) + */ + @MainThread + public abstract void onRequestPredictionUpdate(@NonNull AppPredictionSessionId sessionId); + + private void doDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) { + mSessionCallbacks.remove(sessionId); + onDestroyPredictionSession(sessionId); + } + + /** + * Destroys the interaction session. + * + * @param sessionId the id of the session to destroy + */ + @MainThread + public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) {} + + /** + * Used by the prediction factory to send back results the client app. The can be called + * in response to {@link #onRequestPredictionUpdate(AppPredictionSessionId)} or proactively as + * a result of changes in predictions. + */ + public final void updatePredictions(@NonNull AppPredictionSessionId sessionId, + @NonNull List<AppTarget> targets) { + List<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId); + if (callbacks != null) { + for (CallbackWrapper callback : callbacks) { + callback.accept(targets); + } + } + } + + /** + * Finds the callback wrapper for the given callback. + */ + private CallbackWrapper findCallbackWrapper(ArrayList<CallbackWrapper> callbacks, + IPredictionCallback callback) { + for (int i = callbacks.size() - 1; i >= 0; i--) { + if (callbacks.get(i).isCallback(callback)) { + return callbacks.get(i); + } + } + return null; + } + + private static final class CallbackWrapper implements Consumer<List<AppTarget>>, + IBinder.DeathRecipient { + + private IPredictionCallback mCallback; + + CallbackWrapper(IPredictionCallback callback) { + mCallback = callback; + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to link to death: " + e); + } + } + + public boolean isCallback(@NonNull IPredictionCallback callback) { + return mCallback.equals(callback); + } + + @Override + public void accept(List<AppTarget> ts) { + try { + if (mCallback != null) { + mCallback.onResult(new ParceledListSlice(ts)); + } + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result:" + e); + } + } + + @Override + public void binderDied() { + mCallback = null; + } + } +} diff --git a/core/java/android/service/appprediction/IPredictionService.aidl b/core/java/android/service/appprediction/IPredictionService.aidl new file mode 100644 index 000000000000..3a6d1666f4b9 --- /dev/null +++ b/core/java/android/service/appprediction/IPredictionService.aidl @@ -0,0 +1,53 @@ +/* + * 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. + */ + +package android.service.appprediction; + +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.IPredictionCallback; +import android.content.pm.ParceledListSlice; + +/** + * Interface from the system to a prediction service. + * + * @hide + */ +oneway interface IPredictionService { + + void onCreatePredictionSession(in AppPredictionContext context, + in AppPredictionSessionId sessionId); + + void notifyAppTargetEvent(in AppPredictionSessionId sessionId, in AppTargetEvent event); + + void notifyLocationShown(in AppPredictionSessionId sessionId, in String launchLocation, + in ParceledListSlice targetIds); + + void sortAppTargets(in AppPredictionSessionId sessionId, in ParceledListSlice targets, + in IPredictionCallback callback); + + void registerPredictionUpdates(in AppPredictionSessionId sessionId, + in IPredictionCallback callback); + + void unregisterPredictionUpdates(in AppPredictionSessionId sessionId, + in IPredictionCallback callback); + + void requestPredictionUpdate(in AppPredictionSessionId sessionId); + + void onDestroyPredictionSession(in AppPredictionSessionId sessionId); +} diff --git a/core/java/android/service/contentsuggestions/ContentSuggestionsService.java b/core/java/android/service/contentsuggestions/ContentSuggestionsService.java new file mode 100644 index 000000000000..0da80397ec75 --- /dev/null +++ b/core/java/android/service/contentsuggestions/ContentSuggestionsService.java @@ -0,0 +1,154 @@ +/* + * 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. + */ + +package android.service.contentsuggestions; + +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.annotation.CallSuper; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.Service; +import android.app.contentsuggestions.ClassificationsRequest; +import android.app.contentsuggestions.ContentSuggestionsManager; +import android.app.contentsuggestions.IClassificationsCallback; +import android.app.contentsuggestions.ISelectionsCallback; +import android.app.contentsuggestions.SelectionsRequest; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.GraphicBuffer; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; + +/** + * @hide + */ +@SystemApi +public abstract class ContentSuggestionsService extends Service { + + private static final String TAG = ContentSuggestionsService.class.getSimpleName(); + + private Handler mHandler; + + /** + * The action for the intent used to define the content suggestions service. + */ + public static final String SERVICE_INTERFACE = + "android.service.contentsuggestions.ContentSuggestionsService"; + + private final IContentSuggestionsService mInterface = new IContentSuggestionsService.Stub() { + @Override + public void provideContextImage(int taskId, GraphicBuffer contextImage, + Bundle imageContextRequestExtras) { + mHandler.sendMessage( + obtainMessage(ContentSuggestionsService::processContextImage, + ContentSuggestionsService.this, taskId, + Bitmap.createHardwareBitmap(contextImage), + imageContextRequestExtras)); + } + + @Override + public void suggestContentSelections(SelectionsRequest request, + ISelectionsCallback callback) { + mHandler.sendMessage(obtainMessage(ContentSuggestionsService::suggestContentSelections, + ContentSuggestionsService.this, request, wrapSelectionsCallback(callback))); + + } + + @Override + public void classifyContentSelections(ClassificationsRequest request, + IClassificationsCallback callback) { + mHandler.sendMessage(obtainMessage(ContentSuggestionsService::classifyContentSelections, + ContentSuggestionsService.this, request, wrapClassificationCallback(callback))); + } + + @Override + public void notifyInteraction(String requestId, Bundle interaction) { + mHandler.sendMessage( + obtainMessage(ContentSuggestionsService::notifyInteraction, + ContentSuggestionsService.this, requestId, interaction)); + } + }; + + @CallSuper + @Override + public void onCreate() { + super.onCreate(); + mHandler = new Handler(Looper.getMainLooper(), null, true); + } + + /** @hide */ + @Override + public final IBinder onBind(Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction())) { + return mInterface.asBinder(); + } + Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent); + return null; + } + + /** + * Called by the system to provide the snapshot for the task associated with the given + * {@param taskId}. + */ + public abstract void processContextImage( + int taskId, @Nullable Bitmap contextImage, @NonNull Bundle extras); + + /** + * Called by a client app to make a request for content selections. + */ + public abstract void suggestContentSelections(@NonNull SelectionsRequest request, + @NonNull ContentSuggestionsManager.SelectionsCallback callback); + + /** + * Called by a client app to classify the provided content selections. + */ + public abstract void classifyContentSelections(@NonNull ClassificationsRequest request, + @NonNull ContentSuggestionsManager.ClassificationsCallback callback); + + /** + * Called by a client app to report an interaction. + */ + public abstract void notifyInteraction(@NonNull String requestId, @NonNull Bundle interaction); + + private ContentSuggestionsManager.SelectionsCallback wrapSelectionsCallback( + ISelectionsCallback callback) { + return (statusCode, selections) -> { + try { + callback.onContentSelectionsAvailable(statusCode, selections); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result: " + e); + } + }; + } + + private ContentSuggestionsManager.ClassificationsCallback wrapClassificationCallback( + IClassificationsCallback callback) { + return ((statusCode, classifications) -> { + try { + callback.onContentClassificationsAvailable(statusCode, classifications); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result: " + e); + } + }); + } +} diff --git a/core/java/android/service/contentsuggestions/IContentSuggestionsService.aidl b/core/java/android/service/contentsuggestions/IContentSuggestionsService.aidl new file mode 100644 index 000000000000..1926478322c8 --- /dev/null +++ b/core/java/android/service/contentsuggestions/IContentSuggestionsService.aidl @@ -0,0 +1,43 @@ +/* + * 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. + */ + +package android.service.contentsuggestions; + +import android.app.contentsuggestions.IClassificationsCallback; +import android.app.contentsuggestions.ISelectionsCallback; +import android.app.contentsuggestions.ClassificationsRequest; +import android.app.contentsuggestions.SelectionsRequest; +import android.graphics.GraphicBuffer; +import android.os.Bundle; + +/** + * Interface from the system to an implementation of a content suggestions service. + * + * @hide + */ +oneway interface IContentSuggestionsService { + void provideContextImage( + int taskId, + in GraphicBuffer contextImage, + in Bundle imageContextRequestExtras); + void suggestContentSelections( + in SelectionsRequest request, + in ISelectionsCallback callback); + void classifyContentSelections( + in ClassificationsRequest request, + in IClassificationsCallback callback); + void notifyInteraction(in String requestId, in Bundle interaction); +} diff --git a/core/java/android/util/DebugUtils.java b/core/java/android/util/DebugUtils.java index af73a16e012a..265b0d303f7f 100644 --- a/core/java/android/util/DebugUtils.java +++ b/core/java/android/util/DebugUtils.java @@ -18,6 +18,7 @@ package android.util; import android.annotation.UnsupportedAppUsage; import android.os.Build; + import java.io.PrintWriter; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -250,7 +251,7 @@ public class DebugUtils { if (value == 0 && flagsWasZero) { return constNameWithoutPrefix(prefix, field); } - if ((flags & value) != 0) { + if ((flags & value) == value) { flags &= ~value; res.append(constNameWithoutPrefix(prefix, field)).append('|'); } diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index e59bee42c21c..c4ab91f8b429 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -17,8 +17,10 @@ package com.android.internal.app; import android.app.AppOpsManager; +import android.app.AppOpsManager; import android.content.pm.ParceledListSlice; import android.os.Bundle; +import android.os.RemoteCallback; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsActiveCallback; import com.android.internal.app.IAppOpsNotedCallback; @@ -42,10 +44,15 @@ interface IAppOpsService { int checkPackage(int uid, String packageName); List<AppOpsManager.PackageOps> getPackagesForOps(in int[] ops); List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName, in int[] ops); - ParceledListSlice getAllHistoricalPackagesOps(in String[] ops, - long beginTimeMillis, long endTimeMillis); - AppOpsManager.HistoricalPackageOps getHistoricalPackagesOps(int uid, String packageName, - in String[] ops, long beginTimeMillis, long endTimeMillis); + void getHistoricalOps(int uid, String packageName, in String[] ops, long beginTimeMillis, + long endTimeMillis, in RemoteCallback callback); + void getHistoricalOpsFromDiskRaw(int uid, String packageName, in String[] ops, + long beginTimeMillis, long endTimeMillis, in RemoteCallback callback); + void offsetHistory(long duration); + void setHistoryParameters(int mode, long baseSnapshotInterval, int compressionStep); + void addHistoricalOps(in AppOpsManager.HistoricalOps ops); + void resetHistoryParameters(); + void clearHistory(); List<AppOpsManager.PackageOps> getUidOps(int uid, in int[] ops); void setUidMode(int code, int uid, int mode); void setMode(int code, int uid, String packageName, int mode); diff --git a/core/java/com/android/internal/os/AtomicDirectory.java b/core/java/com/android/internal/os/AtomicDirectory.java new file mode 100644 index 000000000000..f24d12e0c3af --- /dev/null +++ b/core/java/com/android/internal/os/AtomicDirectory.java @@ -0,0 +1,294 @@ +/* + * 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. + */ + +package com.android.internal.os; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.FileUtils; +import android.util.ArrayMap; + +import com.android.internal.util.Preconditions; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; + +/** + * Helper class for performing atomic operations on a directory, by creating a + * backup directory until a write has successfully completed. + * <p> + * Atomic directory guarantees directory integrity by ensuring that a directory has + * been completely written and sync'd to disk before removing its backup. + * As long as the backup directory exists, the original directory is considered + * to be invalid (leftover from a previous attempt to write). + * <p> + * Atomic directory does not confer any file locking semantics. Do not use this + * class when the directory may be accessed or modified concurrently + * by multiple threads or processes. The caller is responsible for ensuring + * appropriate mutual exclusion invariants whenever it accesses the directory. + * <p> + * To ensure atomicity you must always use this class to interact with the + * backing directory when checking existence, making changes, and deleting. + */ +public final class AtomicDirectory { + private final @NonNull ArrayMap<File, FileOutputStream> mOpenFiles = new ArrayMap<>(); + private final @NonNull File mBaseDirectory; + private final @NonNull File mBackupDirectory; + + private int mBaseDirectoryFd = -1; + private int mBackupDirectoryFd = -1; + + /** + * Creates a new instance. + * + * @param baseDirectory The base directory to treat atomically. + */ + public AtomicDirectory(@NonNull File baseDirectory) { + Preconditions.checkNotNull(baseDirectory, "baseDirectory cannot be null"); + mBaseDirectory = baseDirectory; + mBackupDirectory = new File(baseDirectory.getPath() + "_bak"); + } + + /** + * Gets the backup directory if present. This could be useful if you are + * writing new state to the dir but need to access the last persisted state + * at the same time. This means that this call is useful in between + * {@link #startWrite()} and {@link #finishWrite()} or {@link #failWrite()}. + * You should not modify the content returned by this method. + * + * @see #startRead() + */ + public @Nullable File getBackupDirectory() { + return mBackupDirectory; + } + + /** + * Starts reading this directory. After calling this method you should + * not make any changes to its contents. + * + * @throws IOException If an error occurs. + * + * @see #finishRead() + * @see #startWrite() + */ + public @NonNull File startRead() throws IOException { + restore(); + return getOrCreateBaseDirectory(); + } + + /** + * Finishes reading this directory. + * + * @see #startRead() + * @see #startWrite() + */ + public void finishRead() { + mBaseDirectoryFd = -1; + mBackupDirectoryFd = -1; + } + + /** + * Starts editing this directory. After calling this method you should + * add content to the directory only via the APIs on this class. To open a + * file for writing in this directory you should use {@link #openWrite(File)} + * and to close the file {@link #closeWrite(FileOutputStream)}. Once all + * content has been written and all files closed you should commit via a + * call to {@link #finishWrite()} or discard via a call to {@link #failWrite()}. + * + * @throws IOException If an error occurs. + * + * @see #startRead() + * @see #openWrite(File) + * @see #finishWrite() + * @see #failWrite() + */ + public @NonNull File startWrite() throws IOException { + backup(); + return getOrCreateBaseDirectory(); + } + + /** + * Opens a file in this directory for writing. + * + * @param file The file to open. Must be a file in the base directory. + * @return An input stream for reading. + * + * @throws IOException If an I/O error occurs. + * + * @see #closeWrite(FileOutputStream) + */ + public @NonNull FileOutputStream openWrite(@NonNull File file) throws IOException { + if (file.isDirectory() || !file.getParentFile().equals(getOrCreateBaseDirectory())) { + throw new IllegalArgumentException("Must be a file in " + getOrCreateBaseDirectory()); + } + final FileOutputStream destination = new FileOutputStream(file); + if (mOpenFiles.put(file, destination) != null) { + throw new IllegalArgumentException("Already open file" + file.getCanonicalPath()); + } + return destination; + } + + /** + * Closes a previously opened file. + * + * @param destination The stream to the file returned by {@link #openWrite(File)}. + * + * @see #openWrite(File) + */ + public void closeWrite(@NonNull FileOutputStream destination) { + final int indexOfValue = mOpenFiles.indexOfValue(destination); + if (mOpenFiles.removeAt(indexOfValue) == null) { + throw new IllegalArgumentException("Unknown file stream " + destination); + } + FileUtils.sync(destination); + try { + destination.close(); + } catch (IOException ignored) {} + } + + public void failWrite(@NonNull FileOutputStream destination) { + final int indexOfValue = mOpenFiles.indexOfValue(destination); + if (indexOfValue >= 0) { + mOpenFiles.removeAt(indexOfValue); + } + } + + /** + * Finishes the edit and commits all changes. + * + * @see #startWrite() + * + * @throws IllegalStateException is some files are not closed. + */ + public void finishWrite() { + throwIfSomeFilesOpen(); + fsyncDirectoryFd(mBaseDirectoryFd); + deleteDirectory(mBackupDirectory); + fsyncDirectoryFd(mBackupDirectoryFd); + mBaseDirectoryFd = -1; + mBackupDirectoryFd = -1; + } + + /** + * Finishes the edit and discards all changes. + * + * @see #startWrite() + */ + public void failWrite() { + throwIfSomeFilesOpen(); + try{ + restore(); + } catch (IOException ignored) {} + mBaseDirectoryFd = -1; + mBackupDirectoryFd = -1; + } + + /** + * @return Whether this directory exists. + */ + public boolean exists() { + return mBaseDirectory.exists() || mBackupDirectory.exists(); + } + + /** + * Deletes this directory. + */ + public void delete() { + if (mBaseDirectory.exists()) { + deleteDirectory(mBaseDirectory); + fsyncDirectoryFd(mBaseDirectoryFd); + } + if (mBackupDirectory.exists()) { + deleteDirectory(mBackupDirectory); + fsyncDirectoryFd(mBackupDirectoryFd); + } + } + + private @NonNull File getOrCreateBaseDirectory() throws IOException { + if (!mBaseDirectory.exists()) { + if (!mBaseDirectory.mkdirs()) { + throw new IOException("Couldn't create directory " + mBaseDirectory); + } + FileUtils.setPermissions(mBaseDirectory.getPath(), + FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IXOTH, + -1, -1); + } + if (mBaseDirectoryFd < 0) { + mBaseDirectoryFd = getDirectoryFd(mBaseDirectory.getCanonicalPath()); + } + return mBaseDirectory; + } + + private void throwIfSomeFilesOpen() { + if (!mOpenFiles.isEmpty()) { + throw new IllegalStateException("Unclosed files: " + + Arrays.toString(mOpenFiles.keySet().toArray())); + } + } + + private void backup() throws IOException { + if (!mBaseDirectory.exists()) { + return; + } + if (mBaseDirectoryFd < 0) { + mBaseDirectoryFd = getDirectoryFd(mBaseDirectory.getCanonicalPath()); + } + if (mBackupDirectory.exists()) { + deleteDirectory(mBackupDirectory); + } + if (!mBaseDirectory.renameTo(mBackupDirectory)) { + throw new IOException("Couldn't backup " + mBaseDirectory + + " to " + mBackupDirectory); + } + mBackupDirectoryFd = mBaseDirectoryFd; + mBaseDirectoryFd = -1; + fsyncDirectoryFd(mBackupDirectoryFd); + } + + private void restore() throws IOException { + if (!mBackupDirectory.exists()) { + return; + } + if (mBackupDirectoryFd == -1) { + mBackupDirectoryFd = getDirectoryFd(mBackupDirectory.getCanonicalPath()); + } + if (mBaseDirectory.exists()) { + deleteDirectory(mBaseDirectory); + } + if (!mBackupDirectory.renameTo(mBaseDirectory)) { + throw new IOException("Couldn't restore " + mBackupDirectory + + " to " + mBaseDirectory); + } + mBaseDirectoryFd = mBackupDirectoryFd; + mBackupDirectoryFd = -1; + fsyncDirectoryFd(mBaseDirectoryFd); + } + + private static void deleteDirectory(@NonNull File file) { + final File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteDirectory(child); + } + } + file.delete(); + } + + private static native int getDirectoryFd(String path); + private static native void fsyncDirectoryFd(int fd); +} diff --git a/core/java/com/android/internal/util/CollectionUtils.java b/core/java/com/android/internal/util/CollectionUtils.java index 470533f2d002..605df040da59 100644 --- a/core/java/com/android/internal/util/CollectionUtils.java +++ b/core/java/com/android/internal/util/CollectionUtils.java @@ -330,4 +330,18 @@ public class CollectionUtils { public static @NonNull <T> List<T> defeatNullable(@Nullable List<T> val) { return (val != null) ? val : Collections.emptyList(); } + + /** + * @return the first element if not empty/null, null otherwise + */ + public static @Nullable <T> T firstOrNull(@Nullable List<T> cur) { + return isEmpty(cur) ? null : cur.get(0); + } + + /** + * @return list of single given element if it's not null, empty list otherwise + */ + public static @NonNull <T> List<T> singletonOrEmpty(@Nullable T item) { + return item == null ? Collections.emptyList() : Collections.singletonList(item); + } } diff --git a/core/jni/Android.bp b/core/jni/Android.bp index dc6a73a7d75c..088e13ff92bc 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -41,7 +41,7 @@ cc_library_shared { "com_google_android_gles_jni_EGLImpl.cpp", "com_google_android_gles_jni_GLImpl.cpp", // TODO: .arm "android_app_Activity.cpp", - "android_app_ActivityThread.cpp", + "android_app_ActivityThread.cpp", "android_app_NativeActivity.cpp", "android_app_admin_SecurityLog.cpp", "android_opengl_EGL14.cpp", @@ -202,6 +202,7 @@ cc_library_shared { "android_animation_PropertyValuesHolder.cpp", "android_security_Scrypt.cpp", "com_android_internal_net_NetworkStatsFactory.cpp", + "com_android_internal_os_AtomicDirectory.cpp", "com_android_internal_os_ClassLoaderFactory.cpp", "com_android_internal_os_FuseAppLoop.cpp", "com_android_internal_os_Zygote.cpp", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 687b1055c3d6..109222298fe5 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -218,6 +218,7 @@ extern int register_android_animation_PropertyValuesHolder(JNIEnv *env); extern int register_android_security_Scrypt(JNIEnv *env); extern int register_com_android_internal_content_NativeLibraryHelper(JNIEnv *env); extern int register_com_android_internal_net_NetworkStatsFactory(JNIEnv *env); +extern int register_com_android_internal_os_AtomicDirectory(JNIEnv *env); extern int register_com_android_internal_os_ClassLoaderFactory(JNIEnv* env); extern int register_com_android_internal_os_FuseAppLoop(JNIEnv* env); extern int register_com_android_internal_os_Zygote(JNIEnv *env); @@ -1495,6 +1496,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_security_Scrypt), REG_JNI(register_com_android_internal_content_NativeLibraryHelper), REG_JNI(register_com_android_internal_net_NetworkStatsFactory), + REG_JNI(register_com_android_internal_os_AtomicDirectory), REG_JNI(register_com_android_internal_os_FuseAppLoop), }; diff --git a/core/jni/com_android_internal_os_AtomicDirectory.cpp b/core/jni/com_android_internal_os_AtomicDirectory.cpp new file mode 100644 index 000000000000..50b2288a614a --- /dev/null +++ b/core/jni/com_android_internal_os_AtomicDirectory.cpp @@ -0,0 +1,66 @@ +/* + * 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. + */ + +#include <nativehelper/ScopedUtfChars.h> +#include "jni.h" + +#include "core_jni_helpers.h" + +namespace android { + +static jint com_android_internal_os_AtomicDirectory_getDirectoryFd(JNIEnv* env, + jobject /*clazz*/, jstring path) { + ScopedUtfChars path8(env, path); + if (path8.c_str() == NULL) { + ALOGE("Invalid path: %s", path8.c_str()); + return -1; + } + int fd; + if ((fd = TEMP_FAILURE_RETRY(open(path8.c_str(), O_DIRECTORY | O_RDONLY))) == -1) { + ALOGE("Cannot open directory %s, error: %s\n", path8.c_str(), strerror(errno)); + return -1; + } + return fd; +} + +static void com_android_internal_os_AtomicDirectory_fsyncDirectoryFd(JNIEnv* env, + jobject /*clazz*/, jint fd) { + if (TEMP_FAILURE_RETRY(fsync(fd)) == -1) { + ALOGE("Cannot fsync directory %d, error: %s\n", fd, strerror(errno)); + } +} + +/* + * JNI registration. + */ +static const JNINativeMethod gRegisterMethods[] = { + /* name, signature, funcPtr */ + { "fsyncDirectoryFd", + "(I)V", + (void*) com_android_internal_os_AtomicDirectory_fsyncDirectoryFd + }, + { "getDirectoryFd", + "(Ljava/lang/String;)I", + (void*) com_android_internal_os_AtomicDirectory_getDirectoryFd + }, +}; + +int register_com_android_internal_os_AtomicDirectory(JNIEnv* env) { + return RegisterMethodsOrDie(env, "com/android/internal/os/AtomicDirectory", + gRegisterMethods, NELEM(gRegisterMethods)); +} + +}; // namespace android diff --git a/core/proto/android/providers/settings/global.proto b/core/proto/android/providers/settings/global.proto index a00fde9ed0e3..cc5aa20258ce 100644 --- a/core/proto/android/providers/settings/global.proto +++ b/core/proto/android/providers/settings/global.proto @@ -1018,7 +1018,9 @@ message GlobalSettingsProto { optional SettingProto zram_enabled = 139 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto app_ops_constants = 148 [ (android.privacy).dest = DEST_AUTOMATIC ]; + // Please insert fields in alphabetical order and group them into messages // if possible (to avoid reaching the method limit). - // Next tag = 148; + // Next tag = 149; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 449a7b3305ad..8b85a28c4a40 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4254,6 +4254,16 @@ <permission android:name="android.permission.MANAGE_CONTENT_CAPTURE" android:protectionLevel="signature" /> + <!-- @SystemApi Allows an application to manage the content suggestions service. + @hide <p>Not for use by third-party applications.</p> --> + <permission android:name="android.permission.MANAGE_CONTENT_SUGGESTIONS" + android:protectionLevel="signature" /> + + <!-- @SystemApi Allows an application to manage the app predictions service. + @hide <p>Not for use by third-party applications.</p> --> + <permission android:name="android.permission.MANAGE_APP_PREDICTIONS" + android:protectionLevel="signature" /> + <!-- Allows an app to set the theme overlay in /vendor/overlay being used. @hide <p>Not for use by third-party applications.</p> --> @@ -4334,6 +4344,11 @@ <permission android:name="android.permission.BIND_SMS_APP_SERVICE" android:protectionLevel="signature" /> + <!-- @hide Permission that allows configuring appops. + <p>Not for use by third-party applications. --> + <permission android:name="android.permission.MANAGE_APPOPS" + android:protectionLevel="signature" /> + <!-- @hide Permission that allows background clipboard access. <p>Not for use by third-party applications. --> <permission android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND" diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index a7e8deef346a..1feb59a52ad1 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3423,6 +3423,23 @@ --> <string name="config_defaultAugmentedAutofillService" translatable="false"></string> + <!-- The package name for the system's app prediction service. + This service must be trusted, as it can be activated without explicit consent of the user. + Example: "com.android.intelligence/.AppPredictionService" + --> + <string name="config_defaultAppPredictionService" translatable="false"></string> + + <!-- The package name for the system's content suggestions service. + Provides suggestions for text and image selection regions in snapshots of apps and should + be able to classify the type of entities in those selections. + + This service must be trusted, as it can be activated without explicit consent of the user. + If no service with the specified name exists on the device, content suggestions wil be + disabled. + Example: "com.android.contentsuggestions/.ContentSuggestionsService" + --> + <string name="config_defaultContentSuggestionsService" translatable="false"></string> + <!-- Whether the device uses the default focus highlight when focus state isn't specified. --> <bool name="config_useDefaultFocusHighlight">true</bool> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index f707cedfd88f..010accf19d18 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3275,6 +3275,8 @@ <java-symbol type="string" name="config_defaultWellbeingPackage" /> <java-symbol type="string" name="config_defaultContentCaptureService" /> <java-symbol type="string" name="config_defaultAugmentedAutofillService" /> + <java-symbol type="string" name="config_defaultAppPredictionService" /> + <java-symbol type="string" name="config_defaultContentSuggestionsService" /> <java-symbol type="string" name="notification_channel_foreground_service" /> <java-symbol type="string" name="foreground_service_app_in_background" /> diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index 1750dac60e33..0f83a29d8d36 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -19,28 +19,35 @@ package android.app.activity; import static android.content.Intent.ACTION_EDIT; import static android.content.Intent.ACTION_VIEW; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.app.Activity; import android.app.ActivityThread; import android.app.IApplicationThread; +import android.app.servertransaction.ActivityConfigurationChangeItem; import android.app.servertransaction.ActivityRelaunchItem; import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.ClientTransactionItem; import android.app.servertransaction.ResumeActivityItem; import android.app.servertransaction.StopActivityItem; import android.content.Intent; +import android.content.res.Configuration; import android.os.IBinder; import android.support.test.InstrumentationRegistry; import android.support.test.filters.MediumTest; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import android.util.MergedConfiguration; +import android.view.Display; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.CountDownLatch; + /** * Test for verifying {@link android.app.ActivityThread} class. * Build/Install/Run: @@ -50,8 +57,12 @@ import org.junit.runner.RunWith; @MediumTest public class ActivityThreadTest { - private final ActivityTestRule mActivityTestRule = - new ActivityTestRule(TestActivity.class, true /* initialTouchMode */, + // The first sequence number to try with. Use a large number to avoid conflicts with the first a + // few sequence numbers the framework used to launch the test activity. + private static final int BASE_SEQ = 10000; + + private final ActivityTestRule<TestActivity> mActivityTestRule = + new ActivityTestRule<>(TestActivity.class, true /* initialTouchMode */, false /* launchActivity */); @Test @@ -129,6 +140,179 @@ public class ActivityThreadTest { }); } + @Test + public void testHandleActivityConfigurationChanged() { + final TestActivity activity = mActivityTestRule.launchActivity(new Intent()); + + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + final int numOfConfig = activity.mNumOfConfigChanges; + applyConfigurationChange(activity, BASE_SEQ); + assertEquals(numOfConfig + 1, activity.mNumOfConfigChanges); + }); + } + + @Test + public void testHandleActivityConfigurationChanged_DropStaleConfigurations() { + final TestActivity activity = mActivityTestRule.launchActivity(new Intent()); + + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + // Set the sequence number to BASE_SEQ. + applyConfigurationChange(activity, BASE_SEQ); + + final int orientation = activity.mConfig.orientation; + final int numOfConfig = activity.mNumOfConfigChanges; + + // Try to apply an old configuration change. + applyConfigurationChange(activity, BASE_SEQ - 1); + assertEquals(numOfConfig, activity.mNumOfConfigChanges); + assertEquals(orientation, activity.mConfig.orientation); + }); + } + + @Test + public void testHandleActivityConfigurationChanged_ApplyNewConfigurations() { + final TestActivity activity = mActivityTestRule.launchActivity(new Intent()); + + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + // Set the sequence number to BASE_SEQ and record the final sequence number it used. + final int seq = applyConfigurationChange(activity, BASE_SEQ); + + final int orientation = activity.mConfig.orientation; + final int numOfConfig = activity.mNumOfConfigChanges; + + // Try to apply an new configuration change. + applyConfigurationChange(activity, seq + 1); + assertEquals(numOfConfig + 1, activity.mNumOfConfigChanges); + assertNotEquals(orientation, activity.mConfig.orientation); + }); + } + + @Test + public void testHandleActivityConfigurationChanged_PickNewerPendingConfiguration() { + final TestActivity activity = mActivityTestRule.launchActivity(new Intent()); + + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + // Set the sequence number to BASE_SEQ and record the final sequence number it used. + final int seq = applyConfigurationChange(activity, BASE_SEQ); + + final int orientation = activity.mConfig.orientation; + final int numOfConfig = activity.mNumOfConfigChanges; + + final ActivityThread activityThread = activity.getActivityThread(); + + final Configuration pendingConfig = new Configuration(); + pendingConfig.orientation = orientation == Configuration.ORIENTATION_LANDSCAPE + ? Configuration.ORIENTATION_PORTRAIT + : Configuration.ORIENTATION_LANDSCAPE; + pendingConfig.seq = seq + 2; + activityThread.updatePendingActivityConfiguration(activity.getActivityToken(), + pendingConfig); + + final Configuration newConfig = new Configuration(); + newConfig.orientation = orientation; + newConfig.seq = seq + 1; + + activityThread.handleActivityConfigurationChanged(activity.getActivityToken(), + newConfig, Display.INVALID_DISPLAY); + assertEquals(numOfConfig + 1, activity.mNumOfConfigChanges); + assertEquals(pendingConfig.orientation, activity.mConfig.orientation); + }); + } + + @Test + public void testHandleActivityConfigurationChanged_OnlyAppliesNewestConfiguration() + throws Exception { + final TestActivity activity = mActivityTestRule.launchActivity(new Intent()); + + final ActivityThread activityThread = activity.getActivityThread(); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + final Configuration config = new Configuration(); + config.seq = BASE_SEQ; + config.orientation = Configuration.ORIENTATION_PORTRAIT; + + activityThread.handleActivityConfigurationChanged(activity.getActivityToken(), + config, Display.INVALID_DISPLAY); + }); + + final int numOfConfig = activity.mNumOfConfigChanges; + final IApplicationThread appThread = activityThread.getApplicationThread(); + + activity.mConfigLatch = new CountDownLatch(1); + activity.mTestLatch = new CountDownLatch(1); + + Configuration config = new Configuration(); + config.seq = BASE_SEQ + 1; + config.smallestScreenWidthDp = 100; + appThread.scheduleTransaction(newActivityConfigTransaction(activity, config)); + + // Wait until the main thread is performing the configuration change for the configuration + // with sequence number BASE_SEQ + 1 before proceeding. This is to mimic the situation where + // the activity takes very long time to process configuration changes. + activity.mTestLatch.await(); + + config = new Configuration(); + config.seq = BASE_SEQ + 2; + config.smallestScreenWidthDp = 200; + appThread.scheduleTransaction(newActivityConfigTransaction(activity, config)); + + config = new Configuration(); + config.seq = BASE_SEQ + 3; + config.smallestScreenWidthDp = 300; + appThread.scheduleTransaction(newActivityConfigTransaction(activity, config)); + + config = new Configuration(); + config.seq = BASE_SEQ + 4; + config.smallestScreenWidthDp = 400; + appThread.scheduleTransaction(newActivityConfigTransaction(activity, config)); + + activity.mConfigLatch.countDown(); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + activity.mConfigLatch = null; + activity.mTestLatch = null; + + // Only two more configuration changes: one with seq BASE_SEQ + 1; another with seq + // BASE_SEQ + 4. Configurations scheduled in between should be dropped. + assertEquals(numOfConfig + 2, activity.mNumOfConfigChanges); + assertEquals(400, activity.mConfig.smallestScreenWidthDp); + } + + /** + * Calls {@link ActivityThread#handleActivityConfigurationChanged(IBinder, Configuration, int)} + * to try to push activity configuration to the activity for the given sequence number. + * <p> + * It uses orientation to push the configuration and it tries a different orientation if the + * first attempt doesn't make through, to rule out the possibility that the previous + * configuration already has the same orientation. + * + * @param activity the test target activity + * @param seq the specified sequence number + * @return the sequence number this method tried with the last time, so that the caller can use + * the next sequence number for next configuration update. + */ + private int applyConfigurationChange(TestActivity activity, int seq) { + final ActivityThread activityThread = activity.getActivityThread(); + + final int numOfConfig = activity.mNumOfConfigChanges; + Configuration config = new Configuration(); + config.orientation = Configuration.ORIENTATION_PORTRAIT; + config.seq = seq; + activityThread.handleActivityConfigurationChanged(activity.getActivityToken(), config, + Display.INVALID_DISPLAY); + + if (activity.mNumOfConfigChanges > numOfConfig) { + return config.seq; + } + + config = new Configuration(); + config.orientation = Configuration.ORIENTATION_LANDSCAPE; + config.seq = seq + 1; + activityThread.handleActivityConfigurationChanged(activity.getActivityToken(), config, + Display.INVALID_DISPLAY); + + return config.seq; + } + private static ClientTransaction newRelaunchResumeTransaction(Activity activity) { final ClientTransactionItem callbackItem = ActivityRelaunchItem.obtain(null, null, 0, new MergedConfiguration(), false /* preserveWindow */); @@ -162,6 +346,16 @@ public class ActivityThreadTest { return transaction; } + private static ClientTransaction newActivityConfigTransaction(Activity activity, + Configuration config) { + final ActivityConfigurationChangeItem item = ActivityConfigurationChangeItem.obtain(config); + + final ClientTransaction transaction = newTransaction(activity); + transaction.addCallback(item); + + return transaction; + } + private static ClientTransaction newTransaction(Activity activity) { final IApplicationThread appThread = activity.getActivityThread().getApplicationThread(); return ClientTransaction.obtain(appThread, activity.getActivityToken()); @@ -169,5 +363,37 @@ public class ActivityThreadTest { // Test activity public static class TestActivity extends Activity { + int mNumOfConfigChanges; + final Configuration mConfig = new Configuration(); + + /** + * A latch used to notify tests that we're about to wait for configuration latch. This + * is used to notify test code that preExecute phase for activity configuration change + * transaction has passed. + */ + volatile CountDownLatch mTestLatch; + /** + * If not {@code null} {@link #onConfigurationChanged(Configuration)} won't return until the + * latch reaches 0. + */ + volatile CountDownLatch mConfigLatch; + + @Override + public void onConfigurationChanged(Configuration config) { + super.onConfigurationChanged(config); + mConfig.setTo(config); + ++mNumOfConfigChanges; + + if (mConfigLatch != null) { + if (mTestLatch != null) { + mTestLatch.countDown(); + } + try { + mConfigLatch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + } } } diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java index f8633565cc86..4e405cab19b8 100644 --- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java +++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java @@ -27,6 +27,7 @@ import static java.lang.reflect.Modifier.isPublic; import static java.lang.reflect.Modifier.isStatic; import android.platform.test.annotations.Presubmit; +import android.provider.Settings.Global; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -550,7 +551,12 @@ public class SettingsBackupTest { Settings.Global.BACKUP_AGENT_TIMEOUT_PARAMETERS, Settings.Global.BACKUP_MULTI_USER_ENABLED, Settings.Global.ISOLATED_STORAGE_LOCAL, - Settings.Global.ISOLATED_STORAGE_REMOTE); + Settings.Global.ISOLATED_STORAGE_REMOTE, + Settings.Global.APPOP_HISTORY_PARAMETERS, + Settings.Global.APPOP_HISTORY_MODE, + Settings.Global.APPOP_HISTORY_INTERVAL_MULTIPLIER, + Settings.Global.APPOP_HISTORY_BASE_INTERVAL_MILLIS); + private static final Set<String> BACKUP_BLACKLISTED_SECURE_SETTINGS = newHashSet( Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index b9b1cdc6cbc9..b4f19c99c6fe 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -39,7 +39,7 @@ protected: virtual ~WeakLooperCallback() { } public: - WeakLooperCallback(const wp<LooperCallback>& callback) : + explicit WeakLooperCallback(const wp<LooperCallback>& callback) : mCallback(callback) { } diff --git a/libs/protoutil/include/android/util/EncodedBuffer.h b/libs/protoutil/include/android/util/EncodedBuffer.h index c84de4c1c083..0b7f6e46be65 100644 --- a/libs/protoutil/include/android/util/EncodedBuffer.h +++ b/libs/protoutil/include/android/util/EncodedBuffer.h @@ -38,13 +38,13 @@ class EncodedBuffer { public: EncodedBuffer(); - EncodedBuffer(size_t chunkSize); + explicit EncodedBuffer(size_t chunkSize); ~EncodedBuffer(); class Pointer { public: Pointer(); - Pointer(size_t chunkSize); + explicit Pointer(size_t chunkSize); size_t pos() const; size_t index() const; @@ -161,7 +161,7 @@ public: friend class iterator; class iterator { public: - iterator(const EncodedBuffer& buffer); + explicit iterator(const EncodedBuffer& buffer); /** * Returns the number of bytes written in the buffer diff --git a/packages/AppPredictionLib/Android.bp b/packages/AppPredictionLib/Android.bp new file mode 100644 index 000000000000..e0f4dedc9368 --- /dev/null +++ b/packages/AppPredictionLib/Android.bp @@ -0,0 +1,24 @@ +// 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. + +android_library { + name: "app_prediction", + + sdk_version: "system_current", + min_sdk_version: "system_current", + + srcs: [ + "src/**/*.java", + ], +} diff --git a/packages/AppPredictionLib/AndroidManifest.xml b/packages/AppPredictionLib/AndroidManifest.xml new file mode 100644 index 000000000000..b99278881281 --- /dev/null +++ b/packages/AppPredictionLib/AndroidManifest.xml @@ -0,0 +1,20 @@ +<?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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.app.prediction"> +</manifest> diff --git a/packages/AppPredictionLib/src/com/android/app/prediction/Constants.java b/packages/AppPredictionLib/src/com/android/app/prediction/Constants.java new file mode 100644 index 000000000000..0993c9a13f99 --- /dev/null +++ b/packages/AppPredictionLib/src/com/android/app/prediction/Constants.java @@ -0,0 +1,71 @@ +/* + * 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. + */ + +package com.android.app.prediction; + +/** + * Constants to be used with {@link android.app.prediction.AppPredictor}. + */ +public class Constants { + + /** + * UI surface for predictions displayed on the user's home screen + */ + public static final String UI_SURFACE_HOME = "home"; + + /** + * UI surface for predictions displayed on the recents/task switcher view + */ + public static final String UI_SURFACE_RECENTS = "recents"; + + /** + * UI surface for predictions displayed on the share sheet. + */ + public static final String UI_SURFACE_SHARE = "share"; + + /** + * Location constant when an app target or shortcut is started from the apps list + */ + public static final String LAUNCH_LOCATION_APPS_LIST = "apps_list"; + + /** + * Location constant when an app target or shortcut is started from the user's home screen + */ + public static final String LAUNCH_LOCATION_APPS_HOME = "home"; + + /** + * Location constant when an app target or shortcut is started from task switcher + */ + public static final String LAUNCH_LOCATION_APPS_RECENTS = "recents"; + + /** + * Location constant when an app target or shortcut is started in the share sheet while it is + * in collapsed state (showing a limited set of result). + */ + public static final String LAUNCH_LOCATION_APPS_SHARE_COLLAPSED = "share_collapsed"; + + /** + * Location constant when an app target or shortcut is started in the share sheet while it is + * in expended state and showing all the results. + */ + public static final String LAUNCH_LOCATION_APPS_SHARE_EXPANDED = "shared_expanded"; + + /** + * Location constant when an app target or shortcut is started in the share sheet when the + * target is displayed as a placeholder for an deprecated object. + */ + public static final String LAUNCH_LOCATION_APPS_SHARE_LEGACY = "share_legacy"; +} diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 419273ee28bb..bcf37ffef262 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1557,6 +1557,10 @@ class SettingsProtoDumpUtil { Settings.Global.ZRAM_ENABLED, GlobalSettingsProto.ZRAM_ENABLED); + dumpSetting(s, p, + Global.APP_OPS_CONSTANTS, + GlobalSettingsProto.APP_OPS_CONSTANTS); + p.end(token); // Please insert new settings using the same order as in GlobalSettingsProto. diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index bff2c844bb48..fa95bf2ee302 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -134,6 +134,8 @@ <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" /> <uses-permission android:name="android.permission.MANAGE_AUTO_FILL" /> <uses-permission android:name="android.permission.MANAGE_CONTENT_CAPTURE" /> + <uses-permission android:name="android.permission.MANAGE_CONTENT_SUGGESTIONS" /> + <uses-permission android:name="android.permission.MANAGE_APP_PREDICTIONS" /> <uses-permission android:name="android.permission.NETWORK_SETTINGS" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.SET_TIME" /> @@ -162,6 +164,8 @@ <uses-permission android:name="android.permission.SUSPEND_APPS" /> <uses-permission android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND" /> + <uses-permission android:name="android.permission.MANAGE_APPOPS" /> + <application android:label="@string/app_label" android:defaultToDeviceProtectedStorage="true" android:directBootAware="true"> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 1c8a672faf87..17cc1d57582c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -55,6 +55,10 @@ public class KeyguardClockSwitch extends RelativeLayout { * Consumer that accepts the a new ClockPlugin implementation when the Extension reloads. */ private final Consumer<ClockPlugin> mClockPluginConsumer = plugin -> setClockPlugin(plugin); + /** + * Maintain state so that a newly connected plugin can be initialized. + */ + private float mDarkAmount; private final StatusBarStateController.StateListener mStateListener = new StatusBarStateController.StateListener() { @@ -147,6 +151,7 @@ public class KeyguardClockSwitch extends RelativeLayout { mClockPlugin = plugin; mClockPlugin.setStyle(getPaint().getStyle()); mClockPlugin.setTextColor(getCurrentTextColor()); + mClockPlugin.setDarkAmount(mDarkAmount); } /** @@ -208,6 +213,7 @@ public class KeyguardClockSwitch extends RelativeLayout { * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake. */ public void setDarkAmount(float darkAmount) { + mDarkAmount = darkAmount; if (mClockPlugin != null) { mClockPlugin.setDarkAmount(darkAmount); } diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java index 6864ea185834..8e273efb6171 100644 --- a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java +++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java @@ -40,7 +40,6 @@ import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.LayoutInflater; -import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; @@ -56,7 +55,6 @@ import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; -import com.android.systemui.statusbar.policy.IconLogger; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.Utils.DisableStateTracker; @@ -276,9 +274,6 @@ public class BatteryMeterView extends LinearLayout implements public void onTuningChanged(String key, String newValue) { if (StatusBarIconController.ICON_BLACKLIST.equals(key)) { ArraySet<String> icons = StatusBarIconController.getIconBlacklist(newValue); - boolean hidden = icons.contains(mSlotBattery); - Dependency.get(IconLogger.class).onIconVisibility(mSlotBattery, !hidden); - setVisibility(hidden ? View.GONE : View.VISIBLE); } } diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 9d810645f6a2..ec6ecc64d07e 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -86,7 +86,6 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.ExtensionController; import com.android.systemui.statusbar.policy.FlashlightController; import com.android.systemui.statusbar.policy.HotspotController; -import com.android.systemui.statusbar.policy.IconLogger; import com.android.systemui.statusbar.policy.KeyguardMonitor; import com.android.systemui.statusbar.policy.LocationController; import com.android.systemui.statusbar.policy.NetworkController; @@ -217,7 +216,6 @@ public class Dependency extends SystemUI { @Inject Lazy<LeakDetector> mLeakDetector; @Inject Lazy<LeakReporter> mLeakReporter; @Inject Lazy<GarbageMonitor> mGarbageMonitor; - @Inject Lazy<IconLogger> mIconLogger; @Inject Lazy<TunerService> mTunerService; @Inject Lazy<StatusBarWindowController> mStatusBarWindowController; @Inject Lazy<DarkIconDispatcher> mDarkIconDispatcher; @@ -392,8 +390,6 @@ public class Dependency extends SystemUI { mProviders.put(PowerUI.WarningsUI.class, mWarningsUI::get); - mProviders.put(IconLogger.class, mIconLogger::get); - mProviders.put(LightBarController.class, mLightBarController::get); mProviders.put(IWindowManager.class, mIWindowManager::get); diff --git a/packages/SystemUI/src/com/android/systemui/DependencyBinder.java b/packages/SystemUI/src/com/android/systemui/DependencyBinder.java index f324a05bafff..ce9c637cebf6 100644 --- a/packages/SystemUI/src/com/android/systemui/DependencyBinder.java +++ b/packages/SystemUI/src/com/android/systemui/DependencyBinder.java @@ -46,8 +46,6 @@ import com.android.systemui.statusbar.policy.FlashlightController; import com.android.systemui.statusbar.policy.FlashlightControllerImpl; import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.HotspotControllerImpl; -import com.android.systemui.statusbar.policy.IconLogger; -import com.android.systemui.statusbar.policy.IconLoggerImpl; import com.android.systemui.statusbar.policy.KeyguardMonitor; import com.android.systemui.statusbar.policy.KeyguardMonitorImpl; import com.android.systemui.statusbar.policy.LocationController; @@ -133,11 +131,6 @@ public abstract class DependencyBinder { /** */ @Binds - public abstract IconLogger provideIconLogger(IconLoggerImpl loggerImpl); - - /** - */ - @Binds public abstract CastController provideCastController(CastControllerImpl controllerImpl); /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java index db7589d0f333..f846036248d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java @@ -36,7 +36,6 @@ import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconStat import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; -import com.android.systemui.statusbar.policy.IconLogger; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; @@ -61,7 +60,6 @@ public class StatusBarIconControllerImpl extends StatusBarIconList implements Tu private final ArrayList<IconManager> mIconGroups = new ArrayList<>(); private final ArraySet<String> mIconBlacklist = new ArraySet<>(); - private final IconLogger mIconLogger = Dependency.get(IconLogger.class); // Points to light or dark context depending on the... context? private Context mContext; @@ -147,7 +145,6 @@ public class StatusBarIconControllerImpl extends StatusBarIconList implements Tu int viewIndex = getViewIndex(index, holder.getTag()); boolean blocked = mIconBlacklist.contains(slot); - mIconLogger.onIconVisibility(getSlotName(index), holder.isVisible()); mIconGroups.forEach(l -> l.onIconAdded(viewIndex, slot, blocked, holder)); } @@ -281,8 +278,6 @@ public class StatusBarIconControllerImpl extends StatusBarIconList implements Tu return; } - mIconLogger.onIconHidden(slotName); - int slotIndex = getSlotIndex(slotName); List<StatusBarIconHolder> iconsToRemove = slot.getHolderListInViewOrder(); for (StatusBarIconHolder holder : iconsToRemove) { @@ -297,7 +292,6 @@ public class StatusBarIconControllerImpl extends StatusBarIconList implements Tu if (getIcon(index, tag) == null) { return; } - mIconLogger.onIconHidden(getSlotName(index)); super.removeIcon(index, tag); int viewIndex = getViewIndex(index, 0); mIconGroups.forEach(l -> l.onRemoveIcon(viewIndex)); @@ -305,7 +299,6 @@ public class StatusBarIconControllerImpl extends StatusBarIconList implements Tu private void handleSet(int index, StatusBarIconHolder holder) { int viewIndex = getViewIndex(index, holder.getTag()); - mIconLogger.onIconVisibility(getSlotName(index), holder.isVisible()); mIconGroups.forEach(l -> l.onSetIconHolder(viewIndex, holder)); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index de7ef3ba645d..4299af7daf66 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -274,7 +274,6 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C private void updateClockVisibility() { boolean visible = shouldBeVisible(); - Dependency.get(IconLogger.class).onIconVisibility("clock", visible); int visibility = visible ? View.VISIBLE : View.GONE; super.setVisibility(visibility); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IconLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IconLogger.java deleted file mode 100644 index 710e1df61404..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IconLogger.java +++ /dev/null @@ -1,29 +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 com.android.systemui.statusbar.policy; - -public interface IconLogger { - - void onIconShown(String tag); - void onIconHidden(String tag); - - default void onIconVisibility(String tag, boolean visible) { - if (visible) { - onIconShown(tag); - } else { - onIconHidden(tag); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IconLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IconLoggerImpl.java deleted file mode 100644 index ba6369e2c90e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IconLoggerImpl.java +++ /dev/null @@ -1,115 +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 com.android.systemui.statusbar.policy; - -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_NUM_STATUS_ICONS; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_STATUS_ICONS; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.STATUS_BAR_ICONS_CHANGED; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_ACTION; -import static com.android.systemui.Dependency.BG_LOOPER_NAME; - -import android.content.Context; -import android.metrics.LogMaker; -import android.os.Handler; -import android.os.Looper; -import android.util.ArraySet; - -import androidx.annotation.VisibleForTesting; - -import com.android.internal.logging.MetricsLogger; - -import java.util.Arrays; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; - -public class IconLoggerImpl implements IconLogger { - - // Minimum ms between log statements. - // NonFinalForTesting - @VisibleForTesting - protected static long MIN_LOG_INTERVAL = 1000; - - private final Context mContext; - private final Handler mHandler; - private final MetricsLogger mLogger; - private final ArraySet<String> mIcons = new ArraySet<>(); - private final List<String> mIconIndex; - private long mLastLog = System.currentTimeMillis(); - - @Inject - public IconLoggerImpl(Context context, @Named(BG_LOOPER_NAME) Looper bgLooper, - MetricsLogger logger) { - mContext = context; - mHandler = new Handler(bgLooper); - mLogger = logger; - String[] icons = mContext.getResources().getStringArray( - com.android.internal.R.array.config_statusBarIcons); - mIconIndex = Arrays.asList(icons); - doLog(); - } - - @Override - public void onIconShown(String tag) { - synchronized (mIcons) { - if (mIcons.contains(tag)) return; - mIcons.add(tag); - } - if (!mHandler.hasCallbacks(mLog)) { - mHandler.postDelayed(mLog, MIN_LOG_INTERVAL); - } - } - - @Override - public void onIconHidden(String tag) { - synchronized (mIcons) { - if (!mIcons.contains(tag)) return; - mIcons.remove(tag); - } - if (!mHandler.hasCallbacks(mLog)) { - mHandler.postDelayed(mLog, MIN_LOG_INTERVAL); - } - } - - private void doLog() { - long time = System.currentTimeMillis(); - long timeSinceLastLog = time - mLastLog; - mLastLog = time; - - ArraySet<String> icons; - synchronized (mIcons) { - icons = new ArraySet<>(mIcons); - } - mLogger.write(new LogMaker(STATUS_BAR_ICONS_CHANGED) - .setType(TYPE_ACTION) - .setLatency(timeSinceLastLog) - .addTaggedData(FIELD_NUM_STATUS_ICONS, icons.size()) - .addTaggedData(FIELD_STATUS_ICONS, getBitField(icons))); - } - - private int getBitField(ArraySet<String> icons) { - int iconsVisible = 0; - for (String icon : icons) { - int index = mIconIndex.indexOf(icon); - if (index >= 0) { - iconsVisible |= (1 << index); - } - } - return iconsVisible; - } - - private final Runnable mLog = this::doLog; -} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java index 8e02f578f6fd..fbc1c20755a1 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java @@ -132,6 +132,17 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { } @Test + public void onPluginConnected_darkAmountInitialized() { + // GIVEN that the dark amount has already been set + mKeyguardClockSwitch.setDarkAmount(0.5f); + // WHEN a plugin is connected + ClockPlugin plugin = mock(ClockPlugin.class); + mKeyguardClockSwitch.getClockPluginConsumer().accept(plugin); + // THEN dark amount should be initalized on the plugin. + verify(plugin).setDarkAmount(0.5f); + } + + @Test public void onPluginDisconnected_showDefaultClock() { ClockPlugin plugin = mock(ClockPlugin.class); TextClock pluginView = new TextClock(getContext()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/IconLoggerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/IconLoggerImplTest.java deleted file mode 100644 index 5c347301bd8c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/IconLoggerImplTest.java +++ /dev/null @@ -1,177 +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 com.android.systemui.statusbar.policy; - -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_NUM_STATUS_ICONS; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_STATUS_ICONS; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent - .RESERVED_FOR_LOGBUILDER_LATENCY_MILLIS; - -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import static java.lang.Thread.sleep; - -import android.metrics.LogMaker; -import android.support.test.filters.SmallTest; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.testing.TestableLooper.MessageHandler; -import android.testing.TestableLooper.RunWithLooper; -import android.util.Log; - -import com.android.internal.logging.MetricsLogger; -import com.android.systemui.SysuiTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentMatcher; - -@RunWith(AndroidTestingRunner.class) -@RunWithLooper -@SmallTest -public class IconLoggerImplTest extends SysuiTestCase { - - private MetricsLogger mMetricsLogger; - private IconLoggerImpl mIconLogger; - private TestableLooper mTestableLooper; - private MessageHandler mMessageHandler; - - @Before - public void setup() { - IconLoggerImpl.MIN_LOG_INTERVAL = 5; // Low interval for testing - mMetricsLogger = mock(MetricsLogger.class); - mTestableLooper = TestableLooper.get(this); - mMessageHandler = mock(MessageHandler.class); - mTestableLooper.setMessageHandler(mMessageHandler); - String[] iconArray = new String[] { - "test_icon_1", - "test_icon_2", - }; - mContext.getOrCreateTestableResources().addOverride( - com.android.internal.R.array.config_statusBarIcons, iconArray); - mIconLogger = new IconLoggerImpl(mContext, mTestableLooper.getLooper(), mMetricsLogger); - when(mMessageHandler.onMessageHandled(any())).thenReturn(true); - clearInvocations(mMetricsLogger); - } - - @Test - public void testIconShown() throws InterruptedException { - // Should only get one message, for the same icon shown twice. - mIconLogger.onIconShown("test_icon_2"); - mIconLogger.onIconShown("test_icon_2"); - - // There should be some delay before execute. - mTestableLooper.processAllMessages(); - verify(mMessageHandler, never()).onMessageHandled(any()); - - sleep(10); - mTestableLooper.processAllMessages(); - verify(mMessageHandler, times(1)).onMessageHandled(any()); - } - - @Test - public void testIconHidden() throws InterruptedException { - // Add the icon so that it can be removed. - mIconLogger.onIconShown("test_icon_2"); - sleep(10); - mTestableLooper.processAllMessages(); - clearInvocations(mMessageHandler); - - // Should only get one message, for the same icon shown twice. - mIconLogger.onIconHidden("test_icon_2"); - mIconLogger.onIconHidden("test_icon_2"); - - // There should be some delay before execute. - mTestableLooper.processAllMessages(); - verify(mMessageHandler, never()).onMessageHandled(any()); - - sleep(10); - mTestableLooper.processAllMessages(); - verify(mMessageHandler, times(1)).onMessageHandled(any()); - } - - @Test - public void testLog() throws InterruptedException { - mIconLogger.onIconShown("test_icon_2"); - sleep(10); - mTestableLooper.processAllMessages(); - - verify(mMetricsLogger).write(argThat(maker -> { - if (IconLoggerImpl.MIN_LOG_INTERVAL > - (long) maker.getTaggedData(RESERVED_FOR_LOGBUILDER_LATENCY_MILLIS)) { - Log.e("IconLoggerImplTest", "Invalid latency " - + maker.getTaggedData(RESERVED_FOR_LOGBUILDER_LATENCY_MILLIS)); - return false; - } - if (1 != (int) maker.getTaggedData(FIELD_NUM_STATUS_ICONS)) { - Log.e("IconLoggerImplTest", "Invalid icon count " - + maker.getTaggedData(FIELD_NUM_STATUS_ICONS)); - return false; - } - return true; - })); - } - - @Test - public void testBitField() throws InterruptedException { - mIconLogger.onIconShown("test_icon_2"); - sleep(10); - mTestableLooper.processAllMessages(); - - verify(mMetricsLogger).write(argThat(maker -> { - if ((1 << 1) != (int) maker.getTaggedData(FIELD_STATUS_ICONS)) { - Log.e("IconLoggerImplTest", "Invalid bitfield " + Integer.toHexString( - (Integer) maker.getTaggedData(FIELD_NUM_STATUS_ICONS))); - return false; - } - return true; - })); - - mIconLogger.onIconShown("test_icon_1"); - sleep(10); - mTestableLooper.processAllMessages(); - - verify(mMetricsLogger).write(argThat(maker -> { - if ((1 << 1 | 1 << 0) != (int) maker.getTaggedData(FIELD_STATUS_ICONS)) { - Log.e("IconLoggerImplTest", "Invalid bitfield " + Integer.toHexString( - (Integer) maker.getTaggedData(FIELD_NUM_STATUS_ICONS))); - return false; - } - return true; - })); - - mIconLogger.onIconHidden("test_icon_2"); - sleep(10); - mTestableLooper.processAllMessages(); - - verify(mMetricsLogger).write(argThat(maker -> { - if ((1 << 0) != (int) maker.getTaggedData(FIELD_STATUS_ICONS)) { - Log.e("IconLoggerImplTest", "Invalid bitfield " + Integer.toHexString( - (Integer) maker.getTaggedData(FIELD_STATUS_ICONS))); - return false; - } - return true; - })); - } -} diff --git a/services/Android.bp b/services/Android.bp index 0abe53daf818..31385edd015f 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -17,11 +17,13 @@ java_library { static_libs: [ "services.core", "services.accessibility", + "services.appprediction", "services.appwidget", "services.autofill", "services.backup", "services.companion", "services.contentcapture", + "services.contentsuggestions", "services.coverage", "services.devicepolicy", "services.ipmemorystore", diff --git a/services/appprediction/Android.bp b/services/appprediction/Android.bp new file mode 100644 index 000000000000..a7be58783aab --- /dev/null +++ b/services/appprediction/Android.bp @@ -0,0 +1,5 @@ +java_library_static { + name: "services.appprediction", + srcs: ["java/**/*.java"], + libs: ["services.core"], +} diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java new file mode 100644 index 000000000000..833eaa06d759 --- /dev/null +++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java @@ -0,0 +1,156 @@ +/* + * 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. + */ + +package com.android.server.appprediction; + +import static android.Manifest.permission.MANAGE_APP_PREDICTIONS; +import static android.content.Context.APP_PREDICTION_SERVICE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.IPredictionCallback; +import android.app.prediction.IPredictionManager; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.os.Binder; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ShellCallback; +import android.os.UserHandle; + +import com.android.server.infra.AbstractMasterSystemService; +import com.android.server.infra.FrameworkResourcesServiceNameResolver; + +import java.io.FileDescriptor; +import java.util.function.Consumer; + +/** + * A service used to predict app and shortcut usage. + * + * <p>The data collected by this service can be analyzed and combined with other sources to provide + * predictions in different areas of the system such as Launcher and Share sheet. + */ +public class AppPredictionManagerService extends + AbstractMasterSystemService<AppPredictionManagerService, AppPredictionPerUserService> { + + private static final String TAG = AppPredictionManagerService.class.getSimpleName(); + + private static final int MAX_TEMP_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes + + public AppPredictionManagerService(Context context) { + super(context, new FrameworkResourcesServiceNameResolver(context, + com.android.internal.R.string.config_defaultAppPredictionService), null); + } + + @Override + protected AppPredictionPerUserService newServiceLocked(int resolvedUserId, boolean disabled) { + return new AppPredictionPerUserService(this, mLock, resolvedUserId); + } + + @Override + public void onStart() { + publishBinderService(APP_PREDICTION_SERVICE, new PredictionManagerServiceStub()); + } + + @Override + protected void enforceCallingPermissionForManagement() { + getContext().enforceCallingPermission(MANAGE_APP_PREDICTIONS, TAG); + } + + @Override + protected int getMaximumTemporaryServiceDurationMs() { + return MAX_TEMP_SERVICE_DURATION_MS; + } + + private class PredictionManagerServiceStub extends IPredictionManager.Stub { + + @Override + public void createPredictionSession(@NonNull AppPredictionContext context, + @NonNull AppPredictionSessionId sessionId) { + runForUserLocked((service) -> + service.onCreatePredictionSessionLocked(context, sessionId)); + } + + @Override + public void notifyAppTargetEvent(@NonNull AppPredictionSessionId sessionId, + @NonNull AppTargetEvent event) { + runForUserLocked((service) -> service.notifyAppTargetEventLocked(sessionId, event)); + } + + @Override + public void notifyLocationShown(@NonNull AppPredictionSessionId sessionId, + @NonNull String launchLocation, @NonNull ParceledListSlice targetIds) { + runForUserLocked((service) -> + service.notifyLocationShownLocked(sessionId, launchLocation, targetIds)); + } + + @Override + public void sortAppTargets(@NonNull AppPredictionSessionId sessionId, + @NonNull ParceledListSlice targets, + IPredictionCallback callback) { + runForUserLocked((service) -> + service.sortAppTargetsLocked(sessionId, targets, callback)); + } + + @Override + public void registerPredictionUpdates(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + runForUserLocked((service) -> + service.registerPredictionUpdatesLocked(sessionId, callback)); + } + + public void unregisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + runForUserLocked((service) -> + service.unregisterPredictionUpdatesLocked(sessionId, callback)); + } + + @Override + public void requestPredictionUpdate(@NonNull AppPredictionSessionId sessionId) { + runForUserLocked((service) -> service.requestPredictionUpdateLocked(sessionId)); + } + + @Override + public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) { + runForUserLocked((service) -> service.onDestroyPredictionSessionLocked(sessionId)); + } + + public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, + @Nullable FileDescriptor err, + @NonNull String[] args, @Nullable ShellCallback callback, + @NonNull ResultReceiver resultReceiver) throws RemoteException { + new AppPredictionManagerServiceShellCommand(AppPredictionManagerService.this) + .exec(this, in, out, err, args, callback, resultReceiver); + } + + private void runForUserLocked(@NonNull Consumer<AppPredictionPerUserService> c) { + final int userId = UserHandle.getCallingUserId(); + // TODO(b/111701043): Determine what permission model we want for this + long origId = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + final AppPredictionPerUserService service = getServiceForUserLocked(userId); + c.accept(service); + } + } finally { + Binder.restoreCallingIdentity(origId); + } + } + } +} diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerServiceShellCommand.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerServiceShellCommand.java new file mode 100644 index 000000000000..ed7cc67aa46f --- /dev/null +++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerServiceShellCommand.java @@ -0,0 +1,83 @@ +/* + * 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. + */ + +package com.android.server.appprediction; + +import android.annotation.NonNull; +import android.os.ShellCommand; + +import java.io.PrintWriter; + +/** + * The shell command implementation for the AppPredictionManagerService. + */ +public class AppPredictionManagerServiceShellCommand extends ShellCommand { + + private static final String TAG = + AppPredictionManagerServiceShellCommand.class.getSimpleName(); + + private final AppPredictionManagerService mService; + + public AppPredictionManagerServiceShellCommand(@NonNull AppPredictionManagerService service) { + mService = service; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + final PrintWriter pw = getOutPrintWriter(); + switch (cmd) { + case "set": { + final String what = getNextArgRequired(); + switch (what) { + case "temporary-service": { + final int userId = Integer.parseInt(getNextArgRequired()); + String serviceName = getNextArg(); + if (serviceName == null) { + mService.resetTemporaryService(userId); + return 0; + } + final int duration = Integer.parseInt(getNextArgRequired()); + mService.setTemporaryService(userId, serviceName, duration); + pw.println("AppPredictionService temporarily set to " + serviceName + + " for " + duration + "ms"); + break; + } + } + } + break; + default: + return handleDefaultCommands(cmd); + } + return 0; + } + + @Override + public void onHelp() { + try (PrintWriter pw = getOutPrintWriter()) { + pw.println("AppPredictionManagerService commands:"); + pw.println(" help"); + pw.println(" Prints this help text."); + pw.println(""); + pw.println(" set temporary-service USER_ID [COMPONENT_NAME DURATION]"); + pw.println(" Temporarily (for DURATION ms) changes the service implemtation."); + pw.println(" To reset, call with just the USER_ID argument."); + pw.println(""); + } + } +} diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java new file mode 100644 index 000000000000..24d592c38afb --- /dev/null +++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java @@ -0,0 +1,218 @@ +/* + * 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. + */ + +package com.android.server.appprediction; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AppGlobals; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.IPredictionCallback; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ParceledListSlice; +import android.content.pm.ServiceInfo; +import android.os.RemoteException; +import android.service.appprediction.AppPredictionService; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.infra.AbstractPerUserSystemService; + +/** + * Per-user instance of {@link AppPredictionManagerService}. + */ +public class AppPredictionPerUserService extends + AbstractPerUserSystemService<AppPredictionPerUserService, AppPredictionManagerService> + implements RemoteAppPredictionService.RemoteAppPredictionServiceCallbacks { + + private static final String TAG = AppPredictionPerUserService.class.getSimpleName(); + + @Nullable + @GuardedBy("mLock") + private RemoteAppPredictionService mRemoteService; + + protected AppPredictionPerUserService(AppPredictionManagerService master, + Object lock, int userId) { + super(master, lock, userId); + } + + @Override // from PerUserSystemService + protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) + throws NameNotFoundException { + + ServiceInfo si; + try { + si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent, + PackageManager.GET_META_DATA, mUserId); + } catch (RemoteException e) { + throw new NameNotFoundException("Could not get service for " + serviceComponent); + } + // TODO(b/111701043): must check that either the service is from a system component, + // or it matches a service set by shell cmd (so it can be used on CTS tests and when + // OEMs are implementing the real service and also verify the proper permissions + return si; + } + + @GuardedBy("mLock") + @Override // from PerUserSystemService + protected boolean updateLocked(boolean disabled) { + final boolean enabledChanged = super.updateLocked(disabled); + if (enabledChanged) { + if (!isEnabledLocked()) { + // Clear the remote service for the next call + mRemoteService = null; + } + } + return enabledChanged; + } + + /** + * Notifies the service of a new prediction session. + */ + @GuardedBy("mLock") + public void onCreatePredictionSessionLocked(@NonNull AppPredictionContext context, + @NonNull AppPredictionSessionId sessionId) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.onCreatePredictionSession(context, sessionId); + } + } + + /** + * Records an app target event to the service. + */ + @GuardedBy("mLock") + public void notifyAppTargetEventLocked(@NonNull AppPredictionSessionId sessionId, + @NonNull AppTargetEvent event) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.notifyAppTargetEvent(sessionId, event); + } + } + + /** + * Records when a launch location is shown. + */ + @GuardedBy("mLock") + public void notifyLocationShownLocked(@NonNull AppPredictionSessionId sessionId, + @NonNull String launchLocation, @NonNull ParceledListSlice targetIds) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.notifyLocationShown(sessionId, launchLocation, targetIds); + } + } + + /** + * Requests the service to sort a list of apps or shortcuts. + */ + @GuardedBy("mLock") + public void sortAppTargetsLocked(@NonNull AppPredictionSessionId sessionId, + @NonNull ParceledListSlice targets, @NonNull IPredictionCallback callback) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.sortAppTargets(sessionId, targets, callback); + } + } + + /** + * Registers a callback for continuous updates of predicted apps or shortcuts. + */ + @GuardedBy("mLock") + public void registerPredictionUpdatesLocked(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.registerPredictionUpdates(sessionId, callback); + } + } + + /** + * Unregisters a callback for continuous updates of predicted apps or shortcuts. + */ + @GuardedBy("mLock") + public void unregisterPredictionUpdatesLocked(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.unregisterPredictionUpdates(sessionId, callback); + } + } + + /** + * Requests a new set of predicted apps or shortcuts. + */ + @GuardedBy("mLock") + public void requestPredictionUpdateLocked(@NonNull AppPredictionSessionId sessionId) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.requestPredictionUpdate(sessionId); + } + } + + /** + * Notifies the service of the end of an existing prediction session. + */ + @GuardedBy("mLock") + public void onDestroyPredictionSessionLocked(@NonNull AppPredictionSessionId sessionId) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.onDestroyPredictionSession(sessionId); + } + } + + @Override + public void onFailureOrTimeout(boolean timedOut) { + if (isDebug()) { + Slog.d(TAG, "onFailureOrTimeout(): timed out=" + timedOut); + } + + // Do nothing, we are just proxying to the prediction service + } + + @Override + public void onServiceDied(RemoteAppPredictionService service) { + if (isDebug()) { + Slog.d(TAG, "onServiceDied():"); + } + + // Do nothing, we are just proxying to the prediction service + } + + @GuardedBy("mLock") + @Nullable + private RemoteAppPredictionService getRemoteServiceLocked() { + if (mRemoteService == null) { + final String serviceName = getComponentNameLocked(); + if (serviceName == null) { + if (mMaster.verbose) { + Slog.v(TAG, "getRemoteServiceLocked(): not set"); + } + return null; + } + ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName); + + mRemoteService = new RemoteAppPredictionService(getContext(), + AppPredictionService.SERVICE_INTERFACE, serviceComponent, mUserId, this, + mMaster.isBindInstantServiceAllowed(), mMaster.verbose); + } + + return mRemoteService; + } +} diff --git a/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java b/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java new file mode 100644 index 000000000000..45ea86f9e74e --- /dev/null +++ b/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java @@ -0,0 +1,142 @@ +/* + * 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. + */ +package com.android.server.appprediction; + +import android.annotation.NonNull; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.IPredictionCallback; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.os.IBinder; +import android.service.appprediction.IPredictionService; +import android.text.format.DateUtils; + +import com.android.internal.infra.AbstractMultiplePendingRequestsRemoteService; + + +/** + * Proxy to the {@link android.service.appprediction.AppPredictionService} implemention in another + * process. + */ +public class RemoteAppPredictionService extends + AbstractMultiplePendingRequestsRemoteService<RemoteAppPredictionService, + IPredictionService> { + + private static final String TAG = "RemoteAppPredictionService"; + + private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS; + + public RemoteAppPredictionService(Context context, String serviceInterface, + ComponentName componentName, int userId, + RemoteAppPredictionServiceCallbacks callback, boolean bindInstantServiceAllowed, + boolean verbose) { + super(context, serviceInterface, componentName, userId, callback, bindInstantServiceAllowed, + verbose, /* initialCapacity= */ 1); + } + + @Override + protected IPredictionService getServiceInterface(IBinder service) { + return IPredictionService.Stub.asInterface(service); + } + + @Override + protected long getTimeoutIdleBindMillis() { + return PERMANENT_BOUND_TIMEOUT_MS; + } + + @Override + protected long getRemoteRequestMillis() { + return TIMEOUT_REMOTE_REQUEST_MILLIS; + } + + /** + * Notifies the service of a new prediction session. + */ + public void onCreatePredictionSession(@NonNull AppPredictionContext context, + @NonNull AppPredictionSessionId sessionId) { + scheduleAsyncRequest((s) -> s.onCreatePredictionSession(context, sessionId)); + } + + /** + * Records an app target event to the service. + */ + public void notifyAppTargetEvent(@NonNull AppPredictionSessionId sessionId, + @NonNull AppTargetEvent event) { + scheduleAsyncRequest((s) -> s.notifyAppTargetEvent(sessionId, event)); + } + + /** + * Records when a launch location is shown. + */ + public void notifyLocationShown(@NonNull AppPredictionSessionId sessionId, + @NonNull String launchLocation, @NonNull ParceledListSlice targetIds) { + scheduleAsyncRequest((s) -> s.notifyLocationShown(sessionId, launchLocation, targetIds)); + } + + /** + * Requests the service to sort a list of apps or shortcuts. + */ + public void sortAppTargets(@NonNull AppPredictionSessionId sessionId, + @NonNull ParceledListSlice targets, @NonNull IPredictionCallback callback) { + scheduleAsyncRequest((s) -> s.sortAppTargets(sessionId, targets, callback)); + } + + + /** + * Registers a callback for continuous updates of predicted apps or shortcuts. + */ + public void registerPredictionUpdates(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + scheduleAsyncRequest((s) -> s.registerPredictionUpdates(sessionId, callback)); + } + + /** + * Unregisters a callback for continuous updates of predicted apps or shortcuts. + */ + public void unregisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + scheduleAsyncRequest((s) -> s.unregisterPredictionUpdates(sessionId, callback)); + } + + /** + * Requests a new set of predicted apps or shortcuts. + */ + public void requestPredictionUpdate(@NonNull AppPredictionSessionId sessionId) { + scheduleAsyncRequest((s) -> s.requestPredictionUpdate(sessionId)); + } + + /** + * Notifies the service of the end of an existing prediction session. + */ + public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) { + scheduleAsyncRequest((s) -> s.onDestroyPredictionSession(sessionId)); + } + + /** + * Failure callback + */ + public interface RemoteAppPredictionServiceCallbacks + extends VultureCallback<RemoteAppPredictionService> { + + /** + * Notifies a the failure or timeout of a remote call. + */ + void onFailureOrTimeout(boolean timedOut); + } +} diff --git a/services/contentsuggestions/Android.bp b/services/contentsuggestions/Android.bp new file mode 100644 index 000000000000..fc09d2e5196a --- /dev/null +++ b/services/contentsuggestions/Android.bp @@ -0,0 +1,5 @@ +java_library_static { + name: "services.contentsuggestions", + srcs: ["java/**/*.java"], + libs: ["services.core"], +}
\ No newline at end of file diff --git a/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsManagerService.java b/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsManagerService.java new file mode 100644 index 000000000000..58dbea469b9c --- /dev/null +++ b/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsManagerService.java @@ -0,0 +1,205 @@ +/* + * 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. + */ + +package com.android.server.contentsuggestions; + +import static android.Manifest.permission.MANAGE_CONTENT_SUGGESTIONS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.contentsuggestions.ClassificationsRequest; +import android.app.contentsuggestions.IClassificationsCallback; +import android.app.contentsuggestions.IContentSuggestionsManager; +import android.app.contentsuggestions.ISelectionsCallback; +import android.app.contentsuggestions.SelectionsRequest; +import android.content.Context; +import android.os.Binder; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ShellCallback; +import android.os.UserHandle; +import android.util.Slog; + +import com.android.server.LocalServices; +import com.android.server.infra.AbstractMasterSystemService; +import com.android.server.infra.FrameworkResourcesServiceNameResolver; +import com.android.server.wm.ActivityTaskManagerInternal; + +import java.io.FileDescriptor; + +/** + * The system service for providing recents / overview with content suggestion selections and + * classifications. + * + * <p>Calls are received here from + * {@link android.app.contentsuggestions.ContentSuggestionsManager} then delegated to + * a per user version of the service. From there they are routed to the remote actual implementation + * that provides the suggestion selections and classifications. + */ +public class ContentSuggestionsManagerService extends + AbstractMasterSystemService< + ContentSuggestionsManagerService, ContentSuggestionsPerUserService> { + + private static final String TAG = ContentSuggestionsManagerService.class.getSimpleName(); + private static final boolean VERBOSE = false; // TODO: make dynamic + + private static final int MAX_TEMP_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes + + private ActivityTaskManagerInternal mActivityTaskManagerInternal; + + public ContentSuggestionsManagerService(Context context) { + super(context, new FrameworkResourcesServiceNameResolver(context, + com.android.internal.R.string.config_defaultContentSuggestionsService), null); + mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); + } + + @Override + protected ContentSuggestionsPerUserService newServiceLocked(int resolvedUserId, + boolean disabled) { + return new ContentSuggestionsPerUserService(this, mLock, resolvedUserId); + } + + @Override + public void onStart() { + publishBinderService( + Context.CONTENT_SUGGESTIONS_SERVICE, new ContentSuggestionsManagerStub()); + } + + @Override + protected void enforceCallingPermissionForManagement() { + getContext().enforceCallingPermission(MANAGE_CONTENT_SUGGESTIONS, TAG); + } + + @Override + protected int getMaximumTemporaryServiceDurationMs() { + return MAX_TEMP_SERVICE_DURATION_MS; + } + + private boolean isCallerRecents(int userId) { + if (mServiceNameResolver.isTemporary(userId)) { + // If a temporary service is set then skip the recents check + return true; + } + return mActivityTaskManagerInternal.isCallerRecents(Binder.getCallingUid()); + } + + private void enforceCallerIsRecents(int userId, String func) { + if (isCallerRecents(userId)) { + return; + } + + String msg = "Permission Denial: " + func + " from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid() + + " expected caller is recents"; + Slog.w(TAG, msg); + throw new SecurityException(msg); + } + + private class ContentSuggestionsManagerStub extends IContentSuggestionsManager.Stub { + @Override + public void provideContextImage(int taskId, @NonNull Bundle imageContextRequestExtras) { + if (imageContextRequestExtras == null) { + throw new IllegalArgumentException("Expected non-null imageContextRequestExtras"); + } + + final int userId = UserHandle.getCallingUserId(); + enforceCallerIsRecents(userId, "provideContextImage"); + + synchronized (mLock) { + final ContentSuggestionsPerUserService service = getServiceForUserLocked(userId); + if (service != null) { + service.provideContextImageLocked(taskId, imageContextRequestExtras); + } else { + if (VERBOSE) { + Slog.v(TAG, "provideContextImageLocked: no service for " + userId); + } + } + } + } + + @Override + public void suggestContentSelections( + @NonNull SelectionsRequest selectionsRequest, + @NonNull ISelectionsCallback selectionsCallback) { + final int userId = UserHandle.getCallingUserId(); + enforceCallerIsRecents(userId, "suggestContentSelections"); + + synchronized (mLock) { + final ContentSuggestionsPerUserService service = getServiceForUserLocked(userId); + if (service != null) { + service.suggestContentSelectionsLocked(selectionsRequest, selectionsCallback); + } else { + if (VERBOSE) { + Slog.v(TAG, "suggestContentSelectionsLocked: no service for " + userId); + } + } + } + } + + @Override + public void classifyContentSelections( + @NonNull ClassificationsRequest classificationsRequest, + @NonNull IClassificationsCallback callback) { + final int userId = UserHandle.getCallingUserId(); + enforceCallerIsRecents(userId, "classifyContentSelections"); + + synchronized (mLock) { + final ContentSuggestionsPerUserService service = getServiceForUserLocked(userId); + if (service != null) { + service.classifyContentSelectionsLocked(classificationsRequest, callback); + } else { + if (VERBOSE) { + Slog.v(TAG, "classifyContentSelectionsLocked: no service for " + userId); + } + } + } + } + + @Override + public void notifyInteraction(@NonNull String requestId, @NonNull Bundle bundle) { + final int userId = UserHandle.getCallingUserId(); + enforceCallerIsRecents(userId, "notifyInteraction"); + + synchronized (mLock) { + final ContentSuggestionsPerUserService service = getServiceForUserLocked(userId); + if (service != null) { + service.notifyInteractionLocked(requestId, bundle); + } else { + if (VERBOSE) { + Slog.v(TAG, "reportInteractionLocked: no service for " + userId); + } + } + } + } + + public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, + @Nullable FileDescriptor err, + @NonNull String[] args, @Nullable ShellCallback callback, + @NonNull ResultReceiver resultReceiver) throws RemoteException { + // Ensure that the caller is the shell process + final int callingUid = Binder.getCallingUid(); + if (callingUid != android.os.Process.SHELL_UID + && callingUid != android.os.Process.ROOT_UID) { + Slog.e(TAG, "Expected shell caller"); + return; + } + new ContentSuggestionsManagerServiceShellCommand(ContentSuggestionsManagerService.this) + .exec(this, in, out, err, args, callback, resultReceiver); + } + } +} diff --git a/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsManagerServiceShellCommand.java b/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsManagerServiceShellCommand.java new file mode 100644 index 000000000000..e34f1eadcd02 --- /dev/null +++ b/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsManagerServiceShellCommand.java @@ -0,0 +1,84 @@ +/* + * 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. + */ + +package com.android.server.contentsuggestions; + +import android.annotation.NonNull; +import android.os.ShellCommand; + +import java.io.PrintWriter; + +/** + * The shell command implementation for the ContentSuggestionsManagerService. + */ +public class ContentSuggestionsManagerServiceShellCommand extends ShellCommand { + + private static final String TAG = + ContentSuggestionsManagerServiceShellCommand.class.getSimpleName(); + + private final ContentSuggestionsManagerService mService; + + public ContentSuggestionsManagerServiceShellCommand( + @NonNull ContentSuggestionsManagerService service) { + mService = service; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + final PrintWriter pw = getOutPrintWriter(); + switch (cmd) { + case "set": { + final String what = getNextArgRequired(); + switch (what) { + case "temporary-service": { + final int userId = Integer.parseInt(getNextArgRequired()); + String serviceName = getNextArg(); + if (serviceName == null) { + mService.resetTemporaryService(userId); + return 0; + } + final int duration = Integer.parseInt(getNextArgRequired()); + mService.setTemporaryService(userId, serviceName, duration); + pw.println("ContentSuggestionsService temporarily set to " + serviceName + + " for " + duration + "ms"); + break; + } + } + } + break; + default: + return handleDefaultCommands(cmd); + } + return 0; + } + + @Override + public void onHelp() { + try (PrintWriter pw = getOutPrintWriter()) { + pw.println("ContentSuggestionsManagerService commands:"); + pw.println(" help"); + pw.println(" Prints this help text."); + pw.println(""); + pw.println(" set temporary-service USER_ID [COMPONENT_NAME DURATION]"); + pw.println(" Temporarily (for DURATION ms) changes the service implemtation."); + pw.println(" To reset, call with just the USER_ID argument."); + pw.println(""); + } + } +} diff --git a/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsPerUserService.java b/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsPerUserService.java new file mode 100644 index 000000000000..385bc6cf3932 --- /dev/null +++ b/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsPerUserService.java @@ -0,0 +1,162 @@ +/* + * 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. + */ + +package com.android.server.contentsuggestions; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.AppGlobals; +import android.app.contentsuggestions.ClassificationsRequest; +import android.app.contentsuggestions.IClassificationsCallback; +import android.app.contentsuggestions.ISelectionsCallback; +import android.app.contentsuggestions.SelectionsRequest; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.graphics.GraphicBuffer; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.LocalServices; +import com.android.server.infra.AbstractPerUserSystemService; +import com.android.server.wm.ActivityTaskManagerInternal; + +/** + * Per user delegate of {@link ContentSuggestionsManagerService}. + * + * <p>Main job is to forward calls to the remote implementation that can provide suggestion + * selections and classifications. + */ +public final class ContentSuggestionsPerUserService extends + AbstractPerUserSystemService< + ContentSuggestionsPerUserService, ContentSuggestionsManagerService> { + private static final String TAG = ContentSuggestionsPerUserService.class.getSimpleName(); + + @Nullable + @GuardedBy("mLock") + private RemoteContentSuggestionsService mRemoteService; + + @NonNull + private final ActivityTaskManagerInternal mActivityTaskManagerInternal; + + ContentSuggestionsPerUserService( + ContentSuggestionsManagerService master, Object lock, int userId) { + super(master, lock, userId); + mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); + } + + @GuardedBy("mLock") + @Override // from PerUserSystemService + protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) + throws PackageManager.NameNotFoundException { + ServiceInfo si; + try { + si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent, + PackageManager.GET_META_DATA, mUserId); + } catch (RemoteException e) { + throw new PackageManager.NameNotFoundException( + "Could not get service for " + serviceComponent); + } + return si; + } + + @GuardedBy("mLock") + @Override // from PerUserSystemService + protected boolean updateLocked(boolean disabled) { + final boolean enabledChanged = super.updateLocked(disabled); + if (enabledChanged) { + if (!isEnabledLocked()) { + // Clear the remote service for the next call + mRemoteService = null; + } + } + return enabledChanged; + } + + @GuardedBy("mLock") + void provideContextImageLocked(int taskId, @NonNull Bundle imageContextRequestExtras) { + RemoteContentSuggestionsService service = getRemoteServiceLocked(); + if (service != null) { + ActivityManager.TaskSnapshot snapshot = + mActivityTaskManagerInternal.getTaskSnapshot(taskId, false); + GraphicBuffer snapshotBuffer = null; + if (snapshot != null) { + snapshotBuffer = snapshot.getSnapshot(); + } + + service.provideContextImage(taskId, snapshotBuffer, imageContextRequestExtras); + } + } + + @GuardedBy("mLock") + void suggestContentSelectionsLocked( + @NonNull SelectionsRequest selectionsRequest, + @NonNull ISelectionsCallback selectionsCallback) { + RemoteContentSuggestionsService service = getRemoteServiceLocked(); + if (service != null) { + service.suggestContentSelections(selectionsRequest, selectionsCallback); + } + } + + @GuardedBy("mLock") + void classifyContentSelectionsLocked( + @NonNull ClassificationsRequest classificationsRequest, + @NonNull IClassificationsCallback callback) { + RemoteContentSuggestionsService service = getRemoteServiceLocked(); + if (service != null) { + service.classifyContentSelections(classificationsRequest, callback); + } + } + + @GuardedBy("mLock") + void notifyInteractionLocked(@NonNull String requestId, @NonNull Bundle bundle) { + RemoteContentSuggestionsService service = getRemoteServiceLocked(); + if (service != null) { + service.notifyInteraction(requestId, bundle); + } + } + + @GuardedBy("mLock") + @Nullable + private RemoteContentSuggestionsService getRemoteServiceLocked() { + if (mRemoteService == null) { + final String serviceName = getComponentNameLocked(); + if (serviceName == null) { + if (mMaster.verbose) { + Slog.v(TAG, "getRemoteServiceLocked(): not set"); + } + return null; + } + ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName); + + mRemoteService = new RemoteContentSuggestionsService(getContext(), + serviceComponent, mUserId, + new RemoteContentSuggestionsService.Callbacks() { + @Override + public void onServiceDied( + @NonNull RemoteContentSuggestionsService service) { + // TODO(b/120865921): properly implement + Slog.w(TAG, "remote content suggestions service died"); + } + }, mMaster.isBindInstantServiceAllowed(), mMaster.verbose); + } + + return mRemoteService; + } +} diff --git a/services/contentsuggestions/java/com/android/server/contentsuggestions/RemoteContentSuggestionsService.java b/services/contentsuggestions/java/com/android/server/contentsuggestions/RemoteContentSuggestionsService.java new file mode 100644 index 000000000000..bf48d7623255 --- /dev/null +++ b/services/contentsuggestions/java/com/android/server/contentsuggestions/RemoteContentSuggestionsService.java @@ -0,0 +1,97 @@ +/* + * 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. + */ + +package com.android.server.contentsuggestions; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.contentsuggestions.ClassificationsRequest; +import android.app.contentsuggestions.IClassificationsCallback; +import android.app.contentsuggestions.ISelectionsCallback; +import android.app.contentsuggestions.SelectionsRequest; +import android.content.ComponentName; +import android.content.Context; +import android.graphics.GraphicBuffer; +import android.os.Bundle; +import android.os.IBinder; +import android.service.contentsuggestions.ContentSuggestionsService; +import android.service.contentsuggestions.IContentSuggestionsService; +import android.text.format.DateUtils; + +import com.android.internal.infra.AbstractMultiplePendingRequestsRemoteService; + +/** + * Delegates calls from {@link ContentSuggestionsPerUserService} to the remote actual implementation + * of the suggestion selection and classification service. + */ +public class RemoteContentSuggestionsService extends + AbstractMultiplePendingRequestsRemoteService<RemoteContentSuggestionsService, + IContentSuggestionsService> { + + private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS; + + RemoteContentSuggestionsService(Context context, ComponentName serviceName, + int userId, Callbacks callbacks, + boolean bindInstantServiceAllowed, boolean verbose) { + super(context, ContentSuggestionsService.SERVICE_INTERFACE, serviceName, userId, callbacks, + bindInstantServiceAllowed, verbose, /* initialCapacity= */ 1); + } + + @Override + protected IContentSuggestionsService getServiceInterface(IBinder service) { + return IContentSuggestionsService.Stub.asInterface(service); + } + + @Override + protected long getTimeoutIdleBindMillis() { + return PERMANENT_BOUND_TIMEOUT_MS; + } + + @Override + protected long getRemoteRequestMillis() { + return TIMEOUT_REMOTE_REQUEST_MILLIS; + } + + void provideContextImage(int taskId, @Nullable GraphicBuffer contextImage, + @NonNull Bundle imageContextRequestExtras) { + scheduleAsyncRequest((s) -> s.provideContextImage(taskId, contextImage, + imageContextRequestExtras)); + } + + void suggestContentSelections( + @NonNull SelectionsRequest selectionsRequest, + @NonNull ISelectionsCallback selectionsCallback) { + scheduleAsyncRequest( + (s) -> s.suggestContentSelections(selectionsRequest, selectionsCallback)); + } + + void classifyContentSelections( + @NonNull ClassificationsRequest classificationsRequest, + @NonNull IClassificationsCallback callback) { + scheduleAsyncRequest((s) -> s.classifyContentSelections(classificationsRequest, callback)); + } + + void notifyInteraction(@NonNull String requestId, @NonNull Bundle bundle) { + scheduleAsyncRequest((s) -> s.notifyInteraction(requestId, bundle)); + } + + interface Callbacks + extends VultureCallback<RemoteContentSuggestionsService> { + // NOTE: so far we don't need to notify the callback implementation + // (ContentSuggestionsManager) of the request results (success, timeouts, etc..), so this + // callback interface is empty. + } +} diff --git a/services/core/java/com/android/server/IntentResolver.java b/services/core/java/com/android/server/IntentResolver.java index ea80ac1aecef..80fda19aa10c 100644 --- a/services/core/java/com/android/server/IntentResolver.java +++ b/services/core/java/com/android/server/IntentResolver.java @@ -16,32 +16,31 @@ package com.android.server; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - +import android.content.Intent; +import android.content.IntentFilter; import android.net.Uri; -import android.util.FastImmutableArraySet; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.FastImmutableArraySet; import android.util.Log; +import android.util.LogPrinter; import android.util.MutableInt; import android.util.PrintWriterPrinter; -import android.util.Slog; -import android.util.LogPrinter; import android.util.Printer; - -import android.content.Intent; -import android.content.IntentFilter; +import android.util.Slog; import android.util.proto.ProtoOutputStream; import com.android.internal.util.FastPrintWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + /** * {@hide} */ @@ -788,6 +787,7 @@ public abstract class IntentResolver<F extends IntentFilter, R extends Object> { + filter.hasCategory(Intent.CATEGORY_DEFAULT)); if (!defaultOnly || filter.hasCategory(Intent.CATEGORY_DEFAULT)) { final R oneResult = newResult(filter, match, userId); + if (debug) Slog.v(TAG, " Created result: " + oneResult); if (oneResult != null) { dest.add(oneResult); if (debug) { diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java index f27d37315ada..8adc416fda95 100644 --- a/services/core/java/com/android/server/PackageWatchdog.java +++ b/services/core/java/com/android/server/PackageWatchdog.java @@ -16,6 +16,7 @@ package com.android.server; +import android.annotation.Nullable; import android.content.Context; import android.os.Environment; import android.os.Handler; @@ -29,6 +30,7 @@ import android.util.Slog; import android.util.Xml; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.XmlUtils; @@ -46,8 +48,10 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Set; /** * Monitors the health of packages on the system and notifies interested observers when packages @@ -58,7 +62,7 @@ public class PackageWatchdog { // Duration to count package failures before it resets to 0 private static final int TRIGGER_DURATION_MS = 60000; // Number of package failures within the duration above before we notify observers - private static final int TRIGGER_FAILURE_COUNT = 5; + static final int TRIGGER_FAILURE_COUNT = 5; private static final int DB_VERSION = 1; private static final String TAG_PACKAGE_WATCHDOG = "package-watchdog"; private static final String TAG_PACKAGE = "package"; @@ -75,20 +79,13 @@ public class PackageWatchdog { // Handler to run package cleanup runnables private final Handler mTimerHandler; private final Handler mIoHandler; - // Contains (observer-name -> external-observer-handle) that have been registered during the - // current boot. - // It is populated when observers call #registerHealthObserver and it does not survive reboots. - @GuardedBy("mLock") - final ArrayMap<String, PackageHealthObserver> mRegisteredObservers = new ArrayMap<>(); - // Contains (observer-name -> internal-observer-handle) that have ever been registered from + // Contains (observer-name -> observer-handle) that have ever been registered from // previous boots. Observers with all packages expired are periodically pruned. // It is saved to disk on system shutdown and repouplated on startup so it survives reboots. @GuardedBy("mLock") - final ArrayMap<String, ObserverInternal> mAllObservers = new ArrayMap<>(); + private final ArrayMap<String, ObserverInternal> mAllObservers = new ArrayMap<>(); // File containing the XML data of monitored packages /data/system/package-watchdog.xml - private final AtomicFile mPolicyFile = - new AtomicFile(new File(new File(Environment.getDataDirectory(), "system"), - "package-watchdog.xml")); + private final AtomicFile mPolicyFile; // Runnable to prune monitored packages that have expired private final Runnable mPackageCleanup; // Last SystemClock#uptimeMillis a package clean up was executed. @@ -98,14 +95,32 @@ public class PackageWatchdog { // 0 if mPackageCleanup not running. private long mDurationAtLastReschedule; + // TODO(zezeozue): Remove redundant context param private PackageWatchdog(Context context) { mContext = context; + mPolicyFile = new AtomicFile(new File(new File(Environment.getDataDirectory(), "system"), + "package-watchdog.xml")); mTimerHandler = new Handler(Looper.myLooper()); mIoHandler = BackgroundThread.getHandler(); mPackageCleanup = this::rescheduleCleanup; loadFromFile(); } + /** + * Creates a PackageWatchdog for testing that uses the same {@code looper} for all handlers + * and creates package-watchdog.xml in an apps data directory. + */ + @VisibleForTesting + PackageWatchdog(Context context, Looper looper) { + mContext = context; + mPolicyFile = new AtomicFile(new File(context.getFilesDir(), "package-watchdog.xml")); + mTimerHandler = new Handler(looper); + mIoHandler = mTimerHandler; + mPackageCleanup = this::rescheduleCleanup; + loadFromFile(); + } + + /** Creates or gets singleton instance of PackageWatchdog. */ public static PackageWatchdog getInstance(Context context) { synchronized (PackageWatchdog.class) { @@ -124,7 +139,10 @@ public class PackageWatchdog { */ public void registerHealthObserver(PackageHealthObserver observer) { synchronized (mLock) { - mRegisteredObservers.put(observer.getName(), observer); + ObserverInternal internalObserver = mAllObservers.get(observer.getName()); + if (internalObserver != null) { + internalObserver.mRegisteredObserver = observer; + } if (mDurationAtLastReschedule == 0) { // Nothing running, schedule rescheduleCleanup(); @@ -143,7 +161,7 @@ public class PackageWatchdog { * or {@code durationMs} is less than 1 */ public void startObservingHealth(PackageHealthObserver observer, List<String> packageNames, - int durationMs) { + long durationMs) { if (packageNames.isEmpty() || durationMs < 1) { throw new IllegalArgumentException("Observation not started, no packages specified" + "or invalid duration"); @@ -180,11 +198,32 @@ public class PackageWatchdog { public void unregisterHealthObserver(PackageHealthObserver observer) { synchronized (mLock) { mAllObservers.remove(observer.getName()); - mRegisteredObservers.remove(observer.getName()); } saveToFileAsync(); } + /** + * Returns packages observed by {@code observer} + * + * @return an empty set if {@code observer} has some packages observerd from a previous boot + * but has not registered itself in the current boot to receive notifications. Returns null + * if there are no active packages monitored from any boot. + */ + @Nullable + public Set<String> getPackages(PackageHealthObserver observer) { + synchronized (mLock) { + for (int i = 0; i < mAllObservers.size(); i++) { + if (observer.getName().equals(mAllObservers.keyAt(i))) { + if (observer.equals(mAllObservers.valueAt(i).mRegisteredObserver)) { + return mAllObservers.valueAt(i).mPackages.keySet(); + } + return Collections.emptySet(); + } + } + } + return null; + } + // TODO(zezeozue:) Accept current versionCodes of failing packages? /** * Called when a process fails either due to a crash or ANR. @@ -198,33 +237,35 @@ public class PackageWatchdog { public void onPackageFailure(String[] packages) { ArrayMap<String, List<PackageHealthObserver>> packagesToReport = new ArrayMap<>(); synchronized (mLock) { - if (mRegisteredObservers.isEmpty()) { + if (mAllObservers.isEmpty()) { return; } for (int pIndex = 0; pIndex < packages.length; pIndex++) { + // Observers interested in receiving packageName failures + List<PackageHealthObserver> observersToNotify = new ArrayList<>(); for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) { - // Observers interested in receiving packageName failures - List<PackageHealthObserver> observersToNotify = new ArrayList<>(); - PackageHealthObserver activeObserver = - mRegisteredObservers.get(mAllObservers.valueAt(oIndex).mName); - if (activeObserver != null) { - observersToNotify.add(activeObserver); - } - - // Save interested observers and notify them outside the lock - if (!observersToNotify.isEmpty()) { - packagesToReport.put(packages[pIndex], observersToNotify); + PackageHealthObserver registeredObserver = + mAllObservers.valueAt(oIndex).mRegisteredObserver; + if (registeredObserver != null) { + observersToNotify.add(registeredObserver); } } + // Save interested observers and notify them outside the lock + if (!observersToNotify.isEmpty()) { + packagesToReport.put(packages[pIndex], observersToNotify); + } } } // Notify observers for (int pIndex = 0; pIndex < packagesToReport.size(); pIndex++) { List<PackageHealthObserver> observers = packagesToReport.valueAt(pIndex); + String packageName = packages[pIndex]; for (int oIndex = 0; oIndex < observers.size(); oIndex++) { - if (observers.get(oIndex).onHealthCheckFailed(packages[pIndex])) { + PackageHealthObserver observer = observers.get(oIndex); + if (mAllObservers.get(observer.getName()).onPackageFailure(packageName) + && observer.onHealthCheckFailed(packageName)) { // Observer has handled, do not notify others break; } @@ -275,10 +316,12 @@ public class PackageWatchdog { // O if mPackageCleanup not running long elapsedDurationMs = mUptimeAtLastRescheduleMs == 0 ? 0 : uptimeMs - mUptimeAtLastRescheduleMs; - // O if mPackageCleanup not running + // Less than O if mPackageCleanup unexpectedly didn't run yet even though + // and we are past the last duration scheduled to run long remainingDurationMs = mDurationAtLastReschedule - elapsedDurationMs; - - if (mUptimeAtLastRescheduleMs == 0 || nextDurationToScheduleMs < remainingDurationMs) { + if (mUptimeAtLastRescheduleMs == 0 + || remainingDurationMs <= 0 + || nextDurationToScheduleMs < remainingDurationMs) { // First schedule or an earlier reschedule pruneObservers(elapsedDurationMs); mTimerHandler.removeCallbacks(mPackageCleanup); @@ -305,6 +348,7 @@ public class PackageWatchdog { } } Slog.v(TAG, "Earliest package time is " + shortestDurationMs); + return shortestDurationMs; } @@ -409,6 +453,8 @@ public class PackageWatchdog { static class ObserverInternal { public final String mName; public final ArrayMap<String, MonitoredPackage> mPackages; + @Nullable + public PackageHealthObserver mRegisteredObserver; ObserverInternal(String name, List<MonitoredPackage> packages) { mName = name; diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING new file mode 100644 index 000000000000..93e1dd36797a --- /dev/null +++ b/services/core/java/com/android/server/TEST_MAPPING @@ -0,0 +1,15 @@ +{ + "presubmit": [ + { + "name": "FrameworksMockingServicesTests", + "options": [ + { + "include-annotation": "android.platform.test.annotations.Presubmit" + }, + { + "exclude-annotation": "android.support.test.filters.FlakyTest" + } + ] + } + ] +} diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index ed39d8302bf7..8ca4193a064f 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -1654,6 +1654,10 @@ public final class ActiveServices { } } + if ((flags & Context.BIND_RESTRICT_ASSOCIATIONS) != 0) { + mAm.requireAllowedAssociationsLocked(s.appInfo.packageName); + } + mAm.startAssociationLocked(callerApp.uid, callerApp.processName, callerApp.getCurProcState(), s.appInfo.uid, s.appInfo.longVersionCode, s.instanceName, s.processName); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 7f67230b7bad..bb239acb5c60 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -329,7 +329,7 @@ import com.android.internal.util.Preconditions; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.TriFunction; import com.android.server.AlarmManagerInternal; -import com.android.server.AppOpsService; +import com.android.server.appop.AppOpsService; import com.android.server.AttributeCache; import com.android.server.DeviceIdleController; import com.android.server.DisplayThread; @@ -2431,6 +2431,20 @@ public class ActivityManagerService extends IActivityManager.Stub } /** + * Ensures that the given package name has an explicit set of allowed associations. + * If it does not, give it an empty set. + */ + void requireAllowedAssociationsLocked(String packageName) { + if (mAllowedAssociations == null) { + mAllowedAssociations = new ArrayMap<>( + SystemConfig.getInstance().getAllowedAssociations()); + } + if (mAllowedAssociations.get(packageName) == null) { + mAllowedAssociations.put(packageName, new ArraySet<>()); + } + } + + /** * Returns true if the package {@code pkg1} running under user handle {@code uid1} is * allowed association with the package {@code pkg2} running under user handle {@code uid2}. * <p> If either of the packages are running as part of the core system, then the @@ -2438,7 +2452,8 @@ public class ActivityManagerService extends IActivityManager.Stub */ boolean validateAssociationAllowedLocked(String pkg1, int uid1, String pkg2, int uid2) { if (mAllowedAssociations == null) { - mAllowedAssociations = SystemConfig.getInstance().getAllowedAssociations(); + mAllowedAssociations = new ArrayMap<>( + SystemConfig.getInstance().getAllowedAssociations()); } // Interactions with the system uid are always allowed, since that is the core system // that everyone needs to be able to interact with. Also allow reflexive associations diff --git a/services/core/java/com/android/server/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 466fb4e71496..7ede6dcd41bd 100644 --- a/services/core/java/com/android/server/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.android.server; +package com.android.server.appop; import static android.app.AppOpsManager.OP_PLAY_AUDIO; +import static android.app.AppOpsManager.OP_NONE; import static android.app.AppOpsManager.UID_STATE_BACKGROUND; import static android.app.AppOpsManager.UID_STATE_CACHED; import static android.app.AppOpsManager.UID_STATE_FOREGROUND; @@ -35,8 +36,7 @@ import android.app.ActivityManager; import android.app.ActivityThread; import android.app.AppGlobals; import android.app.AppOpsManager; -import android.app.AppOpsManager.HistoricalOpEntry; -import android.app.AppOpsManager.HistoricalPackageOps; +import android.app.AppOpsManager.HistoricalOps; import android.app.AppOpsManagerInternal; import android.app.AppOpsManagerInternal.CheckOpsDelegate; import android.content.BroadcastReceiver; @@ -48,7 +48,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; -import android.content.pm.ParceledListSlice; import android.content.pm.UserInfo; import android.database.ContentObserver; import android.media.AudioAttributes; @@ -59,6 +58,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Process; +import android.os.RemoteCallback; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; @@ -95,6 +95,8 @@ import com.android.internal.util.Preconditions; import com.android.internal.util.XmlUtils; import com.android.internal.util.function.pooled.PooledLambda; +import com.android.server.LocalServices; +import com.android.server.LockGuard; import libcore.util.EmptyArray; import org.xmlpull.v1.XmlPullParser; @@ -213,6 +215,8 @@ public class AppOpsService extends IAppOpsService.Stub { @VisibleForTesting final SparseArray<UidState> mUidStates = new SparseArray<>(); + private final HistoricalRegistry mHistoricalRegistry = new HistoricalRegistry(this); + long mLastRealtime; /* @@ -654,6 +658,7 @@ public class AppOpsService extends IAppOpsService.Stub { public void systemReady() { mConstants.startMonitoring(mContext.getContentResolver()); + mHistoricalRegistry.systemReady(mContext.getContentResolver()); synchronized (this) { boolean changed = false; @@ -1012,113 +1017,99 @@ public class AppOpsService extends IAppOpsService.Stub { } @Override - public @Nullable ParceledListSlice getAllHistoricalPackagesOps(@Nullable String[] opNames, - long beginTimeMillis, long endTimeMillis) { + public void getHistoricalOps(int uid, @NonNull String packageName, + @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis, + @NonNull RemoteCallback callback) { + Preconditions.checkArgument(uid == Process.INVALID_UID || uid >= 0, + "uid must be " + Process.INVALID_UID + " or non negative"); Preconditions.checkArgument(beginTimeMillis >= 0 && beginTimeMillis < endTimeMillis, "beginTimeMillis must be non negative and lesser than endTimeMillis"); + Preconditions.checkNotNull(callback, "callback cannot be null"); + checkValidOpsOrNull(opNames); mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS, - Binder.getCallingPid(), Binder.getCallingUid(), "getAllHistoricalPackagesOps"); + Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalOps"); - ArrayList<HistoricalPackageOps> historicalPackageOpsList = null; + if (mHistoricalRegistry.getMode() == AppOpsManager.HISTORICAL_MODE_DISABLED) { + // TODO (bug:122218838): Remove once the feature fully enabled. + getHistoricalPackagesOpsCompat(uid, packageName, opNames, beginTimeMillis, + endTimeMillis, callback); + } else { + // Must not hold the appops lock + mHistoricalRegistry.getHistoricalOps(uid, packageName, opNames, + beginTimeMillis, endTimeMillis, callback); + } + } - final int uidStateCount = mUidStates.size(); - for (int i = 0; i < uidStateCount; i++) { - final UidState uidState = mUidStates.valueAt(i); - if (uidState.pkgOps == null || uidState.pkgOps.isEmpty()) { - continue; + private void getHistoricalPackagesOpsCompat(int uid, @NonNull String packageName, + @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis, + @NonNull RemoteCallback callback) { + synchronized (AppOpsService.this) { + final HistoricalOps ops = new HistoricalOps(beginTimeMillis, endTimeMillis); + if (opNames == null) { + opNames = AppOpsManager.getOpStrs(); } - final ArrayMap<String, Ops> packages = uidState.pkgOps; - final int packageCount = packages.size(); - for (int j = 0; j < packageCount; j++) { - final Ops pkgOps = packages.valueAt(j); - final AppOpsManager.HistoricalPackageOps historicalPackageOps = - createHistoricalPackageOps(uidState.uid, pkgOps, opNames, - beginTimeMillis, endTimeMillis); - if (historicalPackageOps != null) { - if (historicalPackageOpsList == null) { - historicalPackageOpsList = new ArrayList<>(); + final int uidStateCount = mUidStates.size(); + for (int uidIdx = 0; uidIdx < uidStateCount; uidIdx++) { + final UidState uidState = mUidStates.valueAt(uidIdx); + if (uidState.pkgOps == null || uidState.pkgOps.isEmpty() + || (uid != Process.INVALID_UID && uid != uidState.uid)) { + continue; + } + final ArrayMap<String, Ops> packages = uidState.pkgOps; + final int packageCount = packages.size(); + for (int pkgIdx = 0; pkgIdx < packageCount; pkgIdx++) { + final Ops pkgOps = packages.valueAt(pkgIdx); + if (packageName != null && !packageName.equals(pkgOps.packageName)) { + continue; + } + final int opCount = opNames.length; + for (int opIdx = 0; opIdx < opCount; opIdx++) { + final String opName = opNames[opIdx]; + if (!ArrayUtils.contains(opNames, opName)) { + continue; + } + final int opCode = AppOpsManager.strOpToOp(opName); + final Op op = pkgOps.get(opCode); + if (op == null) { + continue; + } + final int stateCount = AppOpsManager._NUM_UID_STATE; + for (int stateIdx = 0; stateIdx < stateCount; stateIdx++) { + if (op.rejectTime[stateIdx] != 0) { + ops.increaseRejectCount(opCode, uidState.uid, + pkgOps.packageName, stateIdx, 1); + } else if (op.time[stateIdx] != 0) { + ops.increaseAccessCount(opCode, uidState.uid, + pkgOps.packageName, stateIdx, 1); + } + } } - historicalPackageOpsList.add(historicalPackageOps); } } + final Bundle payload = new Bundle(); + payload.putParcelable(AppOpsManager.KEY_HISTORICAL_OPS, ops); + callback.sendResult(payload); } - - if (historicalPackageOpsList == null) { - return null; - } - - return new ParceledListSlice<>(historicalPackageOpsList); - } - - private static @Nullable HistoricalPackageOps createHistoricalPackageOps(int uid, - @Nullable Ops pkgOps, @Nullable String[] opNames, long beginTimeMillis, - long endTimeMillis) { - // TODO: Implement historical data collection - if (pkgOps == null) { - return null; - } - - final HistoricalPackageOps historicalPackageOps = new HistoricalPackageOps(uid, - pkgOps.packageName); - - if (opNames == null) { - opNames = AppOpsManager.getOpStrs(); - } - for (String opName : opNames) { - addHistoricOpEntry(AppOpsManager.strOpToOp(opName), pkgOps, historicalPackageOps); - } - - return historicalPackageOps; } @Override - public @Nullable HistoricalPackageOps getHistoricalPackagesOps(int uid, - @NonNull String packageName, @Nullable String[] opNames, - long beginTimeMillis, long endTimeMillis) { - Preconditions.checkNotNull(packageName, - "packageName cannot be null"); + public void getHistoricalOpsFromDiskRaw(int uid, @NonNull String packageName, + @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis, + @NonNull RemoteCallback callback) { + Preconditions.checkArgument(uid == Process.INVALID_UID || uid >= 0, + "uid must be " + Process.INVALID_UID + " or non negative"); Preconditions.checkArgument(beginTimeMillis >= 0 && beginTimeMillis < endTimeMillis, "beginTimeMillis must be non negative and lesser than endTimeMillis"); + Preconditions.checkNotNull(callback, "callback cannot be null"); + checkValidOpsOrNull(opNames); mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS, - Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalPackagesOps"); + Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalOps"); - final String resolvedPackageName = resolvePackageName(uid, packageName); - if (resolvedPackageName == null) { - return null; - } - - // TODO: Implement historical data collection - final Ops pkgOps = getOpsRawLocked(uid, resolvedPackageName, false /* edit */, - false /* uidMismatchExpected */); - return createHistoricalPackageOps(uid, pkgOps, opNames, beginTimeMillis, endTimeMillis); - } - - private static void addHistoricOpEntry(int opCode, @NonNull Ops ops, - @NonNull HistoricalPackageOps outHistoricalPackageOps) { - final Op op = ops.get(opCode); - if (op == null) { - return; - } - - final HistoricalOpEntry historicalOpEntry = new HistoricalOpEntry(opCode); - - // TODO: Keep per UID state duration - for (int uidState = 0; uidState < AppOpsManager._NUM_UID_STATE; uidState++) { - final int acceptCount; - final int rejectCount; - if (op.rejectTime[uidState] == 0) { - acceptCount = 1; - rejectCount = 0; - } else { - acceptCount = 0; - rejectCount = 1; - } - historicalOpEntry.addEntry(uidState, acceptCount, rejectCount, 0); - } - - outHistoricalPackageOps.addEntry(historicalOpEntry); + // Must not hold the appops lock + mHistoricalRegistry.getHistoricalOpsFromDiskRaw(uid, packageName, opNames, + beginTimeMillis, endTimeMillis, callback); } @Override @@ -1884,6 +1875,8 @@ public class AppOpsService extends IAppOpsService.Stub { + packageName); op.rejectTime[uidState.state] = System.currentTimeMillis(); scheduleOpNotedIfNeededLocked(code, uid, packageName, uidMode); + mHistoricalRegistry.incrementOpRejected(op.op, uid, packageName, + uidState.state); return uidMode; } } else { @@ -1895,12 +1888,16 @@ public class AppOpsService extends IAppOpsService.Stub { + packageName); op.rejectTime[uidState.state] = System.currentTimeMillis(); scheduleOpNotedIfNeededLocked(op.op, uid, packageName, mode); + mHistoricalRegistry.incrementOpRejected(op.op, uid, packageName, + uidState.state); return mode; } } if (DEBUG) Slog.d(TAG, "noteOperation: allowing code " + code + " uid " + uid + " package " + packageName); op.time[uidState.state] = System.currentTimeMillis(); + mHistoricalRegistry.incrementOpAccessedCount(op.op, uid, packageName, + uidState.state); op.rejectTime[uidState.state] = 0; op.proxyUid = proxyUid; op.proxyPackageName = proxyPackageName; @@ -2035,6 +2032,8 @@ public class AppOpsService extends IAppOpsService.Stub { + switchCode + " (" + code + ") uid " + uid + " package " + resolvedPackageName); op.rejectTime[uidState.state] = System.currentTimeMillis(); + mHistoricalRegistry.incrementOpRejected(op.op, uid, packageName, + uidState.state); return uidMode; } } else { @@ -2046,6 +2045,8 @@ public class AppOpsService extends IAppOpsService.Stub { + switchCode + " (" + code + ") uid " + uid + " package " + resolvedPackageName); op.rejectTime[uidState.state] = System.currentTimeMillis(); + mHistoricalRegistry.incrementOpRejected(op.op, uid, packageName, + uidState.state); return mode; } } @@ -2054,6 +2055,8 @@ public class AppOpsService extends IAppOpsService.Stub { if (op.startNesting == 0) { op.startRealtime = SystemClock.elapsedRealtime(); op.time[uidState.state] = System.currentTimeMillis(); + mHistoricalRegistry.incrementOpAccessedCount(op.op, uid, packageName, + uidState.state); op.rejectTime[uidState.state] = 0; op.duration = -1; scheduleOpActiveChangedIfNeededLocked(code, uid, packageName, true); @@ -2216,6 +2219,8 @@ public class AppOpsService extends IAppOpsService.Stub { if (op.startNesting <= 1 || finishNested) { if (op.startNesting == 1 || finishNested) { op.duration = (int)(SystemClock.elapsedRealtime() - op.startRealtime); + mHistoricalRegistry.increaseOpAccessDuration(op.op, op.uid, op.packageName, + op.uidState.state, op.duration); op.time[op.uidState.state] = System.currentTimeMillis(); } else { Slog.w(TAG, "Finishing op nesting under-run: uid " + op.uid + " pkg " @@ -2344,7 +2349,7 @@ public class AppOpsService extends IAppOpsService.Stub { ApplicationInfo appInfo = ActivityThread.getPackageManager() .getApplicationInfo(packageName, PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, UserHandle.getUserId(uid)); if (appInfo != null) { pkgUid = appInfo.uid; @@ -3427,9 +3432,9 @@ public class AppOpsService extends IAppOpsService.Stub { protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) return; - int dumpOp = -1; + int dumpOp = OP_NONE; String dumpPackage = null; - int dumpUid = -1; + int dumpUid = Process.INVALID_UID; int dumpMode = -1; boolean dumpWatchers = false; @@ -3748,8 +3753,8 @@ public class AppOpsService extends IAppOpsService.Stub { } if (pkgOps != null) { for (int pkgi = 0; - (!hasOp || !hasPackage || !hasMode) && pkgi < pkgOps.size(); - pkgi++) { + (!hasOp || !hasPackage || !hasMode) && pkgi < pkgOps.size(); + pkgi++) { Ops ops = pkgOps.valueAt(pkgi); if (!hasOp && ops != null && ops.indexOfKey(dumpOp) >= 0) { hasOp = true; @@ -3973,6 +3978,9 @@ public class AppOpsService extends IAppOpsService.Stub { } } } + + // Must not hold the appops lock + mHistoricalRegistry.dump(" ", pw, dumpUid, dumpPackage, dumpOp); } private static final class Restriction { @@ -4093,6 +4101,47 @@ public class AppOpsService extends IAppOpsService.Stub { return false; } + @Override + public void setHistoryParameters(@AppOpsManager.HistoricalMode int mode, + long baseSnapshotInterval, int compressionStep) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, + "setHistoryParameters"); + // Must not hold the appops lock + mHistoricalRegistry.setHistoryParameters(mode, baseSnapshotInterval, compressionStep); + } + + @Override + public void offsetHistory(long offsetMillis) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, + "offsetHistory"); + // Must not hold the appops lock + mHistoricalRegistry.offsetHistory(offsetMillis); + } + + @Override + public void addHistoricalOps(HistoricalOps ops) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, + "addHistoricalOps"); + // Must not hold the appops lock + mHistoricalRegistry.addHistoricalOps(ops); + } + + @Override + public void resetHistoryParameters() { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, + "resetHistoryParameters"); + // Must not hold the appops lock + mHistoricalRegistry.resetHistoryParameters(); + } + + @Override + public void clearHistory() { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, + "clearHistory"); + // Must not hold the appops lock + mHistoricalRegistry.clearHistory(); + } + private void removeUidsForUserLocked(int userHandle) { for (int i = mUidStates.size() - 1; i >= 0; --i) { final int uid = mUidStates.keyAt(i); @@ -4163,6 +4212,16 @@ public class AppOpsService extends IAppOpsService.Stub { return packageNames; } + private static void checkValidOpsOrNull(String[] opNames) { + if (opNames != null) { + for (String opName : opNames) { + if (AppOpsManager.strOpToOp(opName) == AppOpsManager.OP_NONE) { + throw new IllegalArgumentException("Unknown op: " + opName); + } + } + } + } + private final class ClientRestrictionState implements DeathRecipient { private final IBinder token; SparseArray<boolean[]> perUserRestrictions; diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java new file mode 100644 index 000000000000..8d7811fc8e78 --- /dev/null +++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java @@ -0,0 +1,1495 @@ +/* + * 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. + */ +package com.android.server.appop; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AppOpsManager; +import android.app.AppOpsManager.HistoricalMode; +import android.app.AppOpsManager.HistoricalOp; +import android.app.AppOpsManager.HistoricalOps; +import android.app.AppOpsManager.HistoricalPackageOps; +import android.app.AppOpsManager.HistoricalUidOps; +import android.app.AppOpsManager.UidState; +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Message; +import android.os.Process; +import android.os.RemoteCallback; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.ArraySet; +import android.util.Slog; +import android.util.TimeUtils; +import android.util.Xml; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.os.AtomicDirectory; +import com.android.internal.os.BackgroundThread; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.XmlUtils; +import com.android.internal.util.function.pooled.PooledLambda; +import com.android.server.FgThread; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * This class managers historical app op state. This includes reading, persistence, + * accounting, querying. + * <p> + * The history is kept forever in multiple files. Each file time contains the + * relative offset from the current time which time is encoded in the file name. + * The files contain historical app op state snapshots which have times that + * are relative to the time of the container file. + * + * The data in the files are stored in a logarithmic fashion where where every + * subsequent file would contain data for ten times longer interval with ten + * times more time distance between snapshots. Hence, the more time passes + * the lesser the fidelity. + * <p> + * For example, the first file would contain data for 1 days with snapshots + * every 0.1 days, the next file would contain data for the period 1 to 10 + * days with snapshots every 1 days, and so on. + * <p> + * THREADING AND LOCKING: Reported ops must be processed as quickly as possible. + * We keep ops pending to be persisted in memory and write to disk on a background + * thread. Hence, methods that report op changes are locking only the in memory + * state guarded by the mInMemoryLock which happens to be the app ops service lock + * avoiding a lock addition on the critical path. When a query comes we need to + * evaluate it based off both in memory and on disk state. This means they need to + * be frozen with respect to each other and not change from the querying caller's + * perspective. To achieve this we add a dedicated mOnDiskLock to guard the on + * disk state. To have fast critical path we need to limit the locking of the + * mInMemoryLock, thus for operations that touch in memory and on disk state one + * must grab first the mOnDiskLock and then the mInMemoryLock and limit the + * in memory lock to extraction of relevant data. Locking order is critical to + * avoid deadlocks. The convention is that xxxDLocked suffix means the method + * must be called with the mOnDiskLock lock, xxxMLocked suffix means the method + * must be called with the mInMemoryLock, xxxDMLocked suffix means the method + * must be called with the mOnDiskLock and mInMemoryLock locks acquired in that + * exact order. + */ +// TODO (bug:122218838): Make sure we handle start of epoch time +// TODO (bug:122218838): Validate changed time is handled correctly +final class HistoricalRegistry { + private static final boolean DEBUG = false; + + private static final String LOG_TAG = HistoricalRegistry.class.getSimpleName(); + + private static final String PARAMETER_DELIMITER = ","; + private static final String PARAMETER_ASSIGNMENT = "="; + + @GuardedBy("mLock") + private @NonNull LinkedList<HistoricalOps> mPendingWrites = new LinkedList<>(); + + // Lock for read/write access to on disk state + private final Object mOnDiskLock = new Object(); + + //Lock for read/write access to in memory state + private final @NonNull Object mInMemoryLock; + + private static final int MSG_WRITE_PENDING_HISTORY = 1; + + // See mMode + private static final int DEFAULT_MODE = AppOpsManager.HISTORICAL_MODE_DISABLED; + + // See mBaseSnapshotInterval + private static final long DEFAULT_SNAPSHOT_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(15); + + // See mIntervalCompressionMultiplier + private static final long DEFAULT_COMPRESSION_STEP = 10; + + /** + * Whether history is enabled. + */ + @GuardedBy("mInMemoryLock") + private int mMode = AppOpsManager.HISTORICAL_MODE_DISABLED; + + /** + * This granularity has been chosen to allow clean delineation for intervals + * humans understand, 15 min, 60, min, a day, a week, a month (30 days). + */ + @GuardedBy("mInMemoryLock") + private long mBaseSnapshotInterval = DEFAULT_SNAPSHOT_INTERVAL_MILLIS; + + /** + * The compression between steps. Each subsequent step is this much longer + * in terms of duration and each snapshot is this much more apart from the + * previous step. + */ + @GuardedBy("mInMemoryLock") + private long mIntervalCompressionMultiplier = DEFAULT_COMPRESSION_STEP; + + // The current ops to which to add statistics. + @GuardedBy("mInMemoryLock") + private @Nullable HistoricalOps mCurrentHistoricalOps; + + // The time we should write the next snapshot. + @GuardedBy("mInMemoryLock") + private long mNextPersistDueTimeMillis; + + // How much to offset the history on the next write. + @GuardedBy("mInMemoryLock") + private long mPendingHistoryOffsetMillis; + + // Object managing persistence (read/write) + @GuardedBy("mOnDiskLock") + private Persistence mPersistence = new Persistence(mBaseSnapshotInterval, + mIntervalCompressionMultiplier); + + HistoricalRegistry(@NonNull Object lock) { + mInMemoryLock = lock; + if (mMode != AppOpsManager.HISTORICAL_MODE_DISABLED) { + synchronized (mInMemoryLock) { + // When starting always adjust history to now. + mPendingHistoryOffsetMillis = System.currentTimeMillis() + - mPersistence.getLastPersistTimeMillisDLocked(); + } + } + } + + void systemReady(@NonNull ContentResolver resolver) { + updateParametersFromSetting(resolver); + final Uri uri = Settings.Global.getUriFor(Settings.Global.APPOP_HISTORY_PARAMETERS); + resolver.registerContentObserver(uri, false, new ContentObserver( + FgThread.getHandler()) { + @Override + public void onChange(boolean selfChange) { + updateParametersFromSetting(resolver); + } + }); + } + + private void updateParametersFromSetting(@NonNull ContentResolver resolver) { + final String setting = Settings.Global.getString(resolver, + Settings.Global.APPOP_HISTORY_PARAMETERS); + if (setting == null) { + return; + } + String modeValue = null; + String baseSnapshotIntervalValue = null; + String intervalMultiplierValue = null; + final String[] parameters = setting.split(PARAMETER_DELIMITER); + for (String parameter : parameters) { + final String[] parts = parameter.split(PARAMETER_ASSIGNMENT); + if (parts.length == 2) { + final String key = parts[0].trim(); + switch (key) { + case Settings.Global.APPOP_HISTORY_MODE: { + modeValue = parts[1].trim(); + } break; + case Settings.Global.APPOP_HISTORY_BASE_INTERVAL_MILLIS: { + baseSnapshotIntervalValue = parts[1].trim(); + } break; + case Settings.Global.APPOP_HISTORY_INTERVAL_MULTIPLIER: { + intervalMultiplierValue = parts[1].trim(); + } break; + default: { + Slog.w(LOG_TAG, "Unknown parameter: " + parameter); + } + } + } + } + if (modeValue != null && baseSnapshotIntervalValue != null + && intervalMultiplierValue != null) { + try { + final int mode = AppOpsManager.parseHistoricalMode(modeValue); + final long baseSnapshotInterval = Long.parseLong(baseSnapshotIntervalValue); + final int intervalCompressionMultiplier = Integer.parseInt(intervalMultiplierValue); + setHistoryParameters(mode, baseSnapshotInterval,intervalCompressionMultiplier); + return; + } catch (NumberFormatException ignored) {} + } + Slog.w(LOG_TAG, "Bad value for" + Settings.Global.APPOP_HISTORY_PARAMETERS + + "=" + setting + " resetting!"); + } + + void dump(String prefix, PrintWriter pw, int filterUid, + String filterPackage, int filterOp) { + synchronized (mOnDiskLock) { + synchronized (mInMemoryLock) { + pw.println(); + pw.print(prefix); + pw.print("History:"); + + pw.print(" mode="); + pw.println(AppOpsManager.historicalModeToString(mMode)); + + final StringDumpVisitor visitor = new StringDumpVisitor(prefix + " ", + pw, filterUid, filterPackage, filterOp); + final long nowMillis = System.currentTimeMillis(); + + // Dump in memory state first + final HistoricalOps currentOps = getUpdatedPendingHistoricalOpsMLocked( + nowMillis); + makeRelativeToEpochStart(currentOps, nowMillis); + currentOps.accept(visitor); + + final List<HistoricalOps> ops = mPersistence.readHistoryDLocked(); + if (ops != null) { + // TODO (bug:122218838): Make sure this is properly dumped + final long remainingToFillBatchMillis = mNextPersistDueTimeMillis + - nowMillis - mBaseSnapshotInterval; + final int opCount = ops.size(); + for (int i = 0; i < opCount; i++) { + final HistoricalOps op = ops.get(i); + op.offsetBeginAndEndTime(remainingToFillBatchMillis); + makeRelativeToEpochStart(op, nowMillis); + op.accept(visitor); + } + } else { + pw.println(" Empty"); + } + } + } + } + + @HistoricalMode int getMode() { + synchronized (mInMemoryLock) { + return mMode; + } + } + + @Nullable void getHistoricalOpsFromDiskRaw(int uid, @NonNull String packageName, + @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis, + @NonNull RemoteCallback callback) { + final HistoricalOps result = new HistoricalOps(beginTimeMillis, endTimeMillis); + mPersistence.collectHistoricalOpsDLocked(result, uid, packageName, opNames, + beginTimeMillis, endTimeMillis); + final Bundle payload = new Bundle(); + payload.putParcelable(AppOpsManager.KEY_HISTORICAL_OPS, result); + callback.sendResult(payload); + } + + @Nullable void getHistoricalOps(int uid, @NonNull String packageName, + @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis, + @NonNull RemoteCallback callback) { + final long currentTimeMillis = System.currentTimeMillis(); + if (endTimeMillis == Long.MAX_VALUE) { + endTimeMillis = currentTimeMillis; + } + + // Argument times are based off epoch start while our internal store is + // based off now, so take this into account. + final long inMemoryAdjBeginTimeMillis = Math.max(currentTimeMillis - endTimeMillis, 0); + final long inMemoryAdjEndTimeMillis = Math.max(currentTimeMillis - beginTimeMillis, 0); + final HistoricalOps result = new HistoricalOps(inMemoryAdjBeginTimeMillis, + inMemoryAdjEndTimeMillis); + + synchronized (mOnDiskLock) { + final List<HistoricalOps> pendingWrites; + final HistoricalOps currentOps; + synchronized (mInMemoryLock) { + currentOps = getUpdatedPendingHistoricalOpsMLocked(currentTimeMillis); + if (!(inMemoryAdjBeginTimeMillis >= currentOps.getEndTimeMillis() + || inMemoryAdjEndTimeMillis <= currentOps.getBeginTimeMillis())) { + // Some of the current batch falls into the query, so extract that. + final HistoricalOps currentOpsCopy = new HistoricalOps(currentOps); + currentOpsCopy.filter(uid, packageName, opNames, inMemoryAdjBeginTimeMillis, + inMemoryAdjEndTimeMillis); + result.merge(currentOpsCopy); + } + pendingWrites = new ArrayList<>(mPendingWrites); + mPendingWrites.clear(); + } + + // If the query was only for in-memory state - done. + if (inMemoryAdjEndTimeMillis > currentOps.getEndTimeMillis()) { + // If there is a write in flight we need to force it now + persistPendingHistory(pendingWrites); + // Collect persisted state. + final long onDiskAndInMemoryOffsetMillis = currentTimeMillis + - mNextPersistDueTimeMillis + mBaseSnapshotInterval; + final long onDiskAdjBeginTimeMillis = Math.max(inMemoryAdjBeginTimeMillis + - onDiskAndInMemoryOffsetMillis, 0); + final long onDiskAdjEndTimeMillis = Math.max(inMemoryAdjEndTimeMillis + - onDiskAndInMemoryOffsetMillis, 0); + mPersistence.collectHistoricalOpsDLocked(result, uid, packageName, opNames, + onDiskAdjBeginTimeMillis, onDiskAdjEndTimeMillis); + } + + // Rebase the result time to be since epoch. + result.setBeginAndEndTime(beginTimeMillis, endTimeMillis); + + // Send back the result. + final Bundle payload = new Bundle(); + payload.putParcelable(AppOpsManager.KEY_HISTORICAL_OPS, result); + callback.sendResult(payload); + } + } + + void incrementOpAccessedCount(int op, int uid, @NonNull String packageName, + @UidState int uidState) { + synchronized (mInMemoryLock) { + if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) { + getUpdatedPendingHistoricalOpsMLocked(System.currentTimeMillis()) + .increaseAccessCount(op, uid, packageName, uidState, 1); + + } + } + } + + void incrementOpRejected(int op, int uid, @NonNull String packageName, + @UidState int uidState) { + synchronized (mInMemoryLock) { + if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) { + getUpdatedPendingHistoricalOpsMLocked(System.currentTimeMillis()) + .increaseRejectCount(op, uid, packageName, uidState, 1); + } + } + } + + void increaseOpAccessDuration(int op, int uid, @NonNull String packageName, + @UidState int uidState, long increment) { + synchronized (mInMemoryLock) { + if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) { + getUpdatedPendingHistoricalOpsMLocked(System.currentTimeMillis()) + .increaseAccessDuration(op, uid, packageName, uidState, increment); + } + } + } + + void setHistoryParameters(@HistoricalMode int mode, + long baseSnapshotInterval, long intervalCompressionMultiplier) { + synchronized (mOnDiskLock) { + synchronized (mInMemoryLock) { + boolean resampleHistory = false; + Slog.i(LOG_TAG, "New history parameters: mode:" + + AppOpsManager.historicalModeToString(mMode) + " baseSnapshotInterval:" + + baseSnapshotInterval + " intervalCompressionMultiplier:" + + intervalCompressionMultiplier); + if (mMode != mode) { + mMode = mode; + if (mMode == AppOpsManager.HISTORICAL_MODE_DISABLED) { + clearHistoryOnDiskLocked(); + } + } + if (mBaseSnapshotInterval != baseSnapshotInterval) { + mBaseSnapshotInterval = baseSnapshotInterval; + resampleHistory = true; + } + if (mIntervalCompressionMultiplier != intervalCompressionMultiplier) { + mIntervalCompressionMultiplier = intervalCompressionMultiplier; + resampleHistory = true; + } + if (resampleHistory) { + resampleHistoryOnDiskInMemoryDMLocked(0); + } + } + } + } + + void offsetHistory(long offsetMillis) { + synchronized (mOnDiskLock) { + synchronized (mInMemoryLock) { + final List<HistoricalOps> history = mPersistence.readHistoryDLocked(); + clearHistory(); + if (history != null) { + final int historySize = history.size(); + for (int i = 0; i < historySize; i++) { + final HistoricalOps ops = history.get(i); + ops.offsetBeginAndEndTime(offsetMillis); + } + if (offsetMillis < 0) { + pruneFutureOps(history); + } + mPersistence.persistHistoricalOpsDLocked(history); + } + } + } + } + + void addHistoricalOps(HistoricalOps ops) { + final List<HistoricalOps> pendingWrites; + synchronized (mInMemoryLock) { + // The history files start from mBaseSnapshotInterval - take this into account. + ops.offsetBeginAndEndTime(mBaseSnapshotInterval); + mPendingWrites.offerFirst(ops); + pendingWrites = new ArrayList<>(mPendingWrites); + mPendingWrites.clear(); + } + persistPendingHistory(pendingWrites); + } + + private void resampleHistoryOnDiskInMemoryDMLocked(long offsetMillis) { + mPersistence = new Persistence(mBaseSnapshotInterval, mIntervalCompressionMultiplier); + offsetHistory(offsetMillis); + } + + void resetHistoryParameters() { + setHistoryParameters(DEFAULT_MODE, DEFAULT_SNAPSHOT_INTERVAL_MILLIS, + DEFAULT_COMPRESSION_STEP); + } + + void clearHistory() { + synchronized (mOnDiskLock) { + clearHistoryOnDiskLocked(); + } + } + + private void clearHistoryOnDiskLocked() { + BackgroundThread.getHandler().removeMessages(MSG_WRITE_PENDING_HISTORY); + synchronized (mInMemoryLock) { + mCurrentHistoricalOps = null; + mNextPersistDueTimeMillis = System.currentTimeMillis(); + mPendingWrites.clear(); + } + mPersistence.clearHistoryDLocked(); + } + + private @NonNull HistoricalOps getUpdatedPendingHistoricalOpsMLocked(long now) { + if (mCurrentHistoricalOps != null) { + final long remainingTimeMillis = mNextPersistDueTimeMillis - now; + if (remainingTimeMillis > mBaseSnapshotInterval) { + // If time went backwards we need to push history to the future with the + // overflow over our snapshot interval. If time went forward do nothing + // as we would naturally push history into the past on the next write. + mPendingHistoryOffsetMillis = remainingTimeMillis - mBaseSnapshotInterval; + } + final long elapsedTimeMillis = mBaseSnapshotInterval - remainingTimeMillis; + mCurrentHistoricalOps.setEndTime(elapsedTimeMillis); + if (remainingTimeMillis > 0) { + if (DEBUG) { + Slog.i(LOG_TAG, "Returning current in-memory state"); + } + return mCurrentHistoricalOps; + } + if (mCurrentHistoricalOps.isEmpty()) { + mCurrentHistoricalOps.setBeginAndEndTime(0, 0); + mNextPersistDueTimeMillis = now + mBaseSnapshotInterval; + return mCurrentHistoricalOps; + } + // The current batch is full, so persist taking into account overdue persist time. + mCurrentHistoricalOps.offsetBeginAndEndTime(mBaseSnapshotInterval); + mCurrentHistoricalOps.setBeginTime(mCurrentHistoricalOps.getEndTimeMillis() + - mBaseSnapshotInterval); + final long overdueTimeMillis = Math.abs(remainingTimeMillis); + mCurrentHistoricalOps.offsetBeginAndEndTime(overdueTimeMillis); + schedulePersistHistoricalOpsMLocked(mCurrentHistoricalOps); + } + // The current batch is in the future, i.e. not complete yet. + mCurrentHistoricalOps = new HistoricalOps(0, 0); + mNextPersistDueTimeMillis = now + mBaseSnapshotInterval; + if (DEBUG) { + Slog.i(LOG_TAG, "Returning new in-memory state"); + } + return mCurrentHistoricalOps; + } + + private void persistPendingHistory() { + final List<HistoricalOps> pendingWrites; + synchronized (mOnDiskLock) { + synchronized (mInMemoryLock) { + pendingWrites = new ArrayList<>(mPendingWrites); + mPendingWrites.clear(); + if (mPendingHistoryOffsetMillis != 0) { + resampleHistoryOnDiskInMemoryDMLocked(mPendingHistoryOffsetMillis); + mPendingHistoryOffsetMillis = 0; + } + } + persistPendingHistory(pendingWrites); + } + } + private void persistPendingHistory(@NonNull List<HistoricalOps> pendingWrites) { + synchronized (mOnDiskLock) { + BackgroundThread.getHandler().removeMessages(MSG_WRITE_PENDING_HISTORY); + if (pendingWrites.isEmpty()) { + return; + } + final int opCount = pendingWrites.size(); + // Pending writes are offset relative to each other, so take this + // into account to persist everything in one shot - single write. + for (int i = 0; i < opCount; i++) { + final HistoricalOps current = pendingWrites.get(i); + if (i > 0) { + final HistoricalOps previous = pendingWrites.get(i - 1); + current.offsetBeginAndEndTime(previous.getBeginTimeMillis()); + } + } + mPersistence.persistHistoricalOpsDLocked(pendingWrites); + } + } + + private void schedulePersistHistoricalOpsMLocked(@NonNull HistoricalOps ops) { + final Message message = PooledLambda.obtainMessage( + HistoricalRegistry::persistPendingHistory, HistoricalRegistry.this); + message.what = MSG_WRITE_PENDING_HISTORY; + BackgroundThread.getHandler().sendMessage(message); + mPendingWrites.offerFirst(ops); + } + + private static void makeRelativeToEpochStart(@NonNull HistoricalOps ops, long nowMillis) { + ops.setBeginAndEndTime(nowMillis - ops.getEndTimeMillis(), + nowMillis- ops.getBeginTimeMillis()); + } + + private void pruneFutureOps(@NonNull List<HistoricalOps> ops) { + final int opCount = ops.size(); + for (int i = opCount - 1; i >= 0; i--) { + final HistoricalOps op = ops.get(i); + if (op.getEndTimeMillis() <= mBaseSnapshotInterval) { + ops.remove(i); + } else if (op.getBeginTimeMillis() < mBaseSnapshotInterval) { + final double filterScale = (double) (op.getEndTimeMillis() - mBaseSnapshotInterval) + / (double) op.getDurationMillis(); + Persistence.spliceFromBeginning(op, filterScale); + } + } + } + + private static final class Persistence { + private static final boolean DEBUG = false; + + private static final String LOG_TAG = Persistence.class.getSimpleName(); + + private static final String HISTORY_FILE_SUFFIX = ".xml"; + + private static final String TAG_HISTORY = "history"; + private static final String TAG_OPS = "ops"; + private static final String TAG_UID = "uid"; + private static final String TAG_PACKAGE = "package"; + private static final String TAG_OP = "op"; + private static final String TAG_STATE = "state"; + + private static final String ATTR_VERSION = "version"; + private static final String ATTR_NAME = "name"; + private static final String ATTR_ACCESS_COUNT = "accessCount"; + private static final String ATTR_REJECT_COUNT = "rejectCount"; + private static final String ATTR_ACCESS_DURATION = "accessDuration"; + private static final String ATTR_BEGIN_TIME = "beginTime"; + private static final String ATTR_END_TIME = "endTime"; + private static final String ATTR_OVERFLOW = "overflow"; + + private static final int CURRENT_VERSION = 1; + + private final long mBaseSnapshotInterval; + private final long mIntervalCompressionMultiplier; + + Persistence(long baseSnapshotInterval, long intervalCompressionMultiplier) { + mBaseSnapshotInterval = baseSnapshotInterval; + mIntervalCompressionMultiplier = intervalCompressionMultiplier; + } + + private final AtomicDirectory mHistoricalAppOpsDir = new AtomicDirectory( + new File(new File(Environment.getDataSystemDeDirectory(), "appops"), "history")); + + private File generateFile(@NonNull File baseDir, int depth) { + final long globalBeginMillis = computeGlobalIntervalBeginMillis(depth); + return new File(baseDir, Long.toString(globalBeginMillis) + HISTORY_FILE_SUFFIX); + } + + void clearHistoryDLocked() { + mHistoricalAppOpsDir.delete(); + } + + void persistHistoricalOpsDLocked(@NonNull List<HistoricalOps> ops) { + if (DEBUG) { + Slog.i(LOG_TAG, "Persisting ops:\n" + opsToDebugString(ops)); + enforceOpsWellFormed(ops); + } + try { + final File newBaseDir = mHistoricalAppOpsDir.startWrite(); + final File oldBaseDir = mHistoricalAppOpsDir.getBackupDirectory(); + handlePersistHistoricalOpsRecursiveDLocked(newBaseDir, oldBaseDir, ops, 0); + mHistoricalAppOpsDir.finishWrite(); + } catch (Throwable t) { + Slog.wtf(LOG_TAG, "Failed to write historical app ops, restoring backup", t); + mHistoricalAppOpsDir.failWrite(); + } + } + + @Nullable List<HistoricalOps> readHistoryRawDLocked() { + return collectHistoricalOpsBaseDLocked(Process.INVALID_UID /*filterUid*/, + null /*filterPackageName*/, null /*filterOpNames*/, + 0 /*filterBeginTimeMills*/, Long.MAX_VALUE /*filterEndTimeMills*/); + } + + @Nullable List<HistoricalOps> readHistoryDLocked() { + final List<HistoricalOps> result = readHistoryRawDLocked(); + // Take into account in memory state duration. + if (result != null) { + final int opCount = result.size(); + for (int i = 0; i < opCount; i++) { + result.get(i).offsetBeginAndEndTime(mBaseSnapshotInterval); + } + } + return result; + } + + long getLastPersistTimeMillisDLocked() { + try { + final File baseDir = mHistoricalAppOpsDir.startRead(); + final File file = generateFile(baseDir, 0); + if (file.exists()) { + return file.lastModified(); + } + mHistoricalAppOpsDir.finishRead(); + } catch (IOException e) { + Slog.wtf("Error reading historical app ops. Deleting history.", e); + mHistoricalAppOpsDir.delete(); + } + return 0; + } + + private void collectHistoricalOpsDLocked(@NonNull HistoricalOps currentOps, + int filterUid, @NonNull String filterPackageName, @Nullable String[] filterOpNames, + long filterBeingMillis, long filterEndMillis) { + final List<HistoricalOps> readOps = collectHistoricalOpsBaseDLocked(filterUid, + filterPackageName, filterOpNames, filterBeingMillis, filterEndMillis); + if (readOps != null) { + final int readCount = readOps.size(); + for (int i = 0; i < readCount; i++) { + final HistoricalOps readOp = readOps.get(i); + currentOps.merge(readOp); + } + } + } + + private @Nullable LinkedList<HistoricalOps> collectHistoricalOpsBaseDLocked( + int filterUid, @NonNull String filterPackageName, @Nullable String[] filterOpNames, + long filterBeginTimeMillis, long filterEndTimeMillis) { + try { + final File baseDir = mHistoricalAppOpsDir.startRead(); + final File[] files = baseDir.listFiles(); + if (files == null) { + return null; + } + final ArraySet<File> historyFiles = new ArraySet<>(files.length); + for (File file : files) { + if (file.isFile() && file.getName().endsWith(HISTORY_FILE_SUFFIX)) { + historyFiles.add(file); + } + } + final long[] globalContentOffsetMillis = {0}; + final LinkedList<HistoricalOps> ops = collectHistoricalOpsRecursiveDLocked( + baseDir, filterUid, filterPackageName, filterOpNames, filterBeginTimeMillis, + filterEndTimeMillis, globalContentOffsetMillis, null /*outOps*/, + 0 /*depth*/, historyFiles); + mHistoricalAppOpsDir.finishRead(); + return ops; + } catch (IOException | XmlPullParserException e) { + Slog.wtf("Error reading historical app ops. Deleting history.", e); + mHistoricalAppOpsDir.delete(); + } + return null; + } + + private @Nullable LinkedList<HistoricalOps> collectHistoricalOpsRecursiveDLocked( + @NonNull File baseDir, int filterUid, @NonNull String filterPackageName, + @Nullable String[] filterOpNames, long filterBeginTimeMillis, + long filterEndTimeMillis, @NonNull long[] globalContentOffsetMillis, + @Nullable LinkedList<HistoricalOps> outOps, int depth, + @NonNull ArraySet<File> historyFiles) + throws IOException, XmlPullParserException { + final long previousIntervalEndMillis = (long) Math.pow(mIntervalCompressionMultiplier, + depth) * mBaseSnapshotInterval; + final long currentIntervalEndMillis = (long) Math.pow(mIntervalCompressionMultiplier, + depth + 1) * mBaseSnapshotInterval; + + filterBeginTimeMillis = Math.max(filterBeginTimeMillis - previousIntervalEndMillis, 0); + filterEndTimeMillis = filterEndTimeMillis - previousIntervalEndMillis; + + // Read historical data at this level + final List<HistoricalOps> readOps = readHistoricalOpsLocked(baseDir, + previousIntervalEndMillis, currentIntervalEndMillis, filterUid, + filterPackageName, filterOpNames, filterBeginTimeMillis, filterEndTimeMillis, + globalContentOffsetMillis, depth, historyFiles); + + // Empty is a special signal to stop diving + if (readOps != null && readOps.isEmpty()) { + return outOps; + } + + // Collect older historical data from subsequent levels + outOps = collectHistoricalOpsRecursiveDLocked( + baseDir, filterUid, filterPackageName, filterOpNames, filterBeginTimeMillis, + filterEndTimeMillis, globalContentOffsetMillis, outOps, depth + 1, + historyFiles); + + // Make older historical data relative to the current historical level + if (outOps != null) { + final int opCount = outOps.size(); + for (int i = 0; i < opCount; i++) { + final HistoricalOps collectedOp = outOps.get(i); + collectedOp.offsetBeginAndEndTime(currentIntervalEndMillis); + } + } + + if (readOps != null) { + if (outOps == null) { + outOps = new LinkedList<>(); + } + // Add the read ops to output + final int opCount = readOps.size(); + for (int i = opCount - 1; i >= 0; i--) { + outOps.offerFirst(readOps.get(i)); + } + } + + return outOps; + } + + private boolean createHardLinkToExistingFile(@NonNull File fromFile, @NonNull File toFile) + throws IOException { + if (!fromFile.exists()) { + return false; + } + Files.createLink(toFile.toPath(), fromFile.toPath()); + return true; + } + + private void handlePersistHistoricalOpsRecursiveDLocked(@NonNull File newBaseDir, + @NonNull File oldBaseDir, @Nullable List<HistoricalOps> passedOps, int depth) + throws IOException, XmlPullParserException { + final long previousIntervalEndMillis = (long) Math.pow(mIntervalCompressionMultiplier, + depth) * mBaseSnapshotInterval; + final long currentIntervalEndMillis = (long) Math.pow(mIntervalCompressionMultiplier, + depth + 1) * mBaseSnapshotInterval; + + if (passedOps == null || passedOps.isEmpty()) { + // If there is an old file we need to copy it over to the new state. + final File oldFile = generateFile(oldBaseDir, depth); + final File newFile = generateFile(newBaseDir, depth); + if (createHardLinkToExistingFile(oldFile, newFile)) { + handlePersistHistoricalOpsRecursiveDLocked(newBaseDir, oldBaseDir, + passedOps, depth + 1); + } + return; + } + + if (DEBUG) { + enforceOpsWellFormed(passedOps); + } + + // Normalize passed ops time to be based off this interval start + final int passedOpCount = passedOps.size(); + for (int i = 0; i < passedOpCount; i++) { + final HistoricalOps passedOp = passedOps.get(i); + passedOp.offsetBeginAndEndTime(-previousIntervalEndMillis); + } + + if (DEBUG) { + enforceOpsWellFormed(passedOps); + } + + // Read persisted ops for this interval + final List<HistoricalOps> existingOps = readHistoricalOpsLocked(oldBaseDir, + previousIntervalEndMillis, currentIntervalEndMillis, + Process.INVALID_UID /*filterUid*/, null /*filterPackageName*/, + null /*filterOpNames*/, Long.MIN_VALUE /*filterBeginTimeMillis*/, + Long.MAX_VALUE /*filterEndTimeMillis*/, null, depth, + null /*historyFiles*/); + + if (DEBUG) { + enforceOpsWellFormed(existingOps); + } + + // Offset existing ops to account for elapsed time + final int existingOpCount = existingOps.size(); + if (existingOpCount > 0) { + // Compute elapsed time + final long elapsedTimeMillis = passedOps.get(passedOps.size() - 1) + .getEndTimeMillis(); + for (int i = 0; i < existingOpCount; i++) { + final HistoricalOps existingOp = existingOps.get(i); + existingOp.offsetBeginAndEndTime(elapsedTimeMillis); + } + } + + if (DEBUG) { + enforceOpsWellFormed(existingOps); + } + + final long slotDurationMillis = previousIntervalEndMillis; + + // Consolidate passed ops at the current slot duration ensuring each snapshot is + // full. To achieve this we put all passed and existing ops in a list and will + // merge them to ensure each represents a snapshot at the current granularity. + final List<HistoricalOps> allOps = new LinkedList<>(); + allOps.addAll(passedOps); + allOps.addAll(existingOps); + + if (DEBUG) { + enforceOpsWellFormed(allOps); + } + + // Compute ops to persist and overflow ops + List<HistoricalOps> persistedOps = null; + List<HistoricalOps> overflowedOps = null; + + // We move a snapshot into the next level only if the start time is + // after the end of the current interval. This avoids rewriting all + // files to propagate the information added to the history on every + // iteration. Instead, we would rewrite the next level file only if + // an entire snapshot from the previous level is being propagated. + // The trade off is that we need to store how much the last snapshot + // of the current interval overflows past the interval end. We write + // the overflow data to avoid parsing all snapshots on query. + long intervalOverflowMillis = 0; + final int opCount = allOps.size(); + for (int i = 0; i < opCount; i++) { + final HistoricalOps op = allOps.get(i); + final HistoricalOps persistedOp; + final HistoricalOps overflowedOp; + if (op.getEndTimeMillis() <= currentIntervalEndMillis) { + persistedOp = op; + overflowedOp = null; + } else if (op.getBeginTimeMillis() < currentIntervalEndMillis) { + persistedOp = op; + intervalOverflowMillis = op.getEndTimeMillis() - currentIntervalEndMillis; + if (intervalOverflowMillis > previousIntervalEndMillis) { + final double splitScale = (double) intervalOverflowMillis + / op.getDurationMillis(); + overflowedOp = spliceFromEnd(op, splitScale); + intervalOverflowMillis = op.getEndTimeMillis() - currentIntervalEndMillis; + } else { + overflowedOp = null; + } + } else { + persistedOp = null; + overflowedOp = op; + } + if (persistedOp != null) { + if (persistedOps == null) { + persistedOps = new ArrayList<>(); + } + persistedOps.add(persistedOp); + } + if (overflowedOp != null) { + if (overflowedOps == null) { + overflowedOps = new ArrayList<>(); + } + overflowedOps.add(overflowedOp); + } + } + + if (DEBUG) { + enforceOpsWellFormed(persistedOps); + enforceOpsWellFormed(overflowedOps); + } + + if (persistedOps != null) { + normalizeSnapshotForSlotDuration(persistedOps, slotDurationMillis); + final File newFile = generateFile(newBaseDir, depth); + writeHistoricalOpsDLocked(persistedOps, intervalOverflowMillis, newFile); + if (DEBUG) { + Slog.i(LOG_TAG, "Persisted at depth: " + depth + + " ops:\n" + opsToDebugString(persistedOps)); + enforceOpsWellFormed(persistedOps); + } + } + + handlePersistHistoricalOpsRecursiveDLocked(newBaseDir, oldBaseDir, + overflowedOps, depth + 1); + } + + private @NonNull List<HistoricalOps> readHistoricalOpsLocked(File baseDir, + long intervalBeginMillis, long intervalEndMillis, int filterUid, + @Nullable String filterPackageName, @Nullable String[] filterOpNames, + long filterBeginTimeMillis, long filterEndTimeMillis, + @Nullable long[] cumulativeOverflowMillis, int depth, + @NonNull ArraySet<File> historyFiles) + throws IOException, XmlPullParserException { + final File file = generateFile(baseDir, depth); + if (historyFiles != null) { + historyFiles.remove(file); + } + if (filterBeginTimeMillis >= filterEndTimeMillis + || filterEndTimeMillis < intervalBeginMillis) { + // Don't go deeper + return Collections.emptyList(); + } + if (filterBeginTimeMillis >= (intervalEndMillis + + ((intervalEndMillis - intervalBeginMillis) / mIntervalCompressionMultiplier) + + (cumulativeOverflowMillis != null ? cumulativeOverflowMillis[0] : 0)) + || !file.exists()) { + if (historyFiles == null || historyFiles.isEmpty()) { + // Don't go deeper + return Collections.emptyList(); + } else { + // Keep diving + return null; + } + } + return readHistoricalOpsLocked(file, filterUid, filterPackageName, filterOpNames, + filterBeginTimeMillis, filterEndTimeMillis, cumulativeOverflowMillis); + } + + private @Nullable List<HistoricalOps> readHistoricalOpsLocked(@NonNull File file, + int filterUid, @Nullable String filterPackageName, @Nullable String[] filterOpNames, + long filterBeginTimeMillis, long filterEndTimeMillis, + @Nullable long[] cumulativeOverflowMillis) + throws IOException, XmlPullParserException { + if (DEBUG) { + Slog.i(LOG_TAG, "Reading ops from:" + file); + } + List<HistoricalOps> allOps = null; + try (FileInputStream stream = new FileInputStream(file)) { + final XmlPullParser parser = Xml.newPullParser(); + parser.setInput(stream, StandardCharsets.UTF_8.name()); + XmlUtils.beginDocument(parser, TAG_HISTORY); + final long overflowMillis = XmlUtils.readLongAttribute(parser, ATTR_OVERFLOW, 0); + final int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + if (TAG_OPS.equals(parser.getName())) { + final HistoricalOps ops = readeHistoricalOpsDLocked(parser, + filterUid, filterPackageName, filterOpNames, filterBeginTimeMillis, + filterEndTimeMillis, cumulativeOverflowMillis); + if (ops == null) { + continue; + } + if (ops.isEmpty()) { + XmlUtils.skipCurrentTag(parser); + continue; + } + if (allOps == null) { + allOps = new ArrayList<>(); + } + allOps.add(ops); + } + } + if (cumulativeOverflowMillis != null) { + cumulativeOverflowMillis[0] += overflowMillis; + } + } catch (FileNotFoundException e) { + Slog.i(LOG_TAG, "No history file: " + file.getName()); + return Collections.emptyList(); + } + if (DEBUG) { + if (allOps != null) { + Slog.i(LOG_TAG, "Read from file: " + file + "ops:\n" + + opsToDebugString(allOps)); + enforceOpsWellFormed(allOps); + } + } + return allOps; + } + + private @Nullable HistoricalOps readeHistoricalOpsDLocked( + @NonNull XmlPullParser parser, int filterUid, @Nullable String filterPackageName, + @Nullable String[] filterOpNames, long filterBeginTimeMillis, + long filterEndTimeMillis, @Nullable long[] cumulativeOverflowMillis) + throws IOException, XmlPullParserException { + final long beginTimeMillis = XmlUtils.readLongAttribute(parser, ATTR_BEGIN_TIME, 0) + + (cumulativeOverflowMillis != null ? cumulativeOverflowMillis[0] : 0); + final long endTimeMillis = XmlUtils.readLongAttribute(parser, ATTR_END_TIME, 0) + + (cumulativeOverflowMillis != null ? cumulativeOverflowMillis[0] : 0); + // Keep reading as subsequent records may start matching + if (filterEndTimeMillis < beginTimeMillis) { + return null; + } + // Stop reading as subsequent records will not match + if (filterBeginTimeMillis > endTimeMillis) { + return new HistoricalOps(0, 0); + } + final long filteredBeginTimeMillis = Math.max(beginTimeMillis, filterBeginTimeMillis); + final long filteredEndTimeMillis = Math.min(endTimeMillis, filterEndTimeMillis); + final double filterScale = (double) (filteredEndTimeMillis - filteredBeginTimeMillis) + / (double) (endTimeMillis - beginTimeMillis); + HistoricalOps ops = null; + final int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + if (TAG_UID.equals(parser.getName())) { + final HistoricalOps returnedOps = readHistoricalUidOpsDLocked(ops, parser, + filterUid, filterPackageName, filterOpNames, filterScale); + if (ops == null) { + ops = returnedOps; + } + } + } + if (ops != null) { + ops.setBeginAndEndTime(filteredBeginTimeMillis, filteredEndTimeMillis); + } + return ops; + } + + private @Nullable HistoricalOps readHistoricalUidOpsDLocked( + @Nullable HistoricalOps ops, @NonNull XmlPullParser parser, int filterUid, + @Nullable String filterPackageName, @Nullable String[] filterOpNames, + double filterScale) throws IOException, XmlPullParserException { + final int uid = XmlUtils.readIntAttribute(parser, ATTR_NAME); + if (filterUid != Process.INVALID_UID && filterUid != uid) { + XmlUtils.skipCurrentTag(parser); + return null; + } + final int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + if (TAG_PACKAGE.equals(parser.getName())) { + final HistoricalOps returnedOps = readHistoricalPackageOpsDLocked(ops, + uid, parser, filterPackageName, filterOpNames, filterScale); + if (ops == null) { + ops = returnedOps; + } + } + } + return ops; + } + + private @Nullable HistoricalOps readHistoricalPackageOpsDLocked( + @Nullable HistoricalOps ops, int uid, @NonNull XmlPullParser parser, + @Nullable String filterPackageName, @Nullable String[] filterOpNames, + double filterScale) throws IOException, XmlPullParserException { + final String packageName = XmlUtils.readStringAttribute(parser, ATTR_NAME); + if (filterPackageName != null && !filterPackageName.equals(packageName)) { + XmlUtils.skipCurrentTag(parser); + return null; + } + final int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + if (TAG_OP.equals(parser.getName())) { + final HistoricalOps returnedOps = readHistoricalOpDLocked(ops, uid, + packageName, parser, filterOpNames, filterScale); + if (ops == null) { + ops = returnedOps; + } + } + } + return ops; + } + + private @Nullable HistoricalOps readHistoricalOpDLocked(@Nullable HistoricalOps ops, + int uid, String packageName, @NonNull XmlPullParser parser, + @Nullable String[] filterOpNames, double filterScale) + throws IOException, XmlPullParserException { + final int op = XmlUtils.readIntAttribute(parser, ATTR_NAME); + if (filterOpNames != null && !ArrayUtils.contains(filterOpNames, + AppOpsManager.opToName(op))) { + XmlUtils.skipCurrentTag(parser); + return null; + } + final int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + if (TAG_STATE.equals(parser.getName())) { + final HistoricalOps returnedOps = readUidStateDLocked(ops, uid, + packageName, op, parser, filterScale); + if (ops == null) { + ops = returnedOps; + } + } + } + return ops; + } + + private @Nullable HistoricalOps readUidStateDLocked(@Nullable HistoricalOps ops, + int uid, String packageName, int op, @NonNull XmlPullParser parser, + double filterScale) throws IOException { + final int uidState = XmlUtils.readIntAttribute(parser, ATTR_NAME); + long accessCount = XmlUtils.readLongAttribute(parser, ATTR_ACCESS_COUNT, 0); + if (accessCount > 0) { + if (!Double.isNaN(filterScale)) { + accessCount = (long) HistoricalOps.round( + (double) accessCount * filterScale); + } + if (ops == null) { + ops = new HistoricalOps(0, 0); + } + ops.increaseAccessCount(op, uid, packageName, uidState, accessCount); + } + long rejectCount = XmlUtils.readLongAttribute(parser, ATTR_REJECT_COUNT, 0); + if (rejectCount > 0) { + if (!Double.isNaN(filterScale)) { + rejectCount = (long) HistoricalOps.round( + (double) rejectCount * filterScale); + } + if (ops == null) { + ops = new HistoricalOps(0, 0); + } + ops.increaseRejectCount(op, uid, packageName, uidState, rejectCount); + } + long accessDuration = XmlUtils.readLongAttribute(parser, ATTR_ACCESS_DURATION, 0); + if (accessDuration > 0) { + if (!Double.isNaN(filterScale)) { + accessDuration = (long) HistoricalOps.round( + (double) accessDuration * filterScale); + } + if (ops == null) { + ops = new HistoricalOps(0, 0); + } + ops.increaseAccessDuration(op, uid, packageName, uidState, accessDuration); + } + return ops; + } + + private void writeHistoricalOpsDLocked(@Nullable List<HistoricalOps> allOps, + long intervalOverflowMillis, @NonNull File file) throws IOException { + final FileOutputStream output = mHistoricalAppOpsDir.openWrite(file); + try { + final XmlSerializer serializer = Xml.newSerializer(); + serializer.setOutput(output, StandardCharsets.UTF_8.name()); + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", + true); + serializer.startDocument(null, true); + serializer.startTag(null, TAG_HISTORY); + serializer.attribute(null, ATTR_VERSION, String.valueOf(CURRENT_VERSION)); + if (intervalOverflowMillis != 0) { + serializer.attribute(null, ATTR_OVERFLOW, + Long.toString(intervalOverflowMillis)); + } + if (allOps != null) { + final int opsCount = allOps.size(); + for (int i = 0; i < opsCount; i++) { + final HistoricalOps ops = allOps.get(i); + writeHistoricalOpDLocked(ops, serializer); + } + } + serializer.endTag(null, TAG_HISTORY); + serializer.endDocument(); + mHistoricalAppOpsDir.closeWrite(output); + } catch (IOException e) { + mHistoricalAppOpsDir.failWrite(output); + throw e; + } + } + + private void writeHistoricalOpDLocked(@NonNull HistoricalOps ops, + @NonNull XmlSerializer serializer) throws IOException { + serializer.startTag(null, TAG_OPS); + serializer.attribute(null, ATTR_BEGIN_TIME, Long.toString(ops.getBeginTimeMillis())); + serializer.attribute(null, ATTR_END_TIME, Long.toString(ops.getEndTimeMillis())); + final int uidCount = ops.getUidCount(); + for (int i = 0; i < uidCount; i++) { + final HistoricalUidOps uidOp = ops.getUidOpsAt(i); + writeHistoricalUidOpsDLocked(uidOp, serializer); + } + serializer.endTag(null, TAG_OPS); + } + + private void writeHistoricalUidOpsDLocked(@NonNull HistoricalUidOps uidOps, + @NonNull XmlSerializer serializer) throws IOException { + serializer.startTag(null, TAG_UID); + serializer.attribute(null, ATTR_NAME, Integer.toString(uidOps.getUid())); + final int packageCount = uidOps.getPackageCount(); + for (int i = 0; i < packageCount; i++) { + final HistoricalPackageOps packageOps = uidOps.getPackageOpsAt(i); + writeHistoricalPackageOpsDLocked(packageOps, serializer); + } + serializer.endTag(null, TAG_UID); + } + + private void writeHistoricalPackageOpsDLocked(@NonNull HistoricalPackageOps packageOps, + @NonNull XmlSerializer serializer) throws IOException { + serializer.startTag(null, TAG_PACKAGE); + serializer.attribute(null, ATTR_NAME, packageOps.getPackageName()); + final int opCount = packageOps.getOpCount(); + for (int i = 0; i < opCount; i++) { + final HistoricalOp op = packageOps.getOpAt(i); + writeHistoricalOpDLocked(op, serializer); + } + serializer.endTag(null, TAG_PACKAGE); + } + + private void writeHistoricalOpDLocked(@NonNull HistoricalOp op, + @NonNull XmlSerializer serializer) throws IOException { + serializer.startTag(null, TAG_OP); + serializer.attribute(null, ATTR_NAME, Integer.toString(op.getOpCode())); + for (int uidState = 0; uidState < AppOpsManager._NUM_UID_STATE; uidState++) { + writeUidStateOnLocked(op, uidState, serializer); + } + serializer.endTag(null, TAG_OP); + } + + private void writeUidStateOnLocked(@NonNull HistoricalOp op, @UidState int uidState, + @NonNull XmlSerializer serializer) throws IOException { + final long accessCount = op.getAccessCount(uidState); + final long rejectCount = op.getRejectCount(uidState); + final long accessDuration = op.getAccessDuration(uidState); + if (accessCount == 0 && rejectCount == 0 && accessDuration == 0) { + return; + } + serializer.startTag(null, TAG_STATE); + serializer.attribute(null, ATTR_NAME, Integer.toString(uidState)); + if (accessCount > 0) { + serializer.attribute(null, ATTR_ACCESS_COUNT, Long.toString(accessCount)); + } + if (rejectCount > 0) { + serializer.attribute(null, ATTR_REJECT_COUNT, Long.toString(rejectCount)); + } + if (accessDuration > 0) { + serializer.attribute(null, ATTR_ACCESS_DURATION, Long.toString(accessDuration)); + } + serializer.endTag(null, TAG_STATE); + } + + private static void enforceOpsWellFormed(@NonNull List<HistoricalOps> ops) { + if (ops == null) { + return; + } + HistoricalOps previous; + HistoricalOps current = null; + final int opsCount = ops.size(); + for (int i = 0; i < opsCount; i++) { + previous = current; + current = ops.get(i); + if (current.isEmpty()) { + throw new IllegalStateException("Empty ops:\n" + + opsToDebugString(ops)); + } + if (current.getEndTimeMillis() < current.getBeginTimeMillis()) { + throw new IllegalStateException("Begin after end:\n" + + opsToDebugString(ops)); + } + if (previous != null) { + if (previous.getEndTimeMillis() > current.getBeginTimeMillis()) { + throw new IllegalStateException("Intersecting ops:\n" + + opsToDebugString(ops)); + } + if (previous.getBeginTimeMillis() > current.getBeginTimeMillis()) { + throw new IllegalStateException("Non increasing ops:\n" + + opsToDebugString(ops)); + } + } + } + } + + private long computeGlobalIntervalBeginMillis(int depth) { + long beginTimeMillis = 0; + for (int i = 0; i < depth + 1; i++) { + beginTimeMillis += Math.pow(mIntervalCompressionMultiplier, i); + } + return beginTimeMillis * mBaseSnapshotInterval; + } + + private static @NonNull HistoricalOps spliceFromEnd(@NonNull HistoricalOps ops, + double spliceRatio) { + if (DEBUG) { + Slog.w(LOG_TAG, "Splicing from end:" + ops + " ratio:" + spliceRatio); + } + final HistoricalOps splice = ops.spliceFromEnd(spliceRatio); + if (DEBUG) { + Slog.w(LOG_TAG, "Spliced into:" + ops + " and:" + splice); + } + return splice; + } + + + private static @NonNull HistoricalOps spliceFromBeginning(@NonNull HistoricalOps ops, + double spliceRatio) { + if (DEBUG) { + Slog.w(LOG_TAG, "Splicing from beginning:" + ops + " ratio:" + spliceRatio); + } + final HistoricalOps splice = ops.spliceFromBeginning(spliceRatio); + if (DEBUG) { + Slog.w(LOG_TAG, "Spliced into:" + ops + " and:" + splice); + } + return splice; + } + + private static void normalizeSnapshotForSlotDuration(@NonNull List<HistoricalOps> ops, + long slotDurationMillis) { + if (DEBUG) { + Slog.i(LOG_TAG, "Normalizing for slot duration: " + slotDurationMillis + + " ops:\n" + opsToDebugString(ops)); + enforceOpsWellFormed(ops); + } + long slotBeginTimeMillis; + final int opCount = ops.size(); + for (int processedIdx = opCount - 1; processedIdx >= 0; processedIdx--) { + final HistoricalOps processedOp = ops.get(processedIdx); + slotBeginTimeMillis = Math.max(processedOp.getEndTimeMillis() + - slotDurationMillis, 0); + for (int candidateIdx = processedIdx - 1; candidateIdx >= 0; candidateIdx--) { + final HistoricalOps candidateOp = ops.get(candidateIdx); + final long candidateSlotIntersectionMillis = candidateOp.getEndTimeMillis() + - Math.min(slotBeginTimeMillis, processedOp.getBeginTimeMillis()); + if (candidateSlotIntersectionMillis <= 0) { + break; + } + final float candidateSplitRatio = candidateSlotIntersectionMillis + / (float) candidateOp.getDurationMillis(); + if (Float.compare(candidateSplitRatio, 1.0f) >= 0) { + ops.remove(candidateIdx); + processedIdx--; + processedOp.merge(candidateOp); + } else { + final HistoricalOps endSplice = spliceFromEnd(candidateOp, + candidateSplitRatio); + if (endSplice != null) { + processedOp.merge(endSplice); + } + if (candidateOp.isEmpty()) { + ops.remove(candidateIdx); + processedIdx--; + } + } + } + } + if (DEBUG) { + Slog.i(LOG_TAG, "Normalized for slot duration: " + slotDurationMillis + + " ops:\n" + opsToDebugString(ops)); + enforceOpsWellFormed(ops); + } + } + + private static @NonNull String opsToDebugString(@NonNull List<HistoricalOps> ops) { + StringBuilder builder = new StringBuilder(); + final int opCount = ops.size(); + for (int i = 0; i < opCount; i++) { + builder.append(" "); + builder.append(ops.get(i)); + if (i < opCount - 1) { + builder.append('\n'); + } + } + return builder.toString(); + } + } + + private final class StringDumpVisitor implements AppOpsManager.HistoricalOpsVisitor { + private final long mNow = System.currentTimeMillis(); + + private final SimpleDateFormat mDateFormatter = new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss.SSS"); + private final Date mDate = new Date(); + + private final @NonNull String mOpsPrefix; + private final @NonNull String mUidPrefix; + private final @NonNull String mPackagePrefix; + private final @NonNull String mEntryPrefix; + private final @NonNull String mUidStatePrefix; + private final @NonNull PrintWriter mWriter; + private final int mFilterUid; + private final String mFilterPackage; + private final int mFilterOp; + + StringDumpVisitor(@NonNull String prefix, @NonNull PrintWriter writer, + int filterUid, String filterPackage, int filterOp) { + mOpsPrefix = prefix + " "; + mUidPrefix = mOpsPrefix + " "; + mPackagePrefix = mUidPrefix + " "; + mEntryPrefix = mPackagePrefix + " "; + mUidStatePrefix = mEntryPrefix + " "; + mWriter = writer; + mFilterUid = filterUid; + mFilterPackage = filterPackage; + mFilterOp = filterOp; + } + + @Override + public void visitHistoricalOps(HistoricalOps ops) { + mWriter.println(); + mWriter.print(mOpsPrefix); + mWriter.println("snapshot:"); + mWriter.print(mUidPrefix); + mWriter.print("begin = "); + mDate.setTime(ops.getBeginTimeMillis()); + mWriter.print(mDateFormatter.format(mDate)); + mWriter.print(" ("); + TimeUtils.formatDuration(ops.getBeginTimeMillis() - mNow, mWriter); + mWriter.println(")"); + mWriter.print(mUidPrefix); + mWriter.print("end = "); + mDate.setTime(ops.getEndTimeMillis()); + mWriter.print(mDateFormatter.format(mDate)); + mWriter.print(" ("); + TimeUtils.formatDuration(ops.getEndTimeMillis() - mNow, mWriter); + mWriter.println(")"); + } + + @Override + public void visitHistoricalUidOps(HistoricalUidOps ops) { + if (mFilterUid != Process.INVALID_UID && mFilterUid != ops.getUid()) { + return; + } + mWriter.println(); + mWriter.print(mUidPrefix); + mWriter.print("Uid "); + UserHandle.formatUid(mWriter, ops.getUid()); + mWriter.println(":"); + } + + @Override + public void visitHistoricalPackageOps(HistoricalPackageOps ops) { + if (mFilterPackage != null && !mFilterPackage.equals(ops.getPackageName())) { + return; + } + mWriter.print(mPackagePrefix); + mWriter.print("Package "); + mWriter.print(ops.getPackageName()); + mWriter.println(":"); + } + + @Override + public void visitHistoricalOp(HistoricalOp ops) { + if (mFilterOp != AppOpsManager.OP_NONE && mFilterOp != ops.getOpCode()) { + return; + } + mWriter.print(mEntryPrefix); + mWriter.print(AppOpsManager.opToName(ops.getOpCode())); + mWriter.println(":"); + for (int uidState = 0; uidState < AppOpsManager._NUM_UID_STATE; uidState++) { + boolean printedUidState = false; + final long accessCount = ops.getAccessCount(uidState); + if (accessCount > 0) { + if (!printedUidState) { + mWriter.print(mUidStatePrefix); + mWriter.print(AppOpsManager.uidStateToString(uidState)); + mWriter.print("["); + printedUidState = true; + } + mWriter.print("access="); + mWriter.print(accessCount); + } + final long rejectCount = ops.getRejectCount(uidState); + if (rejectCount > 0) { + if (!printedUidState) { + mWriter.print(mUidStatePrefix); + mWriter.print(AppOpsManager.uidStateToString(uidState)); + mWriter.print("["); + printedUidState = true; + } else { + mWriter.print(","); + } + mWriter.print("reject="); + mWriter.print(rejectCount); + } + final long accessDuration = ops.getAccessDuration(uidState); + if (accessDuration > 0) { + if (!printedUidState) { + mWriter.print(mUidStatePrefix); + mWriter.print(AppOpsManager.uidStateToString(uidState)); + printedUidState = true; + } else { + mWriter.print(","); + } + mWriter.print("duration="); + mWriter.print(accessDuration); + } + if (printedUidState) { + mWriter.println("]"); + } + } + } + } +} diff --git a/services/core/java/com/android/server/appop/TEST_MAPPING b/services/core/java/com/android/server/appop/TEST_MAPPING new file mode 100644 index 000000000000..4901d3a8b06f --- /dev/null +++ b/services/core/java/com/android/server/appop/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "CtsAppOpsTestCases", + } + ] +} diff --git a/services/core/java/com/android/server/display/ColorDisplayService.java b/services/core/java/com/android/server/display/ColorDisplayService.java index 73d3d9591529..b332c47ce281 100644 --- a/services/core/java/com/android/server/display/ColorDisplayService.java +++ b/services/core/java/com/android/server/display/ColorDisplayService.java @@ -18,7 +18,9 @@ package com.android.server.display; import static com.android.server.display.DisplayTransformManager.LEVEL_COLOR_MATRIX_DISPLAY_WHITE_BALANCE; import static com.android.server.display.DisplayTransformManager.LEVEL_COLOR_MATRIX_NIGHT_DISPLAY; +import static com.android.server.display.DisplayTransformManager.LEVEL_COLOR_MATRIX_SATURATION; +import android.Manifest; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TypeEvaluator; @@ -31,6 +33,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.database.ContentObserver; import android.hardware.display.ColorDisplayManager; import android.hardware.display.IColorDisplayManager; @@ -39,6 +42,7 @@ import android.opengl.Matrix; import android.os.Binder; import android.os.Handler; import android.os.Looper; +import android.os.Message; import android.os.UserHandle; import android.provider.Settings.Secure; import android.provider.Settings.System; @@ -61,6 +65,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.time.format.DateTimeParseException; +import java.util.Arrays; /** * Controls the display's color transforms. @@ -83,6 +88,10 @@ public final class ColorDisplayService extends SystemService { Matrix.setIdentityM(MATRIX_IDENTITY, 0); } + private static final int MSG_APPLY_NIGHT_DISPLAY_IMMEDIATE = 0; + private static final int MSG_APPLY_NIGHT_DISPLAY_ANIMATED = 1; + private static final int MSG_APPLY_GLOBAL_SATURATION = 2; + /** * Evaluator used to animate color matrix transitions. */ @@ -161,6 +170,53 @@ public final class ColorDisplayService extends SystemService { } }; + private final TintController mGlobalSaturationTintController = new TintController() { + + private float[] mMatrixGlobalSaturation = new float[16]; + + @Override + public void setUp(Context context, boolean needsLinear) { + } + + @Override + public float[] getMatrix() { + return Arrays.copyOf(mMatrixGlobalSaturation, mMatrixGlobalSaturation.length); + } + + @Override + public void setMatrix(int saturationLevel) { + if (saturationLevel < 0) { + saturationLevel = 0; + } else if (saturationLevel > 100) { + saturationLevel = 100; + } + Slog.d(TAG, "Setting saturation level: " + saturationLevel); + + if (saturationLevel == 100) { + Matrix.setIdentityM(mMatrixGlobalSaturation, 0); + } else { + float saturation = saturationLevel * 0.1f; + float desaturation = 1.0f - saturation; + float[] luminance = {0.231f * desaturation, 0.715f * desaturation, + 0.072f * desaturation}; + mMatrixGlobalSaturation[0] = luminance[0] + saturation; + mMatrixGlobalSaturation[1] = luminance[0]; + mMatrixGlobalSaturation[2] = luminance[0]; + mMatrixGlobalSaturation[4] = luminance[1]; + mMatrixGlobalSaturation[5] = luminance[1] + saturation; + mMatrixGlobalSaturation[6] = luminance[1]; + mMatrixGlobalSaturation[8] = luminance[2]; + mMatrixGlobalSaturation[9] = luminance[2]; + mMatrixGlobalSaturation[10] = luminance[2] + saturation; + } + } + + @Override + public int getLevel() { + return LEVEL_COLOR_MATRIX_SATURATION; + } + }; + private final Handler mHandler; private int mCurrentUser = UserHandle.USER_NULL; @@ -178,7 +234,7 @@ public final class ColorDisplayService extends SystemService { public ColorDisplayService(Context context) { super(context); - mHandler = new Handler(Looper.getMainLooper()); + mHandler = new TintHandler(Looper.getMainLooper()); } @Override @@ -904,6 +960,23 @@ public final class ColorDisplayService extends SystemService { void onDisplayWhiteBalanceStatusChanged(boolean enabled); } + private final class TintHandler extends Handler { + + TintHandler(Looper looper) { + super(looper, null, true /* async */); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_APPLY_GLOBAL_SATURATION: + mGlobalSaturationTintController.setMatrix(msg.arg1); + applyTint(mGlobalSaturationTintController, false); + break; + } + } + } + private final class BinderService extends IColorDisplayManager.Stub { @Override @@ -915,5 +988,27 @@ public final class ColorDisplayService extends SystemService { Binder.restoreCallingIdentity(token); } } + + @Override + public boolean setSaturationLevel(int level) { + final boolean hasTransformsPermission = getContext() + .checkCallingPermission(Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) + == PackageManager.PERMISSION_GRANTED; + final boolean hasLegacyPermission = getContext() + .checkCallingPermission(Manifest.permission.CONTROL_DISPLAY_SATURATION) + == PackageManager.PERMISSION_GRANTED; + if (!hasTransformsPermission && !hasLegacyPermission) { + throw new SecurityException("Permission required to set display saturation level"); + } + final long token = Binder.clearCallingIdentity(); + try { + final Message message = mHandler.obtainMessage(MSG_APPLY_GLOBAL_SATURATION); + message.arg1 = level; + mHandler.sendMessage(message); + } finally { + Binder.restoreCallingIdentity(token); + } + return true; + } } } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index b1ba05c035b3..4a17c6591449 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -724,27 +724,6 @@ public final class DisplayManagerService extends SystemService { } } - private void setSaturationLevelInternal(float level) { - if (level < 0 || level > 1) { - throw new IllegalArgumentException("Saturation level must be between 0 and 1"); - } - float[] matrix = (level == 1.0f ? null : computeSaturationMatrix(level)); - DisplayTransformManager dtm = LocalServices.getService(DisplayTransformManager.class); - dtm.setColorMatrix(DisplayTransformManager.LEVEL_COLOR_MATRIX_SATURATION, matrix); - } - - private static float[] computeSaturationMatrix(float saturation) { - float desaturation = 1.0f - saturation; - float[] luminance = {0.231f * desaturation, 0.715f * desaturation, 0.072f * desaturation}; - float[] matrix = { - luminance[0] + saturation, luminance[0], luminance[0], 0, - luminance[1], luminance[1] + saturation, luminance[1], 0, - luminance[2], luminance[2], luminance[2] + saturation, 0, - 0, 0, 0, 1 - }; - return matrix; - } - private int createVirtualDisplayInternal(IVirtualDisplayCallback callback, IMediaProjection projection, int callingUid, String packageName, String name, int width, int height, int densityDpi, Surface surface, int flags, String uniqueId) { @@ -1836,19 +1815,6 @@ public final class DisplayManagerService extends SystemService { } @Override // Binder call - public void setSaturationLevel(float level) { - mContext.enforceCallingOrSelfPermission( - Manifest.permission.CONTROL_DISPLAY_SATURATION, - "Permission required to set display saturation level"); - final long token = Binder.clearCallingIdentity(); - try { - setSaturationLevelInternal(level); - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override // Binder call public int createVirtualDisplay(IVirtualDisplayCallback callback, IMediaProjection projection, String packageName, String name, int width, int height, int densityDpi, Surface surface, int flags, diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java index 1b0eb19df598..6f5a19612895 100644 --- a/services/core/java/com/android/server/hdmi/Constants.java +++ b/services/core/java/com/android/server/hdmi/Constants.java @@ -256,6 +256,41 @@ final class Constants { static final int USE_LAST_STATE_SYSTEM_AUDIO_CONTROL_ON_POWER_ON = 1; static final int NEVER_SYSTEM_AUDIO_CONTROL_ON_POWER_ON = 2; + // Port id to record local active port for Routing Control features + // They are used to map to corresponding Inputs + // Current interface is only implemented for specific device. + // Developers can add more port number and map them to corresponding inputs on demand. + @IntDef({ + CEC_SWITCH_HOME, + CEC_SWITCH_HDMI1, + CEC_SWITCH_HDMI2, + CEC_SWITCH_HDMI3, + CEC_SWITCH_HDMI4, + CEC_SWITCH_HDMI5, + CEC_SWITCH_HDMI6, + CEC_SWITCH_HDMI7, + CEC_SWITCH_HDMI8, + CEC_SWITCH_ARC, + CEC_SWITCH_BLUETOOTH, + CEC_SWITCH_OPTICAL, + CEC_SWITCH_AUX + }) + @interface LocalActivePort {} + static final int CEC_SWITCH_HOME = 0; + static final int CEC_SWITCH_HDMI1 = 1; + static final int CEC_SWITCH_HDMI2 = 2; + static final int CEC_SWITCH_HDMI3 = 3; + static final int CEC_SWITCH_HDMI4 = 4; + static final int CEC_SWITCH_HDMI5 = 5; + static final int CEC_SWITCH_HDMI6 = 6; + static final int CEC_SWITCH_HDMI7 = 7; + static final int CEC_SWITCH_HDMI8 = 8; + static final int CEC_SWITCH_ARC = 17; + static final int CEC_SWITCH_BLUETOOTH = 18; + static final int CEC_SWITCH_OPTICAL = 19; + static final int CEC_SWITCH_AUX = 20; + static final int CEC_SWITCH_PORT_MAX = 21; + static final String PROPERTY_PREFERRED_ADDRESS_AUDIO_SYSTEM = "persist.sys.hdmi.addr.audiosystem"; static final String PROPERTY_PREFERRED_ADDRESS_PLAYBACK = "persist.sys.hdmi.addr.playback"; @@ -274,6 +309,13 @@ final class Constants { static final String PROPERTY_SET_MENU_LANGUAGE = "ro.hdmi.set_menu_language"; /** + * Property to save the ARC port id on system audio device. + * <p>When ARC is initiated, this port will be used to turn on ARC. + */ + static final String PROPERTY_SYSTEM_AUDIO_DEVICE_ARC_PORT = + "ro.hdmi.property_sytem_audio_device_arc_port"; + + /** * Property to disable muting logic in System Audio Control handling. Default is true. * * <p>True means enabling muting logic. @@ -291,6 +333,24 @@ final class Constants { static final String PROPERTY_HDMI_CEC_NEVER_CLAIM_PLAYBACK_LOGICAL_ADDRESS = "ro.hdmi.property_hdmi_cec_never_claim_playback_logical_address"; + /** + * A comma separated list of logical addresses that HdmiControlService + * will never assign local CEC devices to. + * + * <p> This is useful when HDMI CEC hardware module can't assign multiple logical addresses + * in the range same range of 0-7 or 8-15. + */ + static final String PROPERTY_HDMI_CEC_NEVER_ASSIGN_LOGICAL_ADDRESSES = + "ro.hdmi.property_hdmi_cec_never_assign_logical_addresses"; + + /** + * Property to indicate if the current device is a cec switch device. + * + * <p> Default is false. + */ + static final String PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH = + "ro.hdmi.property_is_device_hdmi_cec_switch"; + // Set to false to allow playback device to go to suspend mode even // when it's an active source. True by default. static final String PROPERTY_KEEP_AWAKE = "persist.sys.hdmi.keep_awake"; @@ -331,13 +391,6 @@ final class Constants { "persist.sys.hdmi.property_sytem_audio_mode_audio_port"; /** - * Property to save the ARC port id on system audio device. - * <p>When ARC is initiated, this port will be used to turn on ARC. - */ - static final String PROPERTY_SYSTEM_AUDIO_DEVICE_ARC_PORT = - "persist.sys.hdmi.property_sytem_audio_device_arc_port"; - - /** * Property to indicate if a CEC audio device should forward volume keys when system audio mode * is off. * diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java index de6cdd48fb09..e777ce8166ac 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecController.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java @@ -22,20 +22,25 @@ import android.hardware.tv.cec.V1_0.SendMessageResult; import android.os.Handler; import android.os.Looper; import android.os.MessageQueue; +import android.os.SystemProperties; import android.util.Slog; import android.util.SparseArray; + import com.android.internal.util.IndentingPrintWriter; import com.android.server.hdmi.HdmiAnnotations.IoThreadOnly; import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; import com.android.server.hdmi.HdmiControlService.DevicePollingCallback; + +import libcore.util.EmptyArray; + import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.LinkedList; import java.util.List; -import java.util.function.Predicate; import java.util.concurrent.ArrayBlockingQueue; -import libcore.util.EmptyArray; +import java.util.function.Predicate; + import sun.util.locale.LanguageTag; /** @@ -112,10 +117,18 @@ final class HdmiCecController { private final NativeWrapper mNativeWrapperImpl; + /** List of logical addresses that should not be assigned to the current device. + * + * <p>Parsed from {@link Constants#PROPERTY_HDMI_CEC_NEVER_ASSIGN_LOGICAL_ADDRESSES} + */ + private final List<Integer> mNeverAssignLogicalAddresses; + // Private constructor. Use HdmiCecController.create(). private HdmiCecController(HdmiControlService service, NativeWrapper nativeWrapper) { mService = service; mNativeWrapperImpl = nativeWrapper; + mNeverAssignLogicalAddresses = mService.getIntList(SystemProperties.get( + Constants.PROPERTY_HDMI_CEC_NEVER_ASSIGN_LOGICAL_ADDRESSES)); } /** @@ -208,7 +221,8 @@ final class HdmiCecController { for (int i = 0; i < NUM_LOGICAL_ADDRESS; ++i) { int curAddress = (startAddress + i) % NUM_LOGICAL_ADDRESS; if (curAddress != Constants.ADDR_UNREGISTERED - && deviceType == HdmiUtils.getTypeFromAddress(curAddress)) { + && deviceType == HdmiUtils.getTypeFromAddress(curAddress) + && !mNeverAssignLogicalAddresses.contains(curAddress)) { boolean acked = false; for (int j = 0; j < HdmiConfig.ADDRESS_ALLOCATION_RETRY; ++j) { if (sendPollMessage(curAddress, curAddress, 1)) { diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java index c338e21105b8..528e0a4e4940 100755 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java @@ -30,6 +30,7 @@ import android.view.KeyEvent; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.IndentingPrintWriter; +import com.android.server.hdmi.Constants.LocalActivePort; import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; import com.android.server.hdmi.HdmiControlService.SendMessageCallback; @@ -1051,7 +1052,7 @@ abstract class HdmiCecLocalDevice { * <p>This check assumes target address is valid. * @param targetPhysicalAddress is the physical address of the target device * @return - * <p>If the target device is under the current device, return the port number of current device + * If the target device is under the current device, return the port number of current device * that the target device is connected to. * * <p>If the target device has the same physical address as the current device, return @@ -1085,4 +1086,27 @@ abstract class HdmiCecLocalDevice { } return TARGET_NOT_UNDER_LOCAL_DEVICE; } + + /** Calculates the physical address for {@code activePortId}. + * + * <p>This method assumes current device physical address is valid. + * <p>If the current device is already the leaf of the whole CEC system + * and can't have devices under it, will return its own physical address. + * + * @param activePortId is the local active port Id + * @return the calculated physical address of the port + */ + protected int getActivePathOnSwitchFromActivePortId(@LocalActivePort int activePortId) { + int myPhysicalAddress = mService.getPhysicalAddress(); + int finalMask = activePortId << 8; + int mask; + for (mask = 0x0F00; mask > 0x000F; mask >>= 4) { + if ((myPhysicalAddress & mask) == 0) { + break; + } else { + finalMask >>= 4; + } + } + return finalMask | myPhysicalAddress; + } } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java index 20908b6d21e2..048a0e413a9b 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java @@ -20,10 +20,12 @@ import static com.android.server.hdmi.Constants.PROPERTY_SYSTEM_AUDIO_CONTROL_ON import static com.android.server.hdmi.Constants.USE_LAST_STATE_SYSTEM_AUDIO_CONTROL_ON_POWER_ON; import android.annotation.Nullable; +import android.content.Intent; import android.hardware.hdmi.HdmiDeviceInfo; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.AudioSystem; +import android.media.tv.TvContract; import android.os.SystemProperties; import android.provider.Settings.Global; @@ -32,6 +34,8 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.server.hdmi.Constants.AudioCodec; import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; +import java.util.HashMap; + /** * Represent a logical device of type {@link HdmiDeviceInfo#DEVICE_AUDIO_SYSTEM} residing in Android * system. @@ -60,16 +64,16 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { // AVR as audio receiver. @ServiceThreadOnly private boolean mArcEstablished = false; - /** - * Return value of {@link #getLocalPortFromPhysicalAddress(int)} - */ - private static final int TARGET_NOT_UNDER_LOCAL_DEVICE = -1; - private static final int TARGET_SAME_PHYSICAL_ADDRESS = 0; + // If the current device uses TvInput for ARC. We assume all other inputs also use TvInput + // when ARC is using TvInput. + private boolean mArcIntentUsed = SystemProperties + .get(Constants.PROPERTY_SYSTEM_AUDIO_DEVICE_ARC_PORT, "0").contains("tvinput"); - // Local active port number used for Routing Control. - // Default 0 means HOME is the current active path. Temp solution only. - // TODO(amyjojo): adding system constants for Atom inputs port and TIF mapping. - private int mLocalActivePath = 0; + // Keeps the mapping (HDMI port ID to TV input URI) to keep track of the TV inputs ready to + // accept input switching request from HDMI devices. Requests for which the corresponding + // input ID is not yet registered by TV input framework need to be buffered for delayed + // processing. + private final HashMap<Integer, String> mTvInputs = new HashMap<>(); protected HdmiCecLocalDeviceAudioSystem(HdmiControlService service) { super(service, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); @@ -81,6 +85,13 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, true); mAutoTvOff = mService.readBooleanSetting( Global.HDMI_CONTROL_AUTO_TV_OFF_ENABLED, true); + // TODO(amyjojo): make the map ro property. + mTvInputs.put(Constants.CEC_SWITCH_HDMI1, + "com.droidlogic.tvinput/.services.Hdmi1InputService/HW5"); + mTvInputs.put(Constants.CEC_SWITCH_HDMI2, + "com.droidlogic.tvinput/.services.Hdmi2InputService/HW6"); + mTvInputs.put(Constants.CEC_SWITCH_HDMI3, + "com.droidlogic.tvinput/.services.Hdmi3InputService/HW7"); } @Override @@ -595,4 +606,135 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { assertRunOnServiceThread(); mAutoDeviceOff = autoDeviceOff; } + + @Override + protected void switchInputOnReceivingNewActivePath(int physicalAddress) { + int port = getLocalPortFromPhysicalAddress(physicalAddress); + // Wake up if the new Active Source is the current device or under it + // or if System Audio Control is enabled. + if ((isSystemAudioActivated() || port >= 0) && mService.isPowerStandbyOrTransient()) { + mService.wakeUp(); + } + + if (isSystemAudioActivated() && port < 0) { + // If system audio mode is on and the new active source is not under the current device, + // Will switch to ARC input. + // TODO(b/115637145): handle system aduio without ARC + routeToInputFromPortId(Constants.CEC_SWITCH_ARC); + } else if (mIsSwitchDevice && port >= 0) { + // If current device is a switch and the new active source is under it, + // will switch to the corresponding active path. + routeToInputFromPortId(port); + } + } + + protected void routeToInputFromPortId(int portId) { + if (mArcIntentUsed) { + routeToTvInputFromPortId(portId); + } else { + // TODO(): implement input switching for devices not using TvInput. + } + } + + protected void routeToTvInputFromPortId(int portId) { + if (portId < 0 || portId >= Constants.CEC_SWITCH_PORT_MAX) { + HdmiLogger.debug("Invalid port number for Tv Input switching."); + return; + } + // TODO(amyjojo): handle if switching to the current input + if (portId == Constants.CEC_SWITCH_HOME && mService.isPlaybackDevice()) { + switchToHomeTvInput(); + } else if (portId == Constants.CEC_SWITCH_ARC) { + switchToTvInput(SystemProperties.get(Constants.PROPERTY_SYSTEM_AUDIO_DEVICE_ARC_PORT)); + return; + } else { + String uri = mTvInputs.get(portId); + if (uri != null) { + switchToTvInput(mTvInputs.get(portId)); + } else { + HdmiLogger.debug("Port number does not match any Tv Input."); + return; + } + } + + setLocalActivePort(portId); + } + + // For device to switch to specific TvInput with corresponding URI. + private void switchToTvInput(String uri) { + mService.getContext().startActivity(new Intent(Intent.ACTION_VIEW, + TvContract.buildChannelUriForPassthroughInput(uri)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + // For device using TvInput to switch to Home. + private void switchToHomeTvInput() { + Intent activityIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_SINGLE_TOP + | Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_NO_ANIMATION); + mService.getContext().startActivity(activityIntent); + } + + @Override + protected void handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message) { + int port = getLocalPortFromPhysicalAddress(physicalAddress); + // Routing change or information sent from switches under the current device can be ignored. + if (port > 0) { + return; + } + // When other switches route to some other devices not under the current device, + // check system audio mode status and do ARC switch if needed. + if (port < 0 && isSystemAudioActivated()) { + handleRoutingChangeAndInformationForSystemAudio(); + return; + } + // When other switches route to the current device + // and the current device is also a switch. + if (port == 0) { + handleRoutingChangeAndInformationForSwitch(message); + } + } + + // Handle the system audio(ARC) part of the logic on receiving routing change or information. + private void handleRoutingChangeAndInformationForSystemAudio() { + if (mService.isPowerStandbyOrTransient()) { + mService.wakeUp(); + } + // TODO(b/115637145): handle system aduio without ARC + routeToInputFromPortId(Constants.CEC_SWITCH_ARC); + } + + // Handle the routing control part of the logic on receiving routing change or information. + private void handleRoutingChangeAndInformationForSwitch(HdmiCecMessage message) { + if (mService.isPowerStandbyOrTransient()) { + mService.wakeUp(); + } + if (getLocalActivePort() == Constants.CEC_SWITCH_HOME && mService.isPlaybackDevice()) { + routeToInputFromPortId(Constants.CEC_SWITCH_HOME); + if (mService.playback() != null) { + mService.playback().setAndBroadcastActiveSource( + message, mService.getPhysicalAddress()); + } else { + setAndBroadcastActiveSource(message, mService.getPhysicalAddress()); + } + return; + } + + int routingInformationPath = + getActivePathOnSwitchFromActivePortId(getLocalActivePort()); + // If current device is already the leaf of the whole HDMI system, will do nothing. + if (routingInformationPath == mService.getPhysicalAddress()) { + HdmiLogger.debug("Current device can't assign valid physical address" + + "to devices under it any more. " + + "It's physical address is " + routingInformationPath); + return; + } + // Otherwise will switch to the current active port and broadcast routing information. + mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingInformation( + mAddress, routingInformationPath)); + routeToInputFromPortId(getLocalActivePort()); + } } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java index e9dd6822366e..379cc1610197 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java @@ -266,7 +266,8 @@ final class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource { setIsActiveSource(physicalAddress == mService.getPhysicalAddress()); } - private void wakeUpIfActiveSource() { + @Override + protected void wakeUpIfActiveSource() { if (!mIsActiveSource) { return; } @@ -277,7 +278,8 @@ final class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource { } } - private void maySendActiveSource(int dest) { + @Override + protected void maySendActiveSource(int dest) { if (mIsActiveSource) { mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource( mAddress, mService.getPhysicalAddress())); diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java index fed66225f839..8d55299243b2 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java @@ -19,8 +19,12 @@ package com.android.server.hdmi; import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.IHdmiControlCallback; import android.os.RemoteException; +import android.os.SystemProperties; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.hdmi.Constants.LocalActivePort; import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; import java.util.List; @@ -35,6 +39,20 @@ abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice { // Indicate if current device is Active Source or not private boolean mIsActiveSource = false; + // Device has cec switch functionality or not. + // Default is false. + protected boolean mIsSwitchDevice = SystemProperties.getBoolean( + Constants.PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH, false); + + // Local active port number used for Routing Control. + // This records the default active port or the previous valid active port. + // Default is HOME input. + // Note that we don't save active path here because for source device, + // new Active Source physical address might not match the local active path + @GuardedBy("mLock") + @LocalActivePort + private int mLocalActivePort = Constants.CEC_SWITCH_HOME; + protected HdmiCecLocalDeviceSource(HdmiControlService service, int deviceType) { super(service, deviceType); } @@ -99,6 +117,7 @@ abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice { setActiveSource(activeSource); } setIsActiveSource(physicalAddress == mService.getPhysicalAddress()); + switchInputOnReceivingNewActivePath(physicalAddress); return true; } @@ -106,16 +125,130 @@ abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice { @ServiceThreadOnly protected boolean handleRequestActiveSource(HdmiCecMessage message) { assertRunOnServiceThread(); - if (mIsActiveSource) { - mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource( - mAddress, mService.getPhysicalAddress())); + maySendActiveSource(message.getSource()); + return true; + } + + @Override + @ServiceThreadOnly + protected boolean handleSetStreamPath(HdmiCecMessage message) { + assertRunOnServiceThread(); + int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); + // If current device is the target path, set to Active Source. + // If the path is under the current device, should switch + if (physicalAddress == mService.getPhysicalAddress() && mService.isPlaybackDevice()) { + setAndBroadcastActiveSource(message, physicalAddress); + } + switchInputOnReceivingNewActivePath(physicalAddress); + return true; + } + + @Override + @ServiceThreadOnly + protected boolean handleRoutingChange(HdmiCecMessage message) { + assertRunOnServiceThread(); + int newPath = HdmiUtils.twoBytesToInt(message.getParams(), 2); + // if the current device is a pure playback device + if (!mIsSwitchDevice + && newPath == mService.getPhysicalAddress() + && mService.isPlaybackDevice()) { + setAndBroadcastActiveSource(message, newPath); } + handleRoutingChangeAndInformation(newPath, message); return true; } + @Override + @ServiceThreadOnly + protected boolean handleRoutingInformation(HdmiCecMessage message) { + assertRunOnServiceThread(); + int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); + // if the current device is a pure playback device + if (!mIsSwitchDevice + && physicalAddress == mService.getPhysicalAddress() + && mService.isPlaybackDevice()) { + setAndBroadcastActiveSource(message, physicalAddress); + } + handleRoutingChangeAndInformation(physicalAddress, message); + return true; + } + + // Method to switch Input with the new Active Path. + // All the devices with Switch functionality should implement this. + protected void switchInputOnReceivingNewActivePath(int physicalAddress) { + // do nothing + } + + // Source device with Switch functionality should implement this method. + // TODO(): decide which type will handle the routing when multi device type is supported + protected void handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message) { + // do nothing + } + + // Active source claiming needs to be handled in the parent class + // since we decide who will be the active source when the device supports + // multiple device types in this method. + // This method should only be called when the device can be the active source. + protected void setAndBroadcastActiveSource(HdmiCecMessage message, int physicalAddress) { + // If the device has both playback and audio system logical addresses, + // playback will claim active source. Otherwise audio system will. + HdmiCecLocalDevice deviceToBeActiveSource = mService.playback(); + if (deviceToBeActiveSource == null) { + deviceToBeActiveSource = mService.audioSystem(); + } + if (this == deviceToBeActiveSource) { + ActiveSource activeSource = ActiveSource.of(mAddress, physicalAddress); + setIsActiveSource(true); + setActiveSource(activeSource); + wakeUpIfActiveSource(); + maySendActiveSource(message.getSource()); + } + } + @ServiceThreadOnly void setIsActiveSource(boolean on) { assertRunOnServiceThread(); mIsActiveSource = on; } + + @ServiceThreadOnly + // Check if current device is the Active Source + boolean isActiveSource() { + assertRunOnServiceThread(); + return mIsActiveSource; + } + + protected void wakeUpIfActiveSource() { + if (!mIsActiveSource) { + return; + } + // Wake up the device if the power is in standby mode + if (mService.isPowerStandbyOrTransient()) { + mService.wakeUp(); + } + return; + } + + protected void maySendActiveSource(int dest) { + if (mIsActiveSource) { + mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource( + mAddress, mService.getPhysicalAddress())); + } + } + + @VisibleForTesting + protected void setLocalActivePort(@LocalActivePort int portId) { + synchronized (mLock) { + mLocalActivePort = portId; + } + } + + // To get the local active port to switch to + // when receivng routing change or information. + @LocalActivePort + protected int getLocalActivePort() { + synchronized (mLock) { + return mLocalActivePort; + } + } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 7e369598a9b1..903045d9b68f 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -417,7 +417,7 @@ public class HdmiControlService extends SystemService { mSettingsObserver = new SettingsObserver(mHandler); } - private static List<Integer> getIntList(String string) { + protected static List<Integer> getIntList(String string) { ArrayList<Integer> list = new ArrayList<>(); TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(','); splitter.setString(string); @@ -2114,11 +2114,15 @@ public class HdmiControlService extends SystemService { return mLocalDevices.contains(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); } + boolean isPlaybackDevice() { + return mLocalDevices.contains(HdmiDeviceInfo.DEVICE_PLAYBACK); + } + boolean isTvDeviceEnabled() { return isTvDevice() && tv() != null; } - private HdmiCecLocalDevicePlayback playback() { + protected HdmiCecLocalDevicePlayback playback() { return (HdmiCecLocalDevicePlayback) mCecController.getLocalDevice(HdmiDeviceInfo.DEVICE_PLAYBACK); } diff --git a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java index 48c71d8b738a..46611dd1b66e 100644 --- a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java +++ b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java @@ -85,7 +85,11 @@ final class OneTouchPlayAction extends HdmiCecFeatureAction { private void broadcastActiveSource() { sendCommand(HdmiCecMessageBuilder.buildActiveSource(getSourceAddress(), getSourcePath())); // Because only source device can create this action, it's safe to cast. - source().setIsActiveSource(true); + HdmiCecLocalDeviceSource source = source(); + source.setIsActiveSource(true); + source.setActiveSource(getSourceAddress(), getSourcePath()); + // Set local active port to HOME when One Touch Play. + source.setLocalActivePort(Constants.CEC_SWITCH_HOME); } private void queryDevicePowerStatus() { diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index e7c3c7bbe21b..45a9bc01223c 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -213,8 +213,6 @@ public class InputManagerService extends IInputManager.Stub private static native void nativeSetFocusedApplication(long ptr, int displayId, InputApplicationHandle application); private static native void nativeSetFocusedDisplay(long ptr, int displayId); - private static native boolean nativeTransferTouchFocus(long ptr, - InputChannel fromChannel, InputChannel toChannel); private static native void nativeSetPointerSpeed(long ptr, int speed); private static native void nativeSetShowTouches(long ptr, boolean enabled); private static native void nativeSetInteractive(long ptr, boolean interactive); @@ -1485,29 +1483,6 @@ public class InputManagerService extends IInputManager.Stub nativeSetSystemUiVisibility(mPtr, visibility); } - /** - * Atomically transfers touch focus from one window to another as identified by - * their input channels. It is possible for multiple windows to have - * touch focus if they support split touch dispatch - * {@link android.view.WindowManager.LayoutParams#FLAG_SPLIT_TOUCH} but this - * method only transfers touch focus of the specified window without affecting - * other windows that may also have touch focus at the same time. - * @param fromChannel The channel of a window that currently has touch focus. - * @param toChannel The channel of the window that should receive touch focus in - * place of the first. - * @return True if the transfer was successful. False if the window with the - * specified channel did not actually have touch focus at the time of the request. - */ - public boolean transferTouchFocus(InputChannel fromChannel, InputChannel toChannel) { - if (fromChannel == null) { - throw new IllegalArgumentException("fromChannel must not be null."); - } - if (toChannel == null) { - throw new IllegalArgumentException("toChannel must not be null."); - } - return nativeTransferTouchFocus(mPtr, fromChannel, toChannel); - } - @Override // Binder call public void tryPointerSpeed(int speed) { if (!checkCallingPermission(android.Manifest.permission.SET_POINTER_SPEED, @@ -1786,14 +1761,13 @@ public class InputManagerService extends IInputManager.Stub // Native callback private void notifyFocusChanged(IBinder token) { - if (mFocusedWindow != token) { - if (mFocusedWindowHasCapture) { - setPointerCapture(false); - } - if (token instanceof IWindow) { - mFocusedWindow = (IWindow) token; - } + if (mFocusedWindow.asBinder() == token) { + Log.w(TAG, "notifyFocusChanged called with unchanged mFocusedWindow=" + mFocusedWindow); + return; } + + setPointerCapture(false); + mFocusedWindow = IWindow.Stub.asInterface(token); } // Native callback. diff --git a/services/core/java/com/android/server/pm/ComponentResolver.java b/services/core/java/com/android/server/pm/ComponentResolver.java index 3b11525e7cda..8facce112b52 100644 --- a/services/core/java/com/android/server/pm/ComponentResolver.java +++ b/services/core/java/com/android/server/pm/ComponentResolver.java @@ -42,6 +42,7 @@ import android.content.pm.ServiceInfo; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.DebugUtils; import android.util.Log; import android.util.LogPrinter; import android.util.Pair; @@ -60,6 +61,7 @@ import java.util.Set; /** Resolves all Android component types [activities, services, providers and receivers]. */ public class ComponentResolver { + private static final boolean DEBUG = false; private static final String TAG = "PackageManager"; private static final boolean DEBUG_FILTERS = false; private static final boolean DEBUG_SHOW_INFO = false; @@ -1198,22 +1200,48 @@ public class ComponentResolver { return packageName.equals(info.activity.owner.packageName); } + private void log(String reason, ActivityIntentInfo info, int match, + int userId) { + Slog.w(TAG, reason + + "; match: " + + DebugUtils.flagsToString(IntentFilter.class, "MATCH_", match) + + "; userId: " + userId + + "; intent info: " + info); + } + @Override protected ResolveInfo newResult(PackageParser.ActivityIntentInfo info, int match, int userId) { - if (!sUserManager.exists(userId)) return null; + if (!sUserManager.exists(userId)) { + if (DEBUG) { + log("User doesn't exist", info, match, userId); + } + return null; + } if (!sPackageManagerInternal.isEnabledAndMatches(info.activity.info, mFlags, userId)) { + if (DEBUG) { + log("!PackageManagerInternal.isEnabledAndMatches; mFlags=" + + DebugUtils.flagsToString(PackageManager.class, "MATCH_", mFlags), + info, match, userId); + } return null; } final PackageParser.Activity activity = info.activity; PackageSetting ps = (PackageSetting) activity.owner.mExtras; if (ps == null) { + if (DEBUG) { + log("info.activity.owner.mExtras == null", info, match, userId); + } return null; } final PackageUserState userState = ps.readUserState(userId); ActivityInfo ai = PackageParser.generateActivityInfo(activity, mFlags, userState, userId); if (ai == null) { + if (DEBUG) { + log("Failed to create ActivityInfo based on " + info.activity, info, match, + userId); + } return null; } final boolean matchExplicitlyVisibleOnly = @@ -1227,15 +1255,31 @@ public class ComponentResolver { final boolean matchInstantApp = (mFlags & PackageManager.MATCH_INSTANT) != 0; // throw out filters that aren't visible to ephemeral apps if (matchVisibleToInstantApp && !(componentVisible || userState.instantApp)) { + if (DEBUG) { + log("Filter(s) not visible to ephemeral apps" + + "; matchVisibleToInstantApp=" + matchVisibleToInstantApp + + "; matchInstantApp=" + matchInstantApp + + "; info.isVisibleToInstantApp()=" + info.isVisibleToInstantApp() + + "; matchExplicitlyVisibleOnly=" + matchExplicitlyVisibleOnly + + "; info.isExplicitlyVisibleToInstantApp()=" + + info.isExplicitlyVisibleToInstantApp(), + info, match, userId); + } return null; } // throw out instant app filters if we're not explicitly requesting them if (!matchInstantApp && userState.instantApp) { + if (DEBUG) { + log("Instant app filter is not explicitly requested", info, match, userId); + } return null; } // throw out instant app filters if updates are available; will trigger // instant app resolution if (userState.instantApp && ps.isUpdateAvailable()) { + if (DEBUG) { + log("Instant app update is available", info, match, userId); + } return null; } final ResolveInfo res = new ResolveInfo(); diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 33019627efda..2dc543712f0c 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -13562,7 +13562,8 @@ public class PackageManagerService extends IPackageManager.Stub } Signature[] callerSignature; - Object obj = mSettings.getSettingLPr(callingUid); + final int appId = UserHandle.getAppId(callingUid); + final Object obj = mSettings.getSettingLPr(appId); if (obj != null) { if (obj instanceof SharedUserSetting) { callerSignature = diff --git a/services/core/java/com/android/server/policy/role/LegacyRoleResolutionPolicy.java b/services/core/java/com/android/server/policy/role/LegacyRoleResolutionPolicy.java new file mode 100644 index 000000000000..45c975b26956 --- /dev/null +++ b/services/core/java/com/android/server/policy/role/LegacyRoleResolutionPolicy.java @@ -0,0 +1,97 @@ +/* + * 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. + */ + +package com.android.server.policy.role; + +import android.annotation.NonNull; +import android.app.role.RoleManager; +import android.content.Context; +import android.os.Debug; +import android.provider.Settings; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.telephony.SmsApplication; +import com.android.internal.util.CollectionUtils; +import com.android.server.role.RoleManagerService; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Logic to retrieve the various legacy(pre-Q) equivalents of role holders. + * + * Unlike {@link RoleManagerService} this is meant to be pretty high-level to allow for depending + * on all kinds of various systems that are historically involved in legacy role resolution, + * e.g. {@link SmsApplication} + * + * @see RoleManagerService#migrateRoleIfNecessary + */ +public class LegacyRoleResolutionPolicy implements RoleManagerService.RoleHoldersResolver { + + private static final boolean DEBUG = false; + private static final String LOG_TAG = "LegacyRoleResolutionPol"; + + @NonNull + private final Context mContext; + + public LegacyRoleResolutionPolicy(Context context) { + mContext = context; + } + + @Override + public List<String> getRoleHolders(String roleName, int userId) { + switch (roleName) { + case RoleManager.ROLE_SMS: { + // Moved over from SmsApplication#getApplication + String result = Settings.Secure.getStringForUser( + mContext.getContentResolver(), + Settings.Secure.SMS_DEFAULT_APPLICATION, userId); + + if (result == null) { + Collection<SmsApplication.SmsApplicationData> applications = + SmsApplication.getApplicationCollectionAsUser(mContext, userId); + SmsApplication.SmsApplicationData applicationData; + String defaultPackage = mContext.getResources() + .getString(com.android.internal.R.string.default_sms_application); + applicationData = + SmsApplication.getApplicationForPackage(applications, defaultPackage); + + if (applicationData == null) { + // Are there any applications? + if (applications.size() != 0) { + applicationData = + (SmsApplication.SmsApplicationData) applications.toArray()[0]; + } + } + if (DEBUG) { + Log.i(LOG_TAG, "Found default sms app: " + applicationData + + " among: " + applications + " from " + Debug.getCallers(4)); + } + SmsApplication.SmsApplicationData app = applicationData; + result = app == null ? null : app.mPackageName; + } + + return CollectionUtils.singletonOrEmpty(result); + } + default: { + Slog.e(LOG_TAG, "Don't know how to find legacy role holders for " + roleName); + return Collections.emptyList(); + } + } + } +} diff --git a/services/core/java/com/android/server/power/OWNERS b/services/core/java/com/android/server/power/OWNERS index 244ccb69e958..5cbe74c5986c 100644 --- a/services/core/java/com/android/server/power/OWNERS +++ b/services/core/java/com/android/server/power/OWNERS @@ -1,6 +1,4 @@ michaelwr@google.com santoscordon@google.com -per-file BatterySaverPolicy.java=omakoto@google.com -per-file ShutdownThread.java=fkupolov@google.com -per-file ThermalManagerService.java=wvw@google.com
\ No newline at end of file +per-file ThermalManagerService.java=wvw@google.com diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 565bb706a5d8..a02787308246 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -96,6 +96,7 @@ import com.android.server.lights.Light; import com.android.server.lights.LightsManager; import com.android.server.policy.WindowManagerPolicy; import com.android.server.power.batterysaver.BatterySaverController; +import com.android.server.power.batterysaver.BatterySaverPolicy; import com.android.server.power.batterysaver.BatterySaverStateMachine; import com.android.server.power.batterysaver.BatterySavingStats; diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java index 6400c88b320f..1d74350de978 100644 --- a/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java +++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java @@ -35,13 +35,13 @@ import android.util.ArrayMap; import android.util.Slog; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; import com.android.server.EventLogTags; import com.android.server.LocalServices; -import com.android.server.power.BatterySaverPolicy; -import com.android.server.power.BatterySaverPolicy.BatterySaverPolicyListener; import com.android.server.power.PowerManagerService; +import com.android.server.power.batterysaver.BatterySaverPolicy.BatterySaverPolicyListener; import com.android.server.power.batterysaver.BatterySavingStats.BatterySaverState; import com.android.server.power.batterysaver.BatterySavingStats.DozeState; import com.android.server.power.batterysaver.BatterySavingStats.InteractiveState; @@ -158,10 +158,9 @@ public class BatterySaverController implements BatterySaverPolicyListener { mBatterySavingStats = batterySavingStats; // Initialize plugins. - final ArrayList<Plugin> plugins = new ArrayList<>(); - plugins.add(new BatterySaverLocationPlugin(mContext)); - - mPlugins = plugins.toArray(new Plugin[plugins.size()]); + mPlugins = new Plugin[] { + new BatterySaverLocationPlugin(mContext) + }; } /** @@ -217,7 +216,7 @@ public class BatterySaverController implements BatterySaverPolicyListener { super(looper); } - public void postStateChanged(boolean sendBroadcast, int reason) { + void postStateChanged(boolean sendBroadcast, int reason) { obtainMessage(MSG_STATE_CHANGED, sendBroadcast ? ARG_SEND_BROADCAST : ARG_DONT_SEND_BROADCAST, reason).sendToTarget(); } @@ -244,9 +243,8 @@ public class BatterySaverController implements BatterySaverPolicyListener { } } - /** - * Called by {@link PowerManagerService} to update the battery saver state. - */ + /** Enable or disable full battery saver. */ + @VisibleForTesting public void enableBatterySaver(boolean enable, int reason) { synchronized (mLock) { if (mEnabled == enable) { @@ -311,7 +309,7 @@ public class BatterySaverController implements BatterySaverPolicyListener { reason); mPreviouslyEnabled = mEnabled; - listeners = mListeners.toArray(new LowPowerModeListener[mListeners.size()]); + listeners = mListeners.toArray(new LowPowerModeListener[0]); enabled = mEnabled; mIsInteractive = isInteractive; diff --git a/services/core/java/com/android/server/power/BatterySaverPolicy.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java index cedb54801dc8..48a041eaab28 100644 --- a/services/core/java/com/android/server/power/BatterySaverPolicy.java +++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.server.power; +package com.android.server.power.batterysaver; import android.content.ContentResolver; import android.content.Context; @@ -36,21 +36,19 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.internal.util.ConcurrentUtils; -import com.android.server.power.batterysaver.BatterySavingStats; -import com.android.server.power.batterysaver.CpuFrequencies; +import com.android.server.power.PowerManagerService; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; /** - * Class to decide whether to turn on battery saver mode for specific service + * Class to decide whether to turn on battery saver mode for specific services. * * IMPORTANT: This class shares the power manager lock, which is very low in the lock hierarchy. * Do not call out with the lock held, such as AccessibilityManager. (Settings provider is okay.) * - * Test: - atest ${ANDROID_BUILD_TOP}/frameworks/base/services/tests/servicestests/src/com/android/server/power/BatterySaverPolicyTest.java + * Test: atest com.android.server.power.batterysaver.BatterySaverPolicyTest.java */ public class BatterySaverPolicy extends ContentObserver { private static final String TAG = "BatterySaverPolicy"; @@ -200,8 +198,10 @@ public class BatterySaverPolicy extends ContentObserver { onChange(true, null); } + @VisibleForTesting public void addListener(BatterySaverPolicyListener listener) { synchronized (mLock) { + // TODO: set this in the constructor instead mListeners.add(listener); } } @@ -244,7 +244,7 @@ public class BatterySaverPolicy extends ContentObserver { // Update. updateConstantsLocked(setting, deviceSpecificSetting); - listeners = mListeners.toArray(new BatterySaverPolicyListener[mListeners.size()]); + listeners = mListeners.toArray(new BatterySaverPolicyListener[0]); } // Notify the listeners. diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java index a60f16d7ca27..b7f28da499e9 100644 --- a/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java +++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java @@ -37,7 +37,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.server.EventLogTags; -import com.android.server.power.BatterySaverPolicy; import com.android.server.power.BatterySaverStateMachineProto; import java.io.PrintWriter; @@ -229,8 +228,8 @@ public class BatterySaverStateMachine { } /** - * Run a {@link Runnable} on a background handler, but lazily. If the same {@link Runnable}, - * it'll be first removed before a new one is posted. + * Run a {@link Runnable} on a background handler, but lazily. If the same {@link Runnable} is + * already registered, it'll be first removed before being re-posted. */ @VisibleForTesting void runOnBgThreadLazy(Runnable r, int delayMillis) { diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySavingStats.java b/services/core/java/com/android/server/power/batterysaver/BatterySavingStats.java index 821320559017..79b44ebd8645 100644 --- a/services/core/java/com/android/server/power/batterysaver/BatterySavingStats.java +++ b/services/core/java/com/android/server/power/batterysaver/BatterySavingStats.java @@ -29,7 +29,6 @@ import com.android.internal.logging.nano.MetricsProto; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.server.EventLogTags; import com.android.server.LocalServices; -import com.android.server.power.BatterySaverPolicy; import java.io.PrintWriter; import java.text.SimpleDateFormat; diff --git a/services/core/java/com/android/server/role/RoleManagerService.java b/services/core/java/com/android/server/role/RoleManagerService.java index f37ca12bbd7f..b4883371b468 100644 --- a/services/core/java/com/android/server/role/RoleManagerService.java +++ b/services/core/java/com/android/server/role/RoleManagerService.java @@ -35,6 +35,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManagerInternal; import android.content.pm.Signature; +import android.os.Binder; import android.os.Handler; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -92,6 +93,15 @@ public class RoleManagerService extends SystemService implements RoleUserState.C @NonNull private final Object mLock = new Object(); + @NonNull + private final RoleHoldersResolver mLegacyRoleResolver; + + /** @see #getRoleHolders(String, int) */ + public interface RoleHoldersResolver { + /** @return a list of packages that hold a given role for a given user */ + List<String> getRoleHolders(String roleName, int userId); + } + /** * Maps user id to its state. */ @@ -118,9 +128,12 @@ public class RoleManagerService extends SystemService implements RoleUserState.C @NonNull private final Handler mListenerHandler = FgThread.getHandler(); - public RoleManagerService(@NonNull Context context) { + public RoleManagerService(@NonNull Context context, + @NonNull RoleHoldersResolver legacyRoleResolver) { super(context); + mLegacyRoleResolver = legacyRoleResolver; + mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); mAppOpsManager = context.getSystemService(AppOpsManager.class); @@ -175,10 +188,17 @@ public class RoleManagerService extends SystemService implements RoleUserState.C private void performInitialGrantsIfNecessary(@UserIdInt int userId) { RoleUserState userState; userState = getOrCreateUserState(userId); + String packagesHash = computeComponentStateHash(userId); String oldPackagesHash = userState.getPackagesHash(); boolean needGrant = !Objects.equals(packagesHash, oldPackagesHash); if (needGrant) { + + //TODO gradually add more role migrations statements here for remaining roles + // Make sure to implement LegacyRoleResolutionPolicy#getRoleHolders + // for a given role before adding a migration statement for it here + migrateRoleIfNecessary(RoleManager.ROLE_SMS, userId); + // Some vital packages state has changed since last role grant // Run grants again Slog.i(LOG_TAG, "Granting default permissions..."); @@ -205,6 +225,20 @@ public class RoleManagerService extends SystemService implements RoleUserState.C } } + private void migrateRoleIfNecessary(String role, @UserIdInt int userId) { + // Any role for which we have a record are already migrated + RoleUserState userState = getOrCreateUserState(userId); + if (!userState.isRoleAvailable(role)) { + userState.addRoleName(role); + List<String> roleHolders = mLegacyRoleResolver.getRoleHolders(role, userId); + Slog.i(LOG_TAG, "Migrating " + role + ", legacy holders: " + roleHolders); + int size = roleHolders.size(); + for (int i = 0; i < size; i++) { + userState.addRoleHolder(role, roleHolders.get(i)); + } + } + } + @Nullable private static String computeComponentStateHash(@UserIdInt int userId) { PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class); @@ -372,6 +406,7 @@ public class RoleManagerService extends SystemService implements RoleUserState.C @Nullable private ArraySet<String> getRoleHoldersInternal(@NonNull String roleName, @UserIdInt int userId) { + migrateRoleIfNecessary(roleName, userId); RoleUserState userState = getOrCreateUserState(userId); return userState.getRoleHolders(roleName); } @@ -530,6 +565,17 @@ public class RoleManagerService extends SystemService implements RoleUserState.C } @Override + public String getDefaultSmsPackage(int userId) { + long identity = Binder.clearCallingIdentity(); + try { + return CollectionUtils.firstOrNull( + getRoleHoldersAsUser(RoleManager.ROLE_SMS, userId)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout, @Nullable String[] args) { if (!DumpUtils.checkDumpPermission(getContext(), LOG_TAG, fout)) { diff --git a/services/core/java/com/android/server/role/RoleUserState.java b/services/core/java/com/android/server/role/RoleUserState.java index 630a39caeb08..69e144951154 100644 --- a/services/core/java/com/android/server/role/RoleUserState.java +++ b/services/core/java/com/android/server/role/RoleUserState.java @@ -205,6 +205,28 @@ public class RoleUserState { } /** + * Adds the given role, effectively marking it as {@link #isRoleAvailable available} + * + * @param roleName the name of the role + * + * @return whether any changes were made + */ + public boolean addRoleName(@NonNull String roleName) { + synchronized (mLock) { + throwIfDestroyedLocked(); + + if (!mRoles.containsKey(roleName)) { + mRoles.put(roleName, new ArraySet<>()); + Slog.i(LOG_TAG, "Added new role: " + roleName); + scheduleWriteFileLocked(); + return true; + } else { + return false; + } + } + } + + /** * Set the names of all available roles. * * @param roleNames the names of all the available roles @@ -231,13 +253,7 @@ public class RoleUserState { int roleNamesSize = roleNames.size(); for (int i = 0; i < roleNamesSize; i++) { - String roleName = roleNames.get(i); - - if (!mRoles.containsKey(roleName)) { - mRoles.put(roleName, new ArraySet<>()); - Slog.i(LOG_TAG, "Added new role: " + roleName); - changed = true; - } + changed |= addRoleName(roleNames.get(i)); } if (changed) { diff --git a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java index 2cab63aa680a..ef771406805b 100644 --- a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java +++ b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java @@ -510,7 +510,8 @@ public final class TextClassificationManagerService extends ITextClassifierServi Slog.d(LOG_TAG, "Binding to " + serviceIntent.getComponent()); willBind = mContext.bindServiceAsUser( serviceIntent, mConnection, - Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, + Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE + | Context.BIND_RESTRICT_ASSOCIATIONS, UserHandle.of(mUserId)); mBinding = willBind; } finally { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 6f8f85f1af26..f8f0d1cfcac8 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -390,6 +390,11 @@ final class ActivityRecord extends ConfigurationContainer { private boolean mTurnScreenOn; /** + * Current sequencing integer of the configuration, for skipping old activity configurations. + */ + private int mConfigurationSeq; + + /** * Temp configs used in {@link #ensureActivityConfiguration(int, boolean)} */ private final Configuration mTmpConfig = new Configuration(); @@ -2568,6 +2573,45 @@ final class ActivityRecord extends ConfigurationContainer { onRequestedOverrideConfigurationChanged(mTmpConfig); } + @Override + void resolveOverrideConfiguration(Configuration newParentConfiguration) { + super.resolveOverrideConfiguration(newParentConfiguration); + + // Assign configuration sequence number into hierarchy because there is a different way than + // ensureActivityConfiguration() in this class that uses configuration in WindowState during + // layout traversals. + mConfigurationSeq = Math.max(++mConfigurationSeq, 1); + getResolvedOverrideConfiguration().seq = mConfigurationSeq; + } + + @Override + public void onConfigurationChanged(Configuration newParentConfig) { + super.onConfigurationChanged(newParentConfig); + + // Configuration's equality doesn't consider seq so if only seq number changes in resolved + // override configuration. Therefore ConfigurationContainer doesn't change merged override + // configuration, but it's used to push configuration changes so explicitly update that. + if (getMergedOverrideConfiguration().seq != getResolvedOverrideConfiguration().seq) { + onMergedOverrideConfigurationChanged(); + } + + // TODO(b/80414790): Remove code below after unification. + // Same as above it doesn't notify configuration listeners, and consequently AppWindowToken + // can't get updated seq number. However WindowState's merged override configuration needs + // to have this seq number because that's also used for activity config pushes during layout + // traversal. Therefore explicitly update them here. + if (mAppWindowToken == null) { + return; + } + final Configuration appWindowTokenRequestedOverrideConfig = + mAppWindowToken.getRequestedOverrideConfiguration(); + if (appWindowTokenRequestedOverrideConfig.seq != getResolvedOverrideConfiguration().seq) { + appWindowTokenRequestedOverrideConfig.seq = + getResolvedOverrideConfiguration().seq; + mAppWindowToken.onMergedOverrideConfigurationChanged(); + } + } + /** Returns true if the configuration is compatible with this activity. */ boolean isConfigurationCompatible(Configuration config) { final int orientation = mAppWindowToken != null diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 0f286ce30ccd..d8644df3684c 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -19,6 +19,7 @@ package com.android.server.wm; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.app.ActivityManager; import android.app.AppProtoEnums; import android.app.IActivityManager; import android.app.IApplicationThread; @@ -479,4 +480,10 @@ public abstract class ActivityTaskManagerInternal { public abstract void setProfilerInfo(ProfilerInfo profilerInfo); public abstract ActivityMetricsLaunchObserverRegistry getLaunchObserverRegistry(); + + /** + * Gets bitmap snapshot of the provided task id. + */ + public abstract ActivityManager.TaskSnapshot getTaskSnapshot(int taskId, + boolean reducedResolution); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 42121ca08696..e5bf21a205e3 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -245,7 +245,7 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.Preconditions; import com.android.internal.util.function.pooled.PooledLambda; -import com.android.server.AppOpsService; +import com.android.server.appop.AppOpsService; import com.android.server.AttributeCache; import com.android.server.LocalServices; import com.android.server.SystemService; @@ -7012,5 +7012,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return mStackSupervisor.getActivityMetricsLogger().getLaunchObserverRegistry(); } } + + @Override + public ActivityManager.TaskSnapshot getTaskSnapshot(int taskId, boolean reducedResolution) { + synchronized (mGlobalLock) { + return ActivityTaskManagerService.this.getTaskSnapshot(taskId, reducedResolution); + } + } } } diff --git a/services/core/java/com/android/server/wm/WindowTraceBuffer.java b/services/core/java/com/android/server/wm/WindowTraceBuffer.java new file mode 100644 index 000000000000..936ee85697b8 --- /dev/null +++ b/services/core/java/com/android/server/wm/WindowTraceBuffer.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L; + +import android.os.Trace; +import android.util.proto.ProtoOutputStream; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Buffer used for window tracing. + */ +abstract class WindowTraceBuffer { + private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; + + final Object mBufferSizeLock = new Object(); + final BlockingQueue<byte[]> mBuffer; + int mBufferSize; + private final int mBufferCapacity; + private final File mTraceFile; + + WindowTraceBuffer(int size, File traceFile) throws IOException { + mBufferCapacity = size; + mTraceFile = traceFile; + mBuffer = new LinkedBlockingQueue<>(); + + initTraceFile(); + } + + int getAvailableSpace() { + return mBufferCapacity - mBufferSize; + } + + /** + * Inserts the specified element into this buffer. + * + * This method is synchronized with {@code #take()} and {@code #clear()} + * for consistency. + * + * @param proto the element to add + * @return {@code true} if the inserted item was inserted into the buffer + * @throws IllegalStateException if the element cannot be added because it is larger + * than the buffer size. + */ + boolean add(ProtoOutputStream proto) throws InterruptedException { + byte[] protoBytes = proto.getBytes(); + int protoLength = protoBytes.length; + if (protoLength > mBufferCapacity) { + throw new IllegalStateException("Trace object too large for the buffer. Buffer size:" + + mBufferCapacity + " Object size: " + protoLength); + } + synchronized (mBufferSizeLock) { + boolean canAdd = canAdd(protoBytes); + if (canAdd) { + mBuffer.offer(protoBytes); + mBufferSize += protoLength; + } + return canAdd; + } + } + + void writeNextBufferElementToFile() throws IOException { + byte[] proto; + try { + proto = take(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + try { + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeToFile"); + try (OutputStream os = new FileOutputStream(mTraceFile, true)) { + os.write(proto); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } + + /** + * Retrieves and removes the head of this queue, waiting if necessary + * until an element becomes available. + * + * This method is synchronized with {@code #add(ProtoOutputStream)} and {@code #clear()} + * for consistency. + * + * @return the head of this buffer, or {@code null} if this buffer is empty + */ + private byte[] take() throws InterruptedException { + byte[] item = mBuffer.take(); + synchronized (mBufferSizeLock) { + mBufferSize -= item.length; + return item; + } + } + + private void initTraceFile() throws IOException { + mTraceFile.delete(); + try (OutputStream os = new FileOutputStream(mTraceFile)) { + mTraceFile.setReadable(true, false); + ProtoOutputStream proto = new ProtoOutputStream(os); + proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); + proto.flush(); + } + } + + /** + * Checks if the element can be added to the buffer. The element is already certain to be + * smaller than the overall buffer size. + * + * @param protoBytes byte array representation of the Proto object to add + * @return <tt>true<</tt> if the element can be added to the buffer or not + */ + abstract boolean canAdd(byte[] protoBytes) throws InterruptedException; + + /** + * Flush all buffer content to the disk. + * + * @throws IOException if the buffer cannot write its contents to the {@link #mTraceFile} + */ + abstract void writeToDisk() throws IOException, InterruptedException; + + /** + * Builder for a {@code WindowTraceBuffer} which creates a {@link WindowTraceQueueBuffer} + */ + static class Builder { + private File mTraceFile; + private int mBufferCapacity; + + + Builder setTraceFile(File traceFile) { + mTraceFile = traceFile; + return this; + } + + Builder setBufferCapacity(int size) { + mBufferCapacity = size; + return this; + } + + File getFile() { + return mTraceFile; + } + + WindowTraceBuffer build() throws IOException { + if (mBufferCapacity <= 0) { + throw new IllegalStateException("Buffer capacity must be greater than 0."); + } + + if (mTraceFile == null) { + throw new IllegalArgumentException("A valid trace file must be specified."); + } + + return new WindowTraceQueueBuffer(mBufferCapacity, mTraceFile); + } + } +} diff --git a/services/core/java/com/android/server/wm/WindowTraceQueueBuffer.java b/services/core/java/com/android/server/wm/WindowTraceQueueBuffer.java new file mode 100644 index 000000000000..b7fc7ac8cb5e --- /dev/null +++ b/services/core/java/com/android/server/wm/WindowTraceQueueBuffer.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.os.Build.IS_USER; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.File; +import java.io.IOException; + +/** + * A buffer structure backed by a {@link java.util.concurrent.BlockingQueue} to store the first + * {@code #size size} bytes of window trace elements. + * Once the buffer is full it will no longer accepts new elements. + */ +class WindowTraceQueueBuffer extends WindowTraceBuffer { + private Thread mWriterThread; + private boolean mCancel; + + @VisibleForTesting + WindowTraceQueueBuffer(int size, File traceFile, boolean startWriterThread) throws IOException { + super(size, traceFile); + if (startWriterThread) { + initializeWriterThread(); + } + } + + WindowTraceQueueBuffer(int size, File traceFile) throws IOException { + this(size, traceFile, !IS_USER); + } + + private void initializeWriterThread() { + mCancel = false; + mWriterThread = new Thread(() -> { + try { + loop(); + } catch (IOException e) { + throw new IllegalStateException("Failed to execute trace write loop thread", e); + } + }, "window_tracing"); + mWriterThread.start(); + } + + private void loop() throws IOException { + while (!mCancel) { + writeNextBufferElementToFile(); + } + } + + private void restartWriterThread() throws InterruptedException { + if (mWriterThread != null) { + mCancel = true; + mWriterThread.interrupt(); + mWriterThread.join(); + initializeWriterThread(); + } + } + + @Override + boolean canAdd(byte[] protoBytes) { + long availableSpace = getAvailableSpace(); + return availableSpace >= protoBytes.length; + } + + @Override + void writeToDisk() throws InterruptedException { + while (!mBuffer.isEmpty()) { + mBufferSizeLock.wait(); + mBufferSizeLock.notify(); + } + restartWriterThread(); + } +} diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java index 8fa56bb065c6..63539c4f9fd9 100644 --- a/services/core/java/com/android/server/wm/WindowTracing.java +++ b/services/core/java/com/android/server/wm/WindowTracing.java @@ -17,31 +17,23 @@ package com.android.server.wm; import static android.os.Build.IS_USER; + import static com.android.server.wm.WindowManagerTraceFileProto.ENTRY; -import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER; -import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H; -import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L; import static com.android.server.wm.WindowManagerTraceProto.ELAPSED_REALTIME_NANOS; import static com.android.server.wm.WindowManagerTraceProto.WHERE; import static com.android.server.wm.WindowManagerTraceProto.WINDOW_MANAGER_SERVICE; +import android.annotation.Nullable; import android.content.Context; import android.os.ShellCommand; import android.os.SystemClock; import android.os.Trace; -import android.annotation.Nullable; import android.util.Log; import android.util.proto.ProtoOutputStream; -import com.android.internal.annotations.VisibleForTesting; - import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; import java.io.PrintWriter; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; /** * A class that allows window manager to dump its state continuously to a trace file, such that a @@ -49,35 +41,42 @@ import java.util.concurrent.BlockingQueue; */ class WindowTracing { + /** + * Maximum buffer size, currently defined as 512 KB + * Size was experimentally defined to fit between 100 to 150 elements. + */ + private static final int WINDOW_TRACE_BUFFER_SIZE = 512 * 1024; private static final String TAG = "WindowTracing"; - private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; private final Object mLock = new Object(); - private final File mTraceFile; - private final BlockingQueue<ProtoOutputStream> mWriteQueue = new ArrayBlockingQueue<>(200); + private final WindowTraceBuffer.Builder mBufferBuilder; + + private WindowTraceBuffer mTraceBuffer; private boolean mEnabled; private volatile boolean mEnabledLockFree; WindowTracing(File file) { - mTraceFile = file; + mBufferBuilder = new WindowTraceBuffer.Builder() + .setTraceFile(file) + .setBufferCapacity(WINDOW_TRACE_BUFFER_SIZE); } void startTrace(@Nullable PrintWriter pw) throws IOException { - if (IS_USER){ + if (IS_USER) { logAndPrintln(pw, "Error: Tracing is not supported on user builds."); return; } synchronized (mLock) { - logAndPrintln(pw, "Start tracing to " + mTraceFile + "."); - mWriteQueue.clear(); - mTraceFile.delete(); - try (OutputStream os = new FileOutputStream(mTraceFile)) { - mTraceFile.setReadable(true, false); - ProtoOutputStream proto = new ProtoOutputStream(os); - proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); - proto.flush(); + logAndPrintln(pw, "Start tracing to " + mBufferBuilder.getFile() + "."); + if (mTraceBuffer != null) { + try { + mTraceBuffer.writeToDisk(); + } catch (InterruptedException e) { + logAndPrintln(pw, "Error: Unable to flush the previous buffer."); + } } + mTraceBuffer = mBufferBuilder.build(); mEnabled = mEnabledLockFree = true; } } @@ -91,67 +90,42 @@ class WindowTracing { } void stopTrace(@Nullable PrintWriter pw) { - if (IS_USER){ + if (IS_USER) { logAndPrintln(pw, "Error: Tracing is not supported on user builds."); return; } synchronized (mLock) { - logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush."); + logAndPrintln(pw, "Stop tracing to " + mBufferBuilder.getFile() + + ". Waiting for traces to flush."); mEnabled = mEnabledLockFree = false; - while (!mWriteQueue.isEmpty()) { + + synchronized (mLock) { if (mEnabled) { logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush."); throw new IllegalStateException("tracing enabled while waiting for flush."); } try { - mLock.wait(); - mLock.notify(); + mTraceBuffer.writeToDisk(); + } catch (IOException e) { + Log.e(TAG, "Unable to write buffer to file", e); } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + Log.e(TAG, "Unable to interrupt window tracing file write thread", e); } } - logAndPrintln(pw, "Trace written to " + mTraceFile + "."); + logAndPrintln(pw, "Trace written to " + mBufferBuilder.getFile() + "."); } } - void appendTraceEntry(ProtoOutputStream proto) { + private void appendTraceEntry(ProtoOutputStream proto) { if (!mEnabledLockFree) { return; } - if (!mWriteQueue.offer(proto)) { - Log.e(TAG, "Dropping window trace entry, queue full"); - } - } - - void loop() { - for (;;) { - loopOnce(); - } - } - - @VisibleForTesting - void loopOnce() { - ProtoOutputStream proto; try { - proto = mWriteQueue.take(); + mTraceBuffer.add(proto); } catch (InterruptedException e) { + Log.e(TAG, "Unable to add element to trace", e); Thread.currentThread().interrupt(); - return; - } - - synchronized (mLock) { - try { - Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeToFile"); - try (OutputStream os = new FileOutputStream(mTraceFile, true /* append */)) { - os.write(proto.getBytes()); - } - } catch (IOException e) { - Log.e(TAG, "Failed to write file " + mTraceFile, e); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); - } - mLock.notify(); } } @@ -161,11 +135,7 @@ class WindowTracing { static WindowTracing createDefaultAndStartLooper(Context context) { File file = new File("/data/misc/wmtrace/wm_trace.pb"); - WindowTracing windowTracing = new WindowTracing(file); - if (!IS_USER){ - new Thread(windowTracing::loop, "window_tracing").start(); - } - return windowTracing; + return new WindowTracing(file); } int onShellCommand(ShellCommand shell, String cmd) { diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 0929e20c9187..6f105ec90836 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -1500,27 +1500,6 @@ static void nativeSetSystemUiVisibility(JNIEnv* /* env */, im->setSystemUiVisibility(visibility); } -static jboolean nativeTransferTouchFocus(JNIEnv* env, - jclass /* clazz */, jlong ptr, jobject fromChannelObj, jobject toChannelObj) { - NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr); - - sp<InputChannel> fromChannel = - android_view_InputChannel_getInputChannel(env, fromChannelObj); - sp<InputChannel> toChannel = - android_view_InputChannel_getInputChannel(env, toChannelObj); - - if (fromChannel == nullptr || toChannel == nullptr) { - return JNI_FALSE; - } - - if (im->getInputManager()->getDispatcher()-> - transferTouchFocus(fromChannel, toChannel)) { - return JNI_TRUE; - } else { - return JNI_FALSE; - } -} - static void nativeSetPointerSpeed(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jint speed) { NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr); @@ -1708,8 +1687,6 @@ static const JNINativeMethod gInputManagerMethods[] = { (void*) nativeSetInputDispatchMode }, { "nativeSetSystemUiVisibility", "(JI)V", (void*) nativeSetSystemUiVisibility }, - { "nativeTransferTouchFocus", "(JLandroid/view/InputChannel;Landroid/view/InputChannel;)Z", - (void*) nativeTransferTouchFocus }, { "nativeSetPointerSpeed", "(JI)V", (void*) nativeSetPointerSpeed }, { "nativeSetShowTouches", "(JZ)V", diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 0e05b5a8e044..76ae5ccd4eeb 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -6103,7 +6103,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { synchronized (getLockObject()) { delegates = getDelegatePackagesInternalLocked(scope, userId); } - if (delegates.size() != 1) { + if (delegates.size() == 0) { + return null; + } else if (delegates.size() > 1) { Slog.wtf(LOG_TAG, "More than one delegate holds " + scope); return null; } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 8235ca3484b6..fef2db921dcf 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -120,6 +120,7 @@ import com.android.server.pm.PackageManagerService; import com.android.server.pm.ShortcutService; import com.android.server.pm.UserManagerService; import com.android.server.policy.PhoneWindowManager; +import com.android.server.policy.role.LegacyRoleResolutionPolicy; import com.android.server.power.PowerManagerService; import com.android.server.power.ShutdownThread; import com.android.server.power.ThermalManagerService; @@ -262,6 +263,10 @@ public final class SystemServer { "com.android.server.accessibility.AccessibilityManagerService$Lifecycle"; private static final String ADB_SERVICE_CLASS = "com.android.server.adb.AdbService$Lifecycle"; + private static final String APP_PREDICTION_MANAGER_SERVICE_CLASS = + "com.android.server.appprediction.AppPredictionManagerService"; + private static final String CONTENT_SUGGESTIONS_SERVICE_CLASS = + "com.android.server.contentsuggestions.ContentSuggestionsManagerService"; private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst"; @@ -1172,6 +1177,16 @@ public final class SystemServer { startContentCaptureService(context); + // App prediction manager service + traceBeginAndSlog("StartAppPredictionService"); + mSystemServiceManager.startService(APP_PREDICTION_MANAGER_SERVICE_CLASS); + traceEnd(); + + // Content suggestions manager service + traceBeginAndSlog("StartContentSuggestionsService"); + mSystemServiceManager.startService(CONTENT_SUGGESTIONS_SERVICE_CLASS); + traceEnd(); + // NOTE: ClipboardService indirectly depends on IntelligenceService traceBeginAndSlog("StartClipboardService"); mSystemServiceManager.startService(ClipboardService.class); @@ -1977,7 +1992,8 @@ public final class SystemServer { // Grants default permissions and defines roles traceBeginAndSlog("StartRoleManagerService"); - mSystemServiceManager.startService(RoleManagerService.class); + mSystemServiceManager.startService(new RoleManagerService( + mSystemContext, new LegacyRoleResolutionPolicy(mSystemContext))); traceEnd(); // No dependency on Webview preparation in system server. But this should diff --git a/services/tests/mockingservicestests/src/com/android/server/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/AlarmManagerServiceTest.java index 148faada6381..6a153d5346ed 100644 --- a/services/tests/mockingservicestests/src/com/android/server/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/AlarmManagerServiceTest.java @@ -60,6 +60,7 @@ import android.os.Handler; import android.os.Looper; import android.os.PowerManager; import android.os.UserHandle; +import android.platform.test.annotations.Presubmit; import android.provider.Settings; import android.util.Log; import android.util.SparseArray; @@ -81,6 +82,7 @@ import org.mockito.quality.Strictness; import java.util.ArrayList; +@Presubmit @SmallTest @RunWith(AndroidJUnit4.class) public class AlarmManagerServiceTest { diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java index 6a10ff487d2d..2cc338cb8674 100644 --- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -70,7 +70,7 @@ import androidx.test.filters.FlakyTest; import androidx.test.filters.MediumTest; import androidx.test.filters.SmallTest; -import com.android.server.AppOpsService; +import com.android.server.appop.AppOpsService; import com.android.server.am.ProcessList.IsolatedUidRange; import com.android.server.am.ProcessList.IsolatedUidRangeAllocator; import com.android.server.wm.ActivityTaskManagerService; diff --git a/services/tests/servicestests/src/com/android/server/am/AppErrorDialogTest.java b/services/tests/servicestests/src/com/android/server/am/AppErrorDialogTest.java index 05cb48b7d64d..8109b1c56207 100644 --- a/services/tests/servicestests/src/com/android/server/am/AppErrorDialogTest.java +++ b/services/tests/servicestests/src/com/android/server/am/AppErrorDialogTest.java @@ -25,7 +25,7 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; -import com.android.server.AppOpsService; +import com.android.server.appop.AppOpsService; import org.junit.Before; import org.junit.Test; diff --git a/services/tests/servicestests/src/com/android/server/am/CoreSettingsObserverTest.java b/services/tests/servicestests/src/com/android/server/am/CoreSettingsObserverTest.java index 9626990efdcd..cbdc6c3faadd 100644 --- a/services/tests/servicestests/src/com/android/server/am/CoreSettingsObserverTest.java +++ b/services/tests/servicestests/src/com/android/server/am/CoreSettingsObserverTest.java @@ -34,7 +34,7 @@ import android.test.mock.MockContentResolver; import androidx.test.filters.SmallTest; import com.android.internal.util.test.FakeSettingsProvider; -import com.android.server.AppOpsService; +import com.android.server.appop.AppOpsService; import org.junit.AfterClass; import org.junit.Before; diff --git a/services/tests/servicestests/src/com/android/server/appops/AppOpsActiveWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java index 7e0dfcfd73f0..65c5781a9114 100644 --- a/services/tests/servicestests/src/com/android/server/appops/AppOpsActiveWatcherTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.appops; +package com.android.server.appop; import static com.google.common.truth.Truth.assertThat; diff --git a/services/tests/servicestests/src/com/android/server/appops/AppOpsNotedWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java index edd89f9e61d1..edd89f9e61d1 100644 --- a/services/tests/servicestests/src/com/android/server/appops/AppOpsNotedWatcherTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java diff --git a/services/tests/servicestests/src/com/android/server/appops/AppOpsServiceTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsServiceTest.java index 4ae9bea7617c..36f84d053817 100644 --- a/services/tests/servicestests/src/com/android/server/appops/AppOpsServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsServiceTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.server; +package com.android.server.appop; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_ERRORED; @@ -36,6 +36,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.appop.AppOpsService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/services/tests/servicestests/src/com/android/server/AppOpsUpgradeTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsUpgradeTest.java index aac96a1dd847..eb0c6279ea99 100644 --- a/services/tests/servicestests/src/com/android/server/AppOpsUpgradeTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsUpgradeTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server; +package com.android.server.appop; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java index 7049b215083a..c442ae845d4b 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java @@ -17,6 +17,7 @@ package com.android.server.hdmi; import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; import static com.android.server.hdmi.Constants.ADDR_BROADCAST; +import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1; import static com.android.server.hdmi.Constants.ADDR_TUNER_1; import static com.android.server.hdmi.Constants.ADDR_TV; import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; @@ -53,6 +54,7 @@ public class HdmiCecLocalDeviceAudioSystemTest { private HdmiControlService mHdmiControlService; private HdmiCecController mHdmiCecController; private HdmiCecLocalDeviceAudioSystem mHdmiCecLocalDeviceAudioSystem; + private HdmiCecLocalDevicePlayback mHdmiCecLocalDevicePlayback; private FakeNativeWrapper mNativeWrapper; private Looper mMyLooper; private TestLooper mTestLooper = new TestLooper(); @@ -133,7 +135,9 @@ public class HdmiCecLocalDeviceAudioSystemTest { mMyLooper = mTestLooper.getLooper(); mHdmiCecLocalDeviceAudioSystem = new HdmiCecLocalDeviceAudioSystem(mHdmiControlService); + mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(mHdmiControlService); mHdmiCecLocalDeviceAudioSystem.init(); + mHdmiCecLocalDevicePlayback.init(); mHdmiControlService.setIoLooper(mMyLooper); mNativeWrapper = new FakeNativeWrapper(); mHdmiCecController = @@ -142,6 +146,7 @@ public class HdmiCecLocalDeviceAudioSystemTest { mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService)); mHdmiControlService.setMessageValidator(new HdmiCecMessageValidator(mHdmiControlService)); mLocalDevices.add(mHdmiCecLocalDeviceAudioSystem); + mLocalDevices.add(mHdmiCecLocalDevicePlayback); mHdmiControlService.initPortInfo(); // No TV device interacts with AVR so system audio control won't be turned on here mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); @@ -557,4 +562,63 @@ public class HdmiCecLocalDeviceAudioSystemTest { mTestLooper.dispatchAll(); assertThat(mNativeWrapper.getOnlyResultMessage()).isEqualTo(expectedMessage); } + + @Test + public void handleActiveSource_activeSourceFromTV_swithToArc() { + mHdmiCecLocalDeviceAudioSystem.setArcStatus(true); + HdmiCecMessage message = + HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); + + ActiveSource expectedActiveSource = ActiveSource.of(ADDR_TV, 0x0000); + + assertThat(mHdmiCecLocalDeviceAudioSystem.handleActiveSource(message)).isTrue(); + mTestLooper.dispatchAll(); + assertThat(mHdmiCecLocalDeviceAudioSystem.getActiveSource()) + .isEqualTo(expectedActiveSource); + } + + @Test + public void handleRoutingChange_currentActivePortIsHome() { + HdmiCecMessage message = + HdmiCecMessageBuilder.buildRoutingChange(ADDR_TV, 0x3000, mAvrPhysicalAddress); + + HdmiCecMessage expectedMessage = + HdmiCecMessageBuilder.buildActiveSource(ADDR_AUDIO_SYSTEM, mAvrPhysicalAddress); + ActiveSource expectedActiveSource = ActiveSource.of(ADDR_AUDIO_SYSTEM, mAvrPhysicalAddress); + int expectedLocalActivePort = Constants.CEC_SWITCH_HOME; + + assertThat(mHdmiCecLocalDeviceAudioSystem.handleRoutingChange(message)).isTrue(); + mTestLooper.dispatchAll(); + assertThat(mHdmiCecLocalDeviceAudioSystem.getActiveSource()) + .isEqualTo(expectedActiveSource); + assertThat(mHdmiCecLocalDeviceAudioSystem.getLocalActivePort()) + .isEqualTo(expectedLocalActivePort); + assertThat(mNativeWrapper.getOnlyResultMessage()).isEqualTo(expectedMessage); + } + + @Test + public void handleRoutingInformation_currentActivePortIsHDMI1() { + HdmiCecMessage message = + HdmiCecMessageBuilder.buildRoutingInformation(ADDR_TV, 0x2000); + mHdmiCecLocalDeviceAudioSystem.setLocalActivePort(Constants.CEC_SWITCH_HDMI1); + HdmiCecMessage expectedMessage = + HdmiCecMessageBuilder.buildRoutingInformation(ADDR_AUDIO_SYSTEM, 0x2100); + + assertThat(mHdmiCecLocalDeviceAudioSystem.handleRoutingInformation(message)).isTrue(); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getOnlyResultMessage()).isEqualTo(expectedMessage); + } + + @Test + public void handleRoutingChange_homeIsActive_playbackSendActiveSource() { + HdmiCecMessage message = + HdmiCecMessageBuilder.buildRoutingChange(ADDR_TV, 0x0000, 0x2000); + + HdmiCecMessage expectedMessage = + HdmiCecMessageBuilder.buildActiveSource(ADDR_PLAYBACK_1, 0x2000); + + assertThat(mHdmiCecLocalDeviceAudioSystem.handleRoutingChange(message)).isTrue(); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getOnlyResultMessage()).isEqualTo(expectedMessage); + } } diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java index 561c61fb979f..0dff03f2f53e 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java @@ -47,6 +47,7 @@ import com.android.server.lights.LightsManager; import com.android.server.policy.WindowManagerPolicy; import com.android.server.power.PowerManagerService.Injector; import com.android.server.power.PowerManagerService.NativeWrapper; +import com.android.server.power.batterysaver.BatterySaverPolicy; import com.android.server.power.batterysaver.BatterySavingStats; import org.junit.Rule; diff --git a/services/tests/servicestests/src/com/android/server/power/BatterySaverPolicyTest.java b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java index acf511e38738..73eefcf618b0 100644 --- a/services/tests/servicestests/src/com/android/server/power/BatterySaverPolicyTest.java +++ b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.server.power; +package com.android.server.power.batterysaver; import static com.google.common.truth.Truth.assertThat; @@ -31,13 +31,12 @@ import android.util.ArrayMap; import com.android.frameworks.servicestests.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; -import com.android.server.power.batterysaver.BatterySavingStats; import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** - * Tests for {@link com.android.server.power.BatterySaverPolicy} + * Tests for {@link com.android.server.power.batterysaver.BatterySaverPolicy} */ public class BatterySaverPolicyTest extends AndroidTestCase { private static final boolean BATTERY_SAVER_ON = true; @@ -62,7 +61,7 @@ public class BatterySaverPolicyTest extends AndroidTestCase { private static final String BATTERY_SAVER_INCORRECT_CONSTANTS = "vi*,!=,,true"; private class BatterySaverPolicyForTest extends BatterySaverPolicy { - public BatterySaverPolicyForTest(Object lock, Context context, + BatterySaverPolicyForTest(Object lock, Context context, BatterySavingStats batterySavingStats) { super(lock, context, batterySavingStats); } @@ -269,15 +268,15 @@ public class BatterySaverPolicyTest extends AndroidTestCase { mBatterySaverPolicy.onChange(); assertThat(mBatterySaverPolicy.getFileValues(true).toString()).isEqualTo("{}"); assertThat(mBatterySaverPolicy.getFileValues(false).toString()) - .isEqualTo("{/sys/devices/system/cpu/cpu1/cpufreq/scaling_max_freq=123, " + - "/sys/devices/system/cpu/cpu2/cpufreq/scaling_max_freq=456}"); + .isEqualTo("{/sys/devices/system/cpu/cpu1/cpufreq/scaling_max_freq=123, " + + "/sys/devices/system/cpu/cpu2/cpufreq/scaling_max_freq=456}"); mDeviceSpecificConfigResId = R.string.config_batterySaverDeviceSpecificConfig_3; mBatterySaverPolicy.onChange(); assertThat(mBatterySaverPolicy.getFileValues(true).toString()) - .isEqualTo("{/sys/devices/system/cpu/cpu3/cpufreq/scaling_max_freq=333, " + - "/sys/devices/system/cpu/cpu4/cpufreq/scaling_max_freq=444}"); + .isEqualTo("{/sys/devices/system/cpu/cpu3/cpufreq/scaling_max_freq=333, " + + "/sys/devices/system/cpu/cpu4/cpufreq/scaling_max_freq=444}"); assertThat(mBatterySaverPolicy.getFileValues(false).toString()) .isEqualTo("{/sys/devices/system/cpu/cpu2/cpufreq/scaling_max_freq=222}"); @@ -287,9 +286,9 @@ public class BatterySaverPolicyTest extends AndroidTestCase { mBatterySaverPolicy.onChange(); assertThat(mBatterySaverPolicy.getFileValues(true).toString()) - .isEqualTo("{/sys/devices/system/cpu/cpu3/cpufreq/scaling_max_freq=1234567890, " + - "/sys/devices/system/cpu/cpu4/cpufreq/scaling_max_freq=14, " + - "/sys/devices/system/cpu/cpu5/cpufreq/scaling_max_freq=15}"); + .isEqualTo("{/sys/devices/system/cpu/cpu3/cpufreq/scaling_max_freq=1234567890, " + + "/sys/devices/system/cpu/cpu4/cpufreq/scaling_max_freq=14, " + + "/sys/devices/system/cpu/cpu5/cpufreq/scaling_max_freq=15}"); assertThat(mBatterySaverPolicy.getFileValues(false).toString()).isEqualTo("{}"); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index b6f181758d3a..8f9d2badce30 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -20,6 +20,9 @@ import static android.view.Display.DEFAULT_DISPLAY; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +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.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_BOTTOM; import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_LEFT; @@ -38,6 +41,7 @@ import static org.junit.Assert.assertTrue; import android.app.ActivityOptions; import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.PauseActivityItem; +import android.content.res.Configuration; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; import android.util.MutableBoolean; @@ -210,4 +214,41 @@ public class ActivityRecordTests extends ActivityTestsBase { assertNull(mActivity.pendingOptions); assertNotNull(activity2.pendingOptions); } + + @Test + public void testNewOverrideConfigurationIncrementsSeq() { + final Configuration newConfig = new Configuration(); + + final int prevSeq = mActivity.getMergedOverrideConfiguration().seq; + mActivity.onRequestedOverrideConfigurationChanged(newConfig); + assertEquals(prevSeq + 1, mActivity.getMergedOverrideConfiguration().seq); + } + + @Test + public void testNewParentConfigurationIncrementsSeq() { + final Configuration newConfig = new Configuration( + mTask.getRequestedOverrideConfiguration()); + newConfig.orientation = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT + ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT; + + final int prevSeq = mActivity.getMergedOverrideConfiguration().seq; + mTask.onRequestedOverrideConfigurationChanged(newConfig); + assertEquals(prevSeq + 1, mActivity.getMergedOverrideConfiguration().seq); + } + + @Test + public void testNotifiesSeqIncrementToAppToken() { + final Configuration appWindowTokenRequestedOrientation = mock(Configuration.class); + mActivity.mAppWindowToken = mock(AppWindowToken.class); + doReturn(appWindowTokenRequestedOrientation).when(mActivity.mAppWindowToken) + .getRequestedOverrideConfiguration(); + + final Configuration newConfig = new Configuration(); + newConfig.orientation = Configuration.ORIENTATION_PORTRAIT; + + final int prevSeq = mActivity.getMergedOverrideConfiguration().seq; + mActivity.onRequestedOverrideConfigurationChanged(newConfig); + assertEquals(prevSeq + 1, appWindowTokenRequestedOrientation.seq); + verify(mActivity.mAppWindowToken).onMergedOverrideConfigurationChanged(); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java index bd29d2a3a884..5589ca1be815 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java @@ -66,7 +66,7 @@ import android.view.Display; import android.view.DisplayInfo; import com.android.internal.app.IVoiceInteractor; -import com.android.server.AppOpsService; +import com.android.server.appop.AppOpsService; import com.android.server.AttributeCache; import com.android.server.ServiceThread; import com.android.server.am.ActivityManagerService; @@ -264,6 +264,8 @@ class ActivityTestsBase { .setOrientation(anyInt(), any(), any()); doCallRealMethod().when(activity.mAppWindowToken).setOrientation(anyInt()); doNothing().when(activity).removeWindowContainer(); + doReturn(mock(Configuration.class)).when(activity.mAppWindowToken) + .getRequestedOverrideConfiguration(); if (mTaskRecord != null) { mTaskRecord.addActivityToTop(activity); diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java index f1c6eab2143d..bb3ab358e71a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java @@ -20,11 +20,9 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; 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.spy; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -125,7 +123,6 @@ public class DragDropControllerTests extends WindowTestsBase { mDisplayContent = spy(mDisplayContent); mWindow = createDropTargetWindow("Drag test window", 0); doReturn(mWindow).when(mDisplayContent).getTouchableWinAtPointLocked(0, 0); - when(mWm.mInputManager.transferTouchFocus(any(), any())).thenReturn(true); synchronized (mWm.mGlobalLock) { mWm.mWindowMap.put(mWindow.mClient.asBinder(), mWindow); @@ -176,7 +173,6 @@ public class DragDropControllerTests extends WindowTestsBase { .setFormat(PixelFormat.TRANSLUCENT) .build(); - assertTrue(mWm.mInputManager.transferTouchFocus(null, null)); mToken = mTarget.performDrag( new SurfaceSession(), 0, 0, mWindow.mClient, flag, surface, 0, 0, 0, 0, 0, data); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskPositioningControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskPositioningControllerTests.java index c343fe7d0675..8c6ac23c9202 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskPositioningControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskPositioningControllerTests.java @@ -18,7 +18,6 @@ package com.android.server.wm; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; @@ -58,10 +57,6 @@ public class TaskPositioningControllerTests extends WindowTestsBase { assertNotNull(mWm.mTaskPositioningController); mTarget = mWm.mTaskPositioningController; - when(mWm.mInputManager.transferTouchFocus( - any(InputChannel.class), - any(InputChannel.class))).thenReturn(true); - mWindow = createWindow(null, TYPE_BASE_APPLICATION, "window"); mWindow.mInputChannel = new InputChannel(); synchronized (mWm.mGlobalLock) { diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTraceBufferTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowTraceBufferTest.java new file mode 100644 index 000000000000..8d834974148c --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTraceBufferTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.platform.test.annotations.Presubmit; +import android.util.proto.ProtoOutputStream; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + + +/** + * Test class for {@link WindowTraceBuffer} and {@link WindowTraceQueueBuffer}. + * + * Build/Install/Run: + * atest WmTests:WindowTraceBufferTest + */ +@SmallTest +@Presubmit +public class WindowTraceBufferTest { + private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; + + private File mFile; + + @Before + public void setUp() throws Exception { + final Context testContext = getInstrumentation().getContext(); + mFile = testContext.getFileStreamPath("tracing_test.dat"); + mFile.delete(); + } + + @After + public void tearDown() throws Exception { + mFile.delete(); + } + + @Test + public void testTraceQueueBuffer_addItem() throws Exception { + ProtoOutputStream toWrite1 = getDummy(1); + ProtoOutputStream toWrite2 = getDummy(2); + ProtoOutputStream toWrite3 = getDummy(3); + byte[] toWrite1Bytes = toWrite1.getBytes(); + byte[] toWrite2Bytes = toWrite2.getBytes(); + byte[] toWrite3Bytes = toWrite3.getBytes(); + + final int objectSize = toWrite1.getBytes().length; + final int bufferCapacity = objectSize * 2; + + final WindowTraceBuffer buffer = buildQueueBuffer(bufferCapacity); + + buffer.add(toWrite1); + assertTrue("First element should be in the list", + buffer.mBuffer.stream().anyMatch(p -> Arrays.equals(p, toWrite1Bytes))); + + buffer.add(toWrite2); + assertTrue("First element should be in the list", + buffer.mBuffer.stream().anyMatch(p -> Arrays.equals(p, toWrite1Bytes))); + assertTrue("Second element should be in the list", + buffer.mBuffer.stream().anyMatch(p -> Arrays.equals(p, toWrite2Bytes))); + + buffer.add(toWrite3); + + assertTrue("Third element should not be in the list", + buffer.mBuffer.stream().noneMatch(p -> Arrays.equals(p, toWrite3Bytes))); + + assertEquals("Buffer should have 2 elements", buffer.mBuffer.size(), 2); + assertEquals(String.format("Buffer is full, used space should be %d", bufferCapacity), + buffer.mBufferSize, bufferCapacity); + assertEquals("Buffer is full, available space should be 0", + buffer.getAvailableSpace(), 0); + } + + private ProtoOutputStream getDummy(int value) { + ProtoOutputStream toWrite = new ProtoOutputStream(); + toWrite.write(MAGIC_NUMBER, value); + toWrite.flush(); + + return toWrite; + } + + private WindowTraceBuffer buildQueueBuffer(int size) throws IOException { + return new WindowTraceQueueBuffer(size, mFile, false); + } +} diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java index 9fc1b6f76e77..bf9bf9a1ba8f 100644 --- a/telephony/java/android/telephony/ServiceState.java +++ b/telephony/java/android/telephony/ServiceState.java @@ -170,7 +170,8 @@ public class ServiceState implements Parcelable { RIL_RADIO_TECHNOLOGY_GSM, RIL_RADIO_TECHNOLOGY_TD_SCDMA, RIL_RADIO_TECHNOLOGY_IWLAN, - RIL_RADIO_TECHNOLOGY_LTE_CA}) + RIL_RADIO_TECHNOLOGY_LTE_CA, + RIL_RADIO_TECHNOLOGY_NR}) public @interface RilRadioTechnology {} /** * Available radio technologies for GSM, UMTS and CDMA. diff --git a/telephony/java/com/android/internal/telephony/SmsApplication.java b/telephony/java/com/android/internal/telephony/SmsApplication.java index 9874f809c0b4..a508b068065f 100644 --- a/telephony/java/com/android/internal/telephony/SmsApplication.java +++ b/telephony/java/com/android/internal/telephony/SmsApplication.java @@ -18,6 +18,8 @@ package com.android.internal.telephony; import android.Manifest.permission; import android.app.AppOpsManager; +import android.app.role.RoleManager; +import android.app.role.RoleManagerCallback; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -29,13 +31,12 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; -import android.content.res.Resources; import android.net.Uri; +import android.os.AsyncTask; import android.os.Binder; import android.os.Debug; import android.os.Process; import android.os.UserHandle; -import android.provider.Settings; import android.provider.Telephony; import android.provider.Telephony.Sms.Intents; import android.telephony.Rlog; @@ -50,6 +51,10 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * Class for managing the primary application that we will deliver SMS/MMS messages to @@ -67,6 +72,7 @@ public final class SmsApplication { private static final String SCHEME_SMSTO = "smsto"; private static final String SCHEME_MMS = "mms"; private static final String SCHEME_MMSTO = "mmsto"; + private static final boolean DEBUG = false; private static final boolean DEBUG_MULTIUSER = false; private static final int[] DEFAULT_APP_EXCLUSIVE_APPOPS = { @@ -240,7 +246,11 @@ public final class SmsApplication { // Get the list of apps registered for SMS Intent intent = new Intent(Intents.SMS_DELIVER_ACTION); - List<ResolveInfo> smsReceivers = packageManager.queryBroadcastReceiversAsUser(intent, 0, + if (DEBUG) { + intent.addFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION); + } + List<ResolveInfo> smsReceivers = packageManager.queryBroadcastReceiversAsUser(intent, + PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); HashMap<String, SmsApplicationData> receivers = new HashMap<String, SmsApplicationData>(); @@ -266,7 +276,8 @@ public final class SmsApplication { // Update any existing entries with mms receiver class intent = new Intent(Intents.WAP_PUSH_DELIVER_ACTION); intent.setDataAndType(null, "application/vnd.wap.mms-message"); - List<ResolveInfo> mmsReceivers = packageManager.queryBroadcastReceiversAsUser(intent, 0, + List<ResolveInfo> mmsReceivers = packageManager.queryBroadcastReceiversAsUser(intent, + PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); for (ResolveInfo resolveInfo : mmsReceivers) { final ActivityInfo activityInfo = resolveInfo.activityInfo; @@ -286,7 +297,8 @@ public final class SmsApplication { // Update any existing entries with respond via message intent class. intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, Uri.fromParts(SCHEME_SMSTO, "", null)); - List<ResolveInfo> respondServices = packageManager.queryIntentServicesAsUser(intent, 0, + List<ResolveInfo> respondServices = packageManager.queryIntentServicesAsUser(intent, + PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); for (ResolveInfo resolveInfo : respondServices) { final ServiceInfo serviceInfo = resolveInfo.serviceInfo; @@ -306,7 +318,8 @@ public final class SmsApplication { // Update any existing entries with supports send to. intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts(SCHEME_SMSTO, "", null)); - List<ResolveInfo> sendToActivities = packageManager.queryIntentActivitiesAsUser(intent, 0, + List<ResolveInfo> sendToActivities = packageManager.queryIntentActivitiesAsUser(intent, + PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); for (ResolveInfo resolveInfo : sendToActivities) { final ActivityInfo activityInfo = resolveInfo.activityInfo; @@ -323,7 +336,9 @@ public final class SmsApplication { // Update any existing entries with the default sms changed handler. intent = new Intent(Telephony.Sms.Intents.ACTION_DEFAULT_SMS_PACKAGE_CHANGED); List<ResolveInfo> smsAppChangedReceivers = - packageManager.queryBroadcastReceiversAsUser(intent, 0, userId); + packageManager.queryBroadcastReceiversAsUser(intent, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); if (DEBUG_MULTIUSER) { Log.i(LOG_TAG, "getApplicationCollectionInternal smsAppChangedActivities=" + smsAppChangedReceivers); @@ -348,7 +363,9 @@ public final class SmsApplication { // Update any existing entries with the external provider changed handler. intent = new Intent(Telephony.Sms.Intents.ACTION_EXTERNAL_PROVIDER_CHANGE); List<ResolveInfo> providerChangedReceivers = - packageManager.queryBroadcastReceiversAsUser(intent, 0, userId); + packageManager.queryBroadcastReceiversAsUser(intent, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); if (DEBUG_MULTIUSER) { Log.i(LOG_TAG, "getApplicationCollectionInternal providerChangedActivities=" + providerChangedReceivers); @@ -373,7 +390,9 @@ public final class SmsApplication { // Update any existing entries with the sim full handler. intent = new Intent(Intents.SIM_FULL_ACTION); List<ResolveInfo> simFullReceivers = - packageManager.queryBroadcastReceiversAsUser(intent, 0, userId); + packageManager.queryBroadcastReceiversAsUser(intent, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); if (DEBUG_MULTIUSER) { Log.i(LOG_TAG, "getApplicationCollectionInternal simFullReceivers=" + simFullReceivers); @@ -406,7 +425,8 @@ public final class SmsApplication { if (smsApplicationData != null) { if (!smsApplicationData.isComplete()) { Log.w(LOG_TAG, "Package " + packageName - + " lacks required manifest declarations to be a default sms app"); + + " lacks required manifest declarations to be a default sms app: " + + smsApplicationData); receivers.remove(packageName); } } @@ -418,7 +438,7 @@ public final class SmsApplication { * Checks to see if we have a valid installed SMS application for the specified package name * @return Data for the specified package name or null if there isn't one */ - private static SmsApplicationData getApplicationForPackage( + public static SmsApplicationData getApplicationForPackage( Collection<SmsApplicationData> applications, String packageName) { if (packageName == null) { return null; @@ -456,8 +476,7 @@ public final class SmsApplication { Log.i(LOG_TAG, "getApplication userId=" + userId); } // Determine which application receives the broadcast - String defaultApplication = Settings.Secure.getStringForUser(context.getContentResolver(), - Settings.Secure.SMS_DEFAULT_APPLICATION, userId); + String defaultApplication = getDefaultSmsPackage(context, userId); if (DEBUG_MULTIUSER) { Log.i(LOG_TAG, "getApplication defaultApp=" + defaultApplication); } @@ -469,27 +488,6 @@ public final class SmsApplication { if (DEBUG_MULTIUSER) { Log.i(LOG_TAG, "getApplication appData=" + applicationData); } - // Picking a new SMS app requires AppOps and Settings.Secure permissions, so we only do - // this if the caller asked us to. - if (updateIfNeeded && applicationData == null) { - // Try to find the default SMS package for this device - Resources r = context.getResources(); - String defaultPackage = - r.getString(com.android.internal.R.string.default_sms_application); - applicationData = getApplicationForPackage(applications, defaultPackage); - - if (applicationData == null) { - // Are there any applications? - if (applications.size() != 0) { - applicationData = (SmsApplicationData)applications.toArray()[0]; - } - } - - // If we found a new default app, update the setting - if (applicationData != null) { - setDefaultApplicationInternal(applicationData.mPackageName, context, userId); - } - } // If we found a package, make sure AppOps permissions are set up correctly if (applicationData != null) { @@ -513,7 +511,7 @@ public final class SmsApplication { // current SMS app will already be the preferred activity - but checking whether or // not this is true is just as expensive as reconfiguring the preferred activity so // we just reconfigure every time. - updateDefaultSmsApp(context, userId, applicationData); + defaultSmsAppChanged(context); } } if (DEBUG_MULTIUSER) { @@ -522,16 +520,17 @@ public final class SmsApplication { return applicationData; } - private static void updateDefaultSmsApp(Context context, int userId, - SmsApplicationData applicationData) { + private static String getDefaultSmsPackage(Context context, int userId) { + return context.getSystemService(RoleManager.class).getDefaultSmsPackage(userId); + } + + /** + * Grants various permissions and appops on sms app change + */ + private static void defaultSmsAppChanged(Context context) { PackageManager packageManager = context.getPackageManager(); AppOpsManager appOps = context.getSystemService(AppOpsManager.class); - // Configure this as the preferred activity for SENDTO sms/mms intents - configurePreferredActivity(packageManager, new ComponentName( - applicationData.mPackageName, applicationData.mSendToClass), - userId); - // Assign permission to special system apps assignExclusiveSmsPermissionsToSystemApp(context, packageManager, appOps, PHONE_PACKAGE_NAME); @@ -603,8 +602,7 @@ public final class SmsApplication { final UserHandle userHandle = UserHandle.of(userId); // Get old package name - String oldPackageName = Settings.Secure.getStringForUser(context.getContentResolver(), - Settings.Secure.SMS_DEFAULT_APPLICATION, userId); + String oldPackageName = getDefaultSmsPackage(context, userId); if (DEBUG_MULTIUSER) { Log.i(LOG_TAG, "setDefaultApplicationInternal old=" + oldPackageName + @@ -636,16 +634,29 @@ public final class SmsApplication { } } - // Update the secure setting. - Settings.Secure.putStringForUser(context.getContentResolver(), - Settings.Secure.SMS_DEFAULT_APPLICATION, applicationData.mPackageName, - userId); - - // Allow relevant appops for the newly configured default SMS app. - setExclusiveAppops(applicationData.mPackageName, appOps, applicationData.mUid, - AppOpsManager.MODE_ALLOWED); + // Update the setting. + CompletableFuture<Void> res = new CompletableFuture<>(); + context.getSystemService(RoleManager.class).addRoleHolderAsUser( + RoleManager.ROLE_SMS, applicationData.mPackageName, UserHandle.of(userId), + AsyncTask.THREAD_POOL_EXECUTOR, new RoleManagerCallback() { + @Override + public void onSuccess() { + res.complete(null); + } + + @Override + public void onFailure() { + res.completeExceptionally(new RuntimeException()); + } + }); + try { + res.get(5, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + Log.e(LOG_TAG, "Exception while adding sms role holder " + applicationData, e); + return; + } - updateDefaultSmsApp(context, userId, applicationData); + defaultSmsAppChanged(context); if (DEBUG_MULTIUSER) { Log.i(LOG_TAG, "setDefaultApplicationInternal oldAppData=" + oldAppData); diff --git a/tests/PackageWatchdog/Android.mk b/tests/PackageWatchdog/Android.mk new file mode 100644 index 000000000000..b53f27d28740 --- /dev/null +++ b/tests/PackageWatchdog/Android.mk @@ -0,0 +1,34 @@ +# 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. + +LOCAL_PATH:= $(call my-dir) + +# PackageWatchdogTest +include $(CLEAR_VARS) +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_PACKAGE_NAME := PackageWatchdogTest +LOCAL_MODULE_TAGS := tests +LOCAL_STATIC_JAVA_LIBRARIES := \ + junit \ + frameworks-base-testutils \ + android-support-test \ + services + +LOCAL_JAVA_LIBRARIES := \ + android.test.runner + +LOCAL_PRIVATE_PLATFORM_APIS := true +LOCAL_COMPATIBILITY_SUITE := device-tests + +include $(BUILD_PACKAGE) diff --git a/tests/PackageWatchdog/AndroidManifest.xml b/tests/PackageWatchdog/AndroidManifest.xml new file mode 100644 index 000000000000..fa89528403c3 --- /dev/null +++ b/tests/PackageWatchdog/AndroidManifest.xml @@ -0,0 +1,28 @@ +<?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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.packagewatchdog" > + + <application android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + </application> + + + <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.tests.packagewatchdog" + android:label="PackageWatchdog Test"/> +</manifest> diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java new file mode 100644 index 000000000000..ec07037b3a8f --- /dev/null +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import static com.android.server.PackageWatchdog.TRIGGER_FAILURE_COUNT; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.os.test.TestLooper; +import android.support.test.InstrumentationRegistry; + +import com.android.server.PackageWatchdog.PackageHealthObserver; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +// TODO(zezeozue): Write test without using PackageWatchdog#getPackages. Just rely on +// behavior of observers receiving crash notifications or not to determine if it's registered +/** + * Test PackageWatchdog. + */ +public class PackageWatchdogTest { + private static final String APP_A = "com.package.a"; + private static final String APP_B = "com.package.b"; + private static final String OBSERVER_NAME_1 = "observer1"; + private static final String OBSERVER_NAME_2 = "observer2"; + private static final String OBSERVER_NAME_3 = "observer3"; + private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1); + private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5); + private TestLooper mTestLooper; + + @Before + public void setUp() throws Exception { + mTestLooper = new TestLooper(); + mTestLooper.startAutoDispatch(); + } + + @After + public void tearDown() throws Exception { + new File(InstrumentationRegistry.getContext().getFilesDir(), + "package-watchdog.xml").delete(); + } + + /** + * Test registration, unregistration, package expiry and duration reduction + */ + @Test + public void testRegistration() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); + TestObserver observer3 = new TestObserver(OBSERVER_NAME_3); + + // Start observing for observer1 which will be unregistered + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + // Start observing for observer2 which will expire + watchdog.startObservingHealth(observer2, Arrays.asList(APP_A, APP_B), SHORT_DURATION); + // Start observing for observer3 which will have expiry duration reduced + watchdog.startObservingHealth(observer3, Arrays.asList(APP_A), LONG_DURATION); + + // Verify packages observed at start + // 1 + assertEquals(1, watchdog.getPackages(observer1).size()); + assertTrue(watchdog.getPackages(observer1).contains(APP_A)); + // 2 + assertEquals(2, watchdog.getPackages(observer2).size()); + assertTrue(watchdog.getPackages(observer2).contains(APP_A)); + assertTrue(watchdog.getPackages(observer2).contains(APP_B)); + // 3 + assertEquals(1, watchdog.getPackages(observer3).size()); + assertTrue(watchdog.getPackages(observer3).contains(APP_A)); + + // Then unregister observer1 + watchdog.unregisterHealthObserver(observer1); + + // Verify observer2 and observer3 left + // 1 + assertNull(watchdog.getPackages(observer1)); + // 2 + assertEquals(2, watchdog.getPackages(observer2).size()); + assertTrue(watchdog.getPackages(observer2).contains(APP_A)); + assertTrue(watchdog.getPackages(observer2).contains(APP_B)); + // 3 + assertEquals(1, watchdog.getPackages(observer3).size()); + assertTrue(watchdog.getPackages(observer3).contains(APP_A)); + + // Then advance time a little and run messages in Handlers so observer2 expires + Thread.sleep(SHORT_DURATION); + mTestLooper.dispatchAll(); + + // Verify observer3 left with reduced expiry duration + // 1 + assertNull(watchdog.getPackages(observer1)); + // 2 + assertNull(watchdog.getPackages(observer2)); + // 3 + assertEquals(1, watchdog.getPackages(observer3).size()); + assertTrue(watchdog.getPackages(observer3).contains(APP_A)); + + // Then advance time some more and run messages in Handlers so observer3 expires + Thread.sleep(LONG_DURATION); + mTestLooper.dispatchAll(); + + // Verify observer3 expired + // 1 + assertNull(watchdog.getPackages(observer1)); + // 2 + assertNull(watchdog.getPackages(observer2)); + // 3 + assertNull(watchdog.getPackages(observer3)); + } + + /** + * Test package observers are persisted and loaded on startup + */ + @Test + public void testPersistence() throws Exception { + PackageWatchdog watchdog1 = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); + + watchdog1.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + watchdog1.startObservingHealth(observer2, Arrays.asList(APP_A, APP_B), SHORT_DURATION); + + // Verify 2 observers are registered and saved internally + // 1 + assertEquals(1, watchdog1.getPackages(observer1).size()); + assertTrue(watchdog1.getPackages(observer1).contains(APP_A)); + // 2 + assertEquals(2, watchdog1.getPackages(observer2).size()); + assertTrue(watchdog1.getPackages(observer2).contains(APP_A)); + assertTrue(watchdog1.getPackages(observer2).contains(APP_B)); + + + // Then advance time and run IO Handler so file is saved + mTestLooper.dispatchAll(); + + // Then start a new watchdog + PackageWatchdog watchdog2 = createWatchdog(); + + // Verify the new watchdog loads observers on startup but nothing registered + assertEquals(0, watchdog2.getPackages(observer1).size()); + assertEquals(0, watchdog2.getPackages(observer2).size()); + // Verify random observer not saved returns null + assertNull(watchdog2.getPackages(new TestObserver(OBSERVER_NAME_3))); + + // Then regiser observer1 + watchdog2.registerHealthObserver(observer1); + watchdog2.registerHealthObserver(observer2); + + // Verify 2 observers are registered after reload + // 1 + assertEquals(1, watchdog1.getPackages(observer1).size()); + assertTrue(watchdog1.getPackages(observer1).contains(APP_A)); + // 2 + assertEquals(2, watchdog1.getPackages(observer2).size()); + assertTrue(watchdog1.getPackages(observer2).contains(APP_A)); + assertTrue(watchdog1.getPackages(observer2).contains(APP_B)); + } + + /** + * Test package failure under threshold does not notify observers + */ + @Test + public void testNoPackageFailureBeforeThreshold() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); + + watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + + // Then fail APP_A below the threshold + for (int i = 0; i < TRIGGER_FAILURE_COUNT - 1; i++) { + watchdog.onPackageFailure(new String[]{APP_A}); + } + + // Verify that observers are not notified + assertEquals(0, observer1.mFailedPackages.size()); + assertEquals(0, observer2.mFailedPackages.size()); + } + + /** + * Test package failure and notifies all observer since none handles the failure + */ + @Test + public void testPackageFailureNotifyAll() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); + + // Start observing for observer1 and observer2 without handling failures + watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A, APP_B), SHORT_DURATION); + + // Then fail APP_A and APP_B above the threshold + for (int i = 0; i < TRIGGER_FAILURE_COUNT; i++) { + watchdog.onPackageFailure(new String[]{APP_A, APP_B}); + } + + // Verify all observers are notifed of all package failures + List<String> observer1Packages = observer1.mFailedPackages; + List<String> observer2Packages = observer2.mFailedPackages; + assertEquals(2, observer1Packages.size()); + assertEquals(1, observer2Packages.size()); + assertEquals(APP_A, observer1Packages.get(0)); + assertEquals(APP_B, observer1Packages.get(1)); + assertEquals(APP_A, observer2Packages.get(0)); + } + + /** + * Test package failure and notifies only one observer because it handles the failure + */ + @Test + public void testPackageFailureNotifyOne() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1, true /* shouldHandle */); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2, true /* shouldHandle */); + + // Start observing for observer1 and observer2 with failure handling + watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + + // Then fail APP_A above the threshold + for (int i = 0; i < TRIGGER_FAILURE_COUNT; i++) { + watchdog.onPackageFailure(new String[]{APP_A}); + } + + // Verify only one observer is notifed + assertEquals(1, observer1.mFailedPackages.size()); + assertEquals(APP_A, observer1.mFailedPackages.get(0)); + assertEquals(0, observer2.mFailedPackages.size()); + } + + private PackageWatchdog createWatchdog() { + return new PackageWatchdog(InstrumentationRegistry.getContext(), + mTestLooper.getLooper()); + } + + private static class TestObserver implements PackageHealthObserver { + private final String mName; + private boolean mShouldHandle; + final List<String> mFailedPackages = new ArrayList<>(); + + TestObserver(String name) { + mName = name; + } + + TestObserver(String name, boolean shouldHandle) { + mName = name; + mShouldHandle = shouldHandle; + } + + public boolean onHealthCheckFailed(String packageName) { + mFailedPackages.add(packageName); + return mShouldHandle; + } + + public String getName() { + return mName; + } + } +} diff --git a/tools/bit/command.h b/tools/bit/command.h index fb44900b0806..dd7103e10fe7 100644 --- a/tools/bit/command.h +++ b/tools/bit/command.h @@ -25,7 +25,7 @@ using namespace std; struct Command { - Command(const string& prog); + explicit Command(const string& prog); ~Command(); void AddArg(const string& arg); diff --git a/tools/stats_log_api_gen/main.cpp b/tools/stats_log_api_gen/main.cpp index 9968bda6458f..4491a8567441 100644 --- a/tools/stats_log_api_gen/main.cpp +++ b/tools/stats_log_api_gen/main.cpp @@ -67,7 +67,7 @@ cpp_type_name(java_type_t type) case JAVA_TYPE_STRING: return "char const*"; case JAVA_TYPE_BYTE_ARRAY: - return "char const*"; + return "const BytesField&"; default: return "UNKNOWN"; } @@ -270,10 +270,6 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, chainField.name.c_str(), chainField.name.c_str()); } } - } else if (*arg == JAVA_TYPE_BYTE_ARRAY) { - fprintf(out, ", %s arg%d, size_t arg%d_length", - cpp_type_name(*arg), argIndex, argIndex); - } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { fprintf(out, ", const std::map<int, int32_t>& arg%d_1, " "const std::map<int, int64_t>& arg%d_2, " @@ -355,7 +351,8 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, fprintf(out, " event.end();\n\n"); } else if (*arg == JAVA_TYPE_BYTE_ARRAY) { fprintf(out, - " event.AppendCharArray(arg%d, arg%d_length);\n", + " event.AppendCharArray(arg%d.arg, " + "arg%d.arg_length);\n", argIndex, argIndex); } else { if (*arg == JAVA_TYPE_STRING) { @@ -397,10 +394,6 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, chainField.name.c_str(), chainField.name.c_str()); } } - } else if (*arg == JAVA_TYPE_BYTE_ARRAY) { - fprintf(out, ", %s arg%d, size_t arg%d_length", - cpp_type_name(*arg), argIndex, argIndex); - } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { fprintf(out, ", const std::map<int, int32_t>& arg%d_1, " @@ -434,8 +427,6 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, chainField.name.c_str(), chainField.name.c_str()); } } - } else if (*arg == JAVA_TYPE_BYTE_ARRAY) { - fprintf(out, ", arg%d, arg%d_length", argIndex, argIndex); } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { fprintf(out, ", arg%d_1, arg%d_2, arg%d_3, arg%d_4", argIndex, argIndex, argIndex, argIndex); @@ -494,7 +485,14 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, fprintf(out, " arg%d = \"\";\n", argIndex); fprintf(out, " }\n"); } - fprintf(out, " event << arg%d;\n", argIndex); + if (*arg == JAVA_TYPE_BYTE_ARRAY) { + fprintf(out, + " event.AppendCharArray(arg%d.arg, " + "arg%d.arg_length);", + argIndex, argIndex); + } else { + fprintf(out, " event << arg%d;\n", argIndex); + } if (argIndex == 2) { fprintf(out, " event.end();\n\n"); fprintf(out, " event.end();\n\n"); @@ -577,7 +575,9 @@ void build_non_chained_decl_map(const Atoms& atoms, static void write_cpp_usage( FILE* out, const string& method_name, const string& atom_code_name, const AtomDecl& atom, const AtomDecl &attributionDecl) { - fprintf(out, " * Usage: %s(StatsLog.%s", method_name.c_str(), atom_code_name.c_str()); + fprintf(out, " * Usage: %s(StatsLog.%s", method_name.c_str(), + atom_code_name.c_str()); + for (vector<AtomField>::const_iterator field = atom.fields.begin(); field != atom.fields.end(); field++) { if (field->javaType == JAVA_TYPE_ATTRIBUTION_CHAIN) { @@ -601,11 +601,6 @@ static void write_cpp_usage( field->name.c_str(), field->name.c_str(), field->name.c_str()); - } else if (field->javaType == JAVA_TYPE_BYTE_ARRAY) { - fprintf(out, ", %s %s, size_t %s_length", - cpp_type_name(field->javaType), field->name.c_str(), - field->name.c_str()); - } else { fprintf(out, ", %s %s", cpp_type_name(field->javaType), field->name.c_str()); } @@ -639,9 +634,6 @@ static void write_cpp_method_header( "const std::map<int, char const*>& arg%d_3, " "const std::map<int, float>& arg%d_4", argIndex, argIndex, argIndex, argIndex); - } else if (*arg == JAVA_TYPE_BYTE_ARRAY) { - fprintf(out, ", %s arg%d, size_t arg%d_length", - cpp_type_name(*arg), argIndex, argIndex); } else { fprintf(out, ", %s arg%d", cpp_type_name(*arg), argIndex); } @@ -708,6 +700,15 @@ write_stats_log_header(FILE* out, const Atoms& atoms, const AtomDecl &attributio fprintf(out, "};\n"); fprintf(out, "\n"); + fprintf(out, "struct BytesField {\n"); + fprintf(out, + " BytesField(char const* array, size_t len) : arg(array), " + "arg_length(len) {}\n"); + fprintf(out, " char const* arg;\n"); + fprintf(out, " size_t arg_length;\n"); + fprintf(out, "};\n"); + fprintf(out, "\n"); + fprintf(out, "struct StateAtomFieldOptions {\n"); fprintf(out, " std::vector<int> primaryFields;\n"); fprintf(out, " int exclusiveField;\n"); @@ -1183,6 +1184,11 @@ write_stats_log_jni(FILE* out, const string& java_method_name, const string& cpp fprintf(out, " str%d = NULL;\n", argIndex); fprintf(out, " }\n"); + fprintf(out, + " android::util::BytesField bytesField%d(str%d, " + "str%d_length);", + argIndex, argIndex, argIndex); + } else if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { hadStringOrChain = true; for (auto chainField : attributionDecl.fields) { @@ -1240,7 +1246,8 @@ write_stats_log_jni(FILE* out, const string& java_method_name, const string& cpp // stats_write call argIndex = 1; - fprintf(out, "\n int ret = android::util::%s(code", cpp_method_name.c_str()); + fprintf(out, "\n int ret = android::util::%s(code", + cpp_method_name.c_str()); for (vector<java_type_t>::const_iterator arg = signature->begin(); arg != signature->end(); arg++) { if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { @@ -1255,16 +1262,12 @@ write_stats_log_jni(FILE* out, const string& java_method_name, const string& cpp } } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { fprintf(out, ", int32_t_map, int64_t_map, string_map, float_map"); + } else if (*arg == JAVA_TYPE_BYTE_ARRAY) { + fprintf(out, ", bytesField%d", argIndex); } else { - const char* argName = (*arg == JAVA_TYPE_STRING || - *arg == JAVA_TYPE_BYTE_ARRAY) - ? "str" - : "arg"; + const char* argName = + (*arg == JAVA_TYPE_STRING) ? "str" : "arg"; fprintf(out, ", (%s)%s%d", cpp_type_name(*arg), argName, argIndex); - - if (*arg == JAVA_TYPE_BYTE_ARRAY) { - fprintf(out, ", %s%d_length", argName, argIndex); - } } argIndex++; } diff --git a/tools/streaming_proto/Errors.h b/tools/streaming_proto/Errors.h index f14bbfd55b5f..bddd9819e8f5 100644 --- a/tools/streaming_proto/Errors.h +++ b/tools/streaming_proto/Errors.h @@ -11,7 +11,7 @@ using namespace std; struct Error { Error(); - explicit Error(const Error& that); + Error(const Error& that); Error(const string& filename, int lineno, const char* message); string filename; diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java index 7f56a010c4d0..517bf3b47a7a 100644 --- a/wifi/java/android/net/wifi/WifiManager.java +++ b/wifi/java/android/net/wifi/WifiManager.java @@ -189,6 +189,7 @@ public class WifiManager { */ public static final int STATUS_NETWORK_SUGGESTIONS_ERROR_REMOVE_INVALID = 5; + /** @hide */ @IntDef(prefix = { "STATUS_NETWORK_SUGGESTIONS_" }, value = { STATUS_NETWORK_SUGGESTIONS_SUCCESS, STATUS_NETWORK_SUGGESTIONS_ERROR_INTERNAL, @@ -1652,7 +1653,7 @@ public class WifiManager { * suggestion back using this API.</li> * * @param networkSuggestions List of network suggestions provided by the app. - * @return Status code corresponding to the values in {@link NetworkSuggestionsStatusCode}. + * @return Status code for the operation. One of the STATUS_NETWORK_SUGGESTIONS_ values. * {@link WifiNetworkSuggestion#equals(Object)} any previously provided suggestions by the app. * @throws {@link SecurityException} if the caller is missing required permissions. */ @@ -1673,8 +1674,7 @@ public class WifiManager { * * @param networkSuggestions List of network suggestions to be removed. Pass an empty list * to remove all the previous suggestions provided by the app. - * @return Status code corresponding to the values in - * {@link NetworkSuggestionsStatusCode}. + * @return Status code for the operation. One of the STATUS_NETWORK_SUGGESTIONS_ values. * Any matching suggestions are removed from the device and will not be considered for any * further connection attempts. */ |