Merge "Import translations. DO NOT MERGE"
diff --git a/Android.bp b/Android.bp
index 0210bb3..fb018a5 100644
--- a/Android.bp
+++ b/Android.bp
@@ -104,6 +104,7 @@
"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/role/IOnRoleHoldersChangedListener.aidl",
"core/java/android/app/role/IRoleManager.aidl",
"core/java/android/app/role/IRoleManagerCallback.aidl",
"core/java/android/app/slice/ISliceManager.aidl",
@@ -784,18 +785,6 @@
],
}
-// A host library containing the inspector annotations for inspector-annotation-processor.
-java_library_host {
- name: "inspector-annotation",
- srcs: [
- "core/java/android/view/inspector/InspectableNodeName.java",
- "core/java/android/view/inspector/InspectableProperty.java",
- // Needed for the ResourceId.ID_NULL constant
- "core/java/android/content/res/ResourceId.java",
- "core/java/android/annotation/AnyRes.java",
- ],
-}
-
// A host library including just UnsupportedAppUsage.java so that the annotation
// processor can also use this annotation.
java_library_host {
diff --git a/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java b/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java
index 5be0cb0..99e4ba1 100644
--- a/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java
+++ b/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java
@@ -39,6 +39,7 @@
@LargeTest
public class BinderCallsStatsPerfTest {
private static final int DEFAULT_BUCKET_SIZE = 1000;
+ private static final int WORKSOURCE_UID = 1;
static class FakeCpuTimeBinderCallsStats extends BinderCallsStats {
private int mTimeMs;
@@ -117,8 +118,8 @@
Binder b = new Binder();
while (state.keepRunning()) {
for (int i = 0; i < 10000; i++) {
- CallSession s = mBinderCallsStats.callStarted(b, i % maxBucketSize);
- mBinderCallsStats.callEnded(s, 0, 0);
+ CallSession s = mBinderCallsStats.callStarted(b, i % maxBucketSize, WORKSOURCE_UID);
+ mBinderCallsStats.callEnded(s, 0, 0, WORKSOURCE_UID);
}
}
}
diff --git a/api/current.txt b/api/current.txt
index 931b9fa..b8a37d4 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -9402,6 +9402,7 @@
method public static void cancelSync(android.content.SyncRequest);
method public final android.net.Uri canonicalize(android.net.Uri);
method public final int delete(android.net.Uri, java.lang.String, java.lang.String[]);
+ method public android.os.Bundle getCache(android.net.Uri);
method public static deprecated android.content.SyncInfo getCurrentSync();
method public static java.util.List<android.content.SyncInfo> getCurrentSyncs();
method public static int getIsSyncable(android.accounts.Account, java.lang.String);
@@ -9432,6 +9433,7 @@
method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException;
method public final android.content.res.AssetFileDescriptor openTypedAssetFileDescriptor(android.net.Uri, java.lang.String, android.os.Bundle) throws java.io.FileNotFoundException;
method public final android.content.res.AssetFileDescriptor openTypedAssetFileDescriptor(android.net.Uri, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException;
+ method public void putCache(android.net.Uri, android.os.Bundle);
method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String);
method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, android.os.CancellationSignal);
method public final android.database.Cursor query(android.net.Uri, java.lang.String[], android.os.Bundle, android.os.CancellationSignal);
@@ -10236,6 +10238,7 @@
field public static final java.lang.String EXTRA_ASSIST_INPUT_HINT_KEYBOARD = "android.intent.extra.ASSIST_INPUT_HINT_KEYBOARD";
field public static final java.lang.String EXTRA_ASSIST_PACKAGE = "android.intent.extra.ASSIST_PACKAGE";
field public static final java.lang.String EXTRA_ASSIST_UID = "android.intent.extra.ASSIST_UID";
+ field public static final java.lang.String EXTRA_AUTO_LAUNCH_SINGLE_CHOICE = "android.intent.extra.AUTO_LAUNCH_SINGLE_CHOICE";
field public static final java.lang.String EXTRA_BCC = "android.intent.extra.BCC";
field public static final java.lang.String EXTRA_BUG_REPORT = "android.intent.extra.BUG_REPORT";
field public static final java.lang.String EXTRA_CC = "android.intent.extra.CC";
@@ -23394,6 +23397,7 @@
method public int getStreamType();
method public boolean getTimestamp(android.media.AudioTimestamp);
method public int getUnderrunCount();
+ method public static boolean isDirectPlaybackSupported(android.media.AudioFormat, android.media.AudioAttributes);
method public void pause() throws java.lang.IllegalStateException;
method public void play() throws java.lang.IllegalStateException;
method public void registerStreamEventCallback(java.util.concurrent.Executor, android.media.AudioTrack.StreamEventCallback);
@@ -24751,6 +24755,7 @@
field public static final java.lang.String MIMETYPE_AUDIO_AMR_NB = "audio/3gpp";
field public static final java.lang.String MIMETYPE_AUDIO_AMR_WB = "audio/amr-wb";
field public static final java.lang.String MIMETYPE_AUDIO_EAC3 = "audio/eac3";
+ field public static final java.lang.String MIMETYPE_AUDIO_EAC3_JOC = "audio/eac3-joc";
field public static final java.lang.String MIMETYPE_AUDIO_FLAC = "audio/flac";
field public static final java.lang.String MIMETYPE_AUDIO_G711_ALAW = "audio/g711-alaw";
field public static final java.lang.String MIMETYPE_AUDIO_G711_MLAW = "audio/g711-mlaw";
@@ -37449,6 +37454,8 @@
field public static final java.lang.String EXTRA_ORIENTATION = "android.provider.extra.ORIENTATION";
field public static final java.lang.String EXTRA_PROMPT = "android.provider.extra.PROMPT";
field public static final java.lang.String METADATA_EXIF = "android:documentExif";
+ field public static final java.lang.String METADATA_TREE_COUNT = "android:metadataTreeCount";
+ field public static final java.lang.String METADATA_TREE_SIZE = "android:metadataTreeSize";
field public static final java.lang.String METADATA_TYPES = "android:documentMetadataTypes";
field public static final java.lang.String PROVIDER_INTERFACE = "android.content.action.DOCUMENTS_PROVIDER";
field public static final java.lang.String QUERY_ARG_DISPLAY_NAME = "android:query-arg-display-name";
@@ -37825,6 +37832,7 @@
public static final class MediaStore.Downloads implements android.provider.MediaStore.DownloadColumns {
method public static android.net.Uri getContentUri(java.lang.String);
+ field public static final java.lang.String CONTENT_TYPE = "vnd.android.cursor.dir/download";
field public static final android.net.Uri EXTERNAL_CONTENT_URI;
field public static final android.net.Uri INTERNAL_CONTENT_URI;
}
@@ -40564,6 +40572,7 @@
public final class UserData implements android.os.Parcelable {
method public int describeContents();
method public java.lang.String getFieldClassificationAlgorithm();
+ method public java.lang.String getFieldClassificationAlgorithmForCategory(java.lang.String);
method public java.lang.String getId();
method public static int getMaxCategoryCount();
method public static int getMaxFieldClassificationIdsSize();
@@ -40579,6 +40588,7 @@
method public android.service.autofill.UserData.Builder add(java.lang.String, java.lang.String);
method public android.service.autofill.UserData build();
method public android.service.autofill.UserData.Builder setFieldClassificationAlgorithm(java.lang.String, android.os.Bundle);
+ method public android.service.autofill.UserData.Builder setFieldClassificationAlgorithmForCategory(java.lang.String, java.lang.String, android.os.Bundle);
}
public abstract interface Validator {
@@ -42328,8 +42338,9 @@
method public void swapConference();
method public void unhold();
method public void unregisterCallback(android.telecom.Call.Callback);
- field public static final java.lang.String AVAILABLE_PHONE_ACCOUNTS = "selectPhoneAccountAccounts";
+ field public static final deprecated java.lang.String AVAILABLE_PHONE_ACCOUNTS = "selectPhoneAccountAccounts";
field public static final java.lang.String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS";
+ field public static final java.lang.String EXTRA_SUGGESTED_PHONE_ACCOUNTS = "android.telecom.extra.SUGGESTED_PHONE_ACCOUNTS";
field public static final int STATE_ACTIVE = 4; // 0x4
field public static final int STATE_CONNECTING = 9; // 0x9
field public static final int STATE_DIALING = 1; // 0x1
@@ -42900,6 +42911,20 @@
field public static final android.os.Parcelable.Creator<android.telecom.PhoneAccountHandle> CREATOR;
}
+ public final class PhoneAccountSuggestion implements android.os.Parcelable {
+ method public int describeContents();
+ method public android.telecom.PhoneAccountHandle getPhoneAccountHandle();
+ method public int getReason();
+ method public boolean shouldAutoSelect();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.telecom.PhoneAccountSuggestion> CREATOR;
+ field public static final int REASON_FREQUENT = 2; // 0x2
+ field public static final int REASON_INTRA_CARRIER = 1; // 0x1
+ field public static final int REASON_NONE = 0; // 0x0
+ field public static final int REASON_OTHER = 4; // 0x4
+ field public static final int REASON_USER_SET = 3; // 0x3
+ }
+
public final class RemoteConference {
method public void disconnect();
method public java.util.List<android.telecom.RemoteConnection> getConferenceableConnections();
diff --git a/api/system-current.txt b/api/system-current.txt
index 29089b3..cd01cd5 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -91,6 +91,7 @@
field public static final java.lang.String INSTALL_PACKAGE_UPDATES = "android.permission.INSTALL_PACKAGE_UPDATES";
field public static final java.lang.String INSTALL_SELF_UPDATES = "android.permission.INSTALL_SELF_UPDATES";
field public static final java.lang.String INTENT_FILTER_VERIFICATION_AGENT = "android.permission.INTENT_FILTER_VERIFICATION_AGENT";
+ field public static final java.lang.String INTERACT_ACROSS_PROFILES = "android.permission.INTERACT_ACROSS_PROFILES";
field public static final java.lang.String INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS";
field public static final java.lang.String INTERACT_ACROSS_USERS_FULL = "android.permission.INTERACT_ACROSS_USERS_FULL";
field public static final java.lang.String INTERNAL_SYSTEM_WINDOW = "android.permission.INTERNAL_SYSTEM_WINDOW";
@@ -135,6 +136,7 @@
field public static final java.lang.String NOTIFICATION_DURING_SETUP = "android.permission.NOTIFICATION_DURING_SETUP";
field public static final java.lang.String NOTIFY_TV_INPUTS = "android.permission.NOTIFY_TV_INPUTS";
field public static final java.lang.String OBSERVE_APP_USAGE = "android.permission.OBSERVE_APP_USAGE";
+ field public static final java.lang.String OBSERVE_ROLE_HOLDERS = "android.permission.OBSERVE_ROLE_HOLDERS";
field public static final java.lang.String OVERRIDE_WIFI_CONFIG = "android.permission.OVERRIDE_WIFI_CONFIG";
field public static final java.lang.String PACKAGE_USAGE_STATS = "android.permission.PACKAGE_USAGE_STATS";
field public static final java.lang.String PACKAGE_VERIFICATION_AGENT = "android.permission.PACKAGE_VERIFICATION_AGENT";
@@ -172,6 +174,7 @@
field public static final java.lang.String REGISTER_CONNECTION_MANAGER = "android.permission.REGISTER_CONNECTION_MANAGER";
field public static final java.lang.String REGISTER_SIM_SUBSCRIPTION = "android.permission.REGISTER_SIM_SUBSCRIPTION";
field public static final java.lang.String REMOVE_DRM_CERTIFICATES = "android.permission.REMOVE_DRM_CERTIFICATES";
+ field public static final java.lang.String REMOVE_TASKS = "android.permission.REMOVE_TASKS";
field public static final java.lang.String RESET_PASSWORD = "android.permission.RESET_PASSWORD";
field public static final java.lang.String RESTRICTED_VR_ACCESS = "android.permission.RESTRICTED_VR_ACCESS";
field public static final java.lang.String RETRIEVE_WINDOW_CONTENT = "android.permission.RETRIEVE_WINDOW_CONTENT";
@@ -598,6 +601,8 @@
method public boolean isDeviceManaged();
method public boolean isDeviceProvisioned();
method public boolean isDeviceProvisioningConfigApplied();
+ method public boolean isManagedKiosk();
+ method public boolean isUnattendedManagedKiosk();
method public void notifyPendingSystemUpdate(long);
method public void notifyPendingSystemUpdate(long, boolean);
method public boolean packageHasActiveAdmins(java.lang.String);
@@ -884,12 +889,18 @@
package android.app.role {
+ public abstract interface OnRoleHoldersChangedListener {
+ method public abstract void onRoleHoldersChanged(java.lang.String, android.os.UserHandle);
+ }
+
public final class RoleManager {
+ method public void addOnRoleHoldersChangedListenerAsUser(java.util.concurrent.Executor, android.app.role.OnRoleHoldersChangedListener, android.os.UserHandle);
method public void addRoleHolderAsUser(java.lang.String, java.lang.String, android.os.UserHandle, java.util.concurrent.Executor, android.app.role.RoleManagerCallback);
method public boolean addRoleHolderFromController(java.lang.String, java.lang.String);
method public void clearRoleHoldersAsUser(java.lang.String, android.os.UserHandle, java.util.concurrent.Executor, android.app.role.RoleManagerCallback);
method public java.util.List<java.lang.String> getRoleHolders(java.lang.String);
method public java.util.List<java.lang.String> getRoleHoldersAsUser(java.lang.String, android.os.UserHandle);
+ method public void removeOnRoleHoldersChangedListenerAsUser(android.app.role.OnRoleHoldersChangedListener, android.os.UserHandle);
method public void removeRoleHolderAsUser(java.lang.String, java.lang.String, android.os.UserHandle, java.util.concurrent.Executor, android.app.role.RoleManagerCallback);
method public boolean removeRoleHolderFromController(java.lang.String, java.lang.String);
method public void setRoleNamesFromController(java.util.List<java.lang.String>);
@@ -1043,6 +1054,10 @@
method public void setDetectNotResponding(long);
}
+ public abstract class ContentResolver {
+ method public android.graphics.drawable.Drawable getTypeDrawable(java.lang.String);
+ }
+
public abstract class Context {
method public boolean bindServiceAsUser(android.content.Intent, android.content.ServiceConnection, int, android.os.UserHandle);
method public abstract android.content.Context createCredentialProtectedStorageContext();
@@ -1141,6 +1156,10 @@
field public int targetSandboxVersion;
}
+ public class CrossProfileApps {
+ method public void startAnyActivity(android.content.ComponentName, android.os.UserHandle);
+ }
+
public final class InstantAppInfo implements android.os.Parcelable {
ctor public InstantAppInfo(android.content.pm.ApplicationInfo, java.lang.String[], java.lang.String[]);
ctor public InstantAppInfo(java.lang.String, java.lang.CharSequence, java.lang.String[], java.lang.String[]);
@@ -1354,6 +1373,7 @@
public class PermissionInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable {
field public static final int FLAG_REMOVED = 2; // 0x2
+ field public static final int PROTECTION_FLAG_DOCUMENTER = 262144; // 0x40000
field public static final int PROTECTION_FLAG_OEM = 16384; // 0x4000
field public static final int PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER = 65536; // 0x10000
field public static final int PROTECTION_FLAG_WELLBEING = 131072; // 0x20000
@@ -4428,6 +4448,7 @@
ctor public RuntimePermissionPresenterService();
method public final void attachBaseContext(android.content.Context);
method public final android.os.IBinder onBind(android.content.Intent);
+ method public abstract int onCountPermissionApps(java.util.List<java.lang.String>, boolean, boolean);
method public abstract java.util.List<android.permission.RuntimePermissionPresentationInfo> onGetAppPermissions(java.lang.String);
method public abstract void onRevokeRuntimePermission(java.lang.String, java.lang.String);
field public static final java.lang.String SERVICE_INTERFACE = "android.permission.RuntimePermissionPresenterService";
@@ -4563,11 +4584,6 @@
field public static final int FLAG_REMOVABLE_USB = 524288; // 0x80000
}
- public final class MediaStore {
- method public static void deleteContributedMedia(android.content.Context, java.lang.String);
- method public static long getContributedMediaSize(android.content.Context, java.lang.String);
- }
-
public abstract class SearchIndexableData {
ctor public SearchIndexableData();
ctor public SearchIndexableData(android.content.Context);
@@ -4912,7 +4928,10 @@
public abstract class AutofillFieldClassificationService extends android.app.Service {
method public android.os.IBinder onBind(android.content.Intent);
- method public float[][] onGetScores(java.lang.String, android.os.Bundle, java.util.List<android.view.autofill.AutofillValue>, java.util.List<java.lang.String>);
+ method public float[][] onCalculateScores(java.util.List<android.view.autofill.AutofillValue>, java.util.List<java.lang.String>, java.util.List<java.lang.String>, java.lang.String, android.os.Bundle, java.util.Map, java.util.Map);
+ method public deprecated float[][] onGetScores(java.lang.String, android.os.Bundle, java.util.List<android.view.autofill.AutofillValue>, java.util.List<java.lang.String>);
+ field public static final java.lang.String REQUIRED_ALGORITHM_EDIT_DISTANCE = "EDIT_DISTANCE";
+ field public static final java.lang.String REQUIRED_ALGORITHM_EXACT_MATCH = "EXACT_MATCH";
field public static final java.lang.String SERVICE_INTERFACE = "android.service.autofill.AutofillFieldClassificationService";
field public static final java.lang.String SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS = "android.autofill.field_classification.available_algorithms";
field public static final java.lang.String SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM = "android.autofill.field_classification.default_algorithm";
@@ -5630,6 +5649,10 @@
field public static final int CAPABILITY_MULTI_USER = 32; // 0x20
}
+ public final class PhoneAccountSuggestion implements android.os.Parcelable {
+ ctor public PhoneAccountSuggestion(android.telecom.PhoneAccountHandle, int, boolean);
+ }
+
public final class RemoteConference {
method public deprecated void setAudioState(android.telecom.AudioState);
}
diff --git a/api/test-current.txt b/api/test-current.txt
index 5bedc72..4717969 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -10,6 +10,8 @@
field public static final java.lang.String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES";
field public static final java.lang.String MANAGE_ACTIVITY_STACKS = "android.permission.MANAGE_ACTIVITY_STACKS";
field public static final java.lang.String READ_CELL_BROADCASTS = "android.permission.READ_CELL_BROADCASTS";
+ field public static final java.lang.String REMOVE_TASKS = "android.permission.REMOVE_TASKS";
+ field public static final java.lang.String WRITE_OBB = "android.permission.WRITE_OBB";
}
}
@@ -372,6 +374,7 @@
}
public class PermissionInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable {
+ field public static final int PROTECTION_FLAG_DOCUMENTER = 262144; // 0x40000
field public static final int PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER = 65536; // 0x10000
field public static final int PROTECTION_FLAG_VENDOR_PRIVILEGED = 32768; // 0x8000
field public static final int PROTECTION_FLAG_WELLBEING = 131072; // 0x20000
@@ -1006,8 +1009,8 @@
}
public final class MediaStore {
- method public static void deleteContributedMedia(android.content.Context, java.lang.String);
- method public static long getContributedMediaSize(android.content.Context, java.lang.String);
+ method public static void deleteContributedMedia(android.content.Context, java.lang.String, android.os.UserHandle) throws java.io.IOException;
+ method public static long getContributedMediaSize(android.content.Context, java.lang.String, android.os.UserHandle) throws java.io.IOException;
}
public final class Settings {
@@ -1093,6 +1096,15 @@
package android.service.autofill {
+ public abstract class AutofillFieldClassificationService extends android.app.Service {
+ method public android.os.IBinder onBind(android.content.Intent);
+ field public static final java.lang.String REQUIRED_ALGORITHM_EDIT_DISTANCE = "EDIT_DISTANCE";
+ field public static final java.lang.String REQUIRED_ALGORITHM_EXACT_MATCH = "EXACT_MATCH";
+ field public static final java.lang.String SERVICE_INTERFACE = "android.service.autofill.AutofillFieldClassificationService";
+ field public static final java.lang.String SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS = "android.autofill.field_classification.available_algorithms";
+ field public static final java.lang.String SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM = "android.autofill.field_classification.default_algorithm";
+ }
+
public final class CharSequenceTransformation extends android.service.autofill.InternalTransformation implements android.os.Parcelable android.service.autofill.Transformation {
method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception;
}
@@ -1149,6 +1161,10 @@
method public android.view.autofill.AutofillValue sanitize(android.view.autofill.AutofillValue);
}
+ public final class UserData implements android.os.Parcelable {
+ method public android.util.ArrayMap<java.lang.String, java.lang.String> getFieldClassificationAlgorithms();
+ }
+
public abstract interface ValueFinder {
method public default java.lang.String findByAutofillId(android.view.autofill.AutofillId);
method public abstract android.view.autofill.AutofillValue findRawValueByAutofillId(android.view.autofill.AutofillId);
@@ -1267,6 +1283,10 @@
ctor public CallAudioState(boolean, int, int, android.bluetooth.BluetoothDevice, java.util.Collection<android.bluetooth.BluetoothDevice>);
}
+ public final class PhoneAccountSuggestion implements android.os.Parcelable {
+ ctor public PhoneAccountSuggestion(android.telecom.PhoneAccountHandle, int, boolean);
+ }
+
}
package android.telephony {
diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
index 3723fce..e3748f1 100644
--- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
+++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
@@ -201,7 +201,7 @@
private void doEnabled(@UserIdInt int userId) {
try {
- boolean isEnabled = mBmgr.isBackupEnabled();
+ boolean isEnabled = mBmgr.isBackupEnabledForUser(userId);
System.out.println("Backup Manager currently "
+ enableToString(isEnabled));
} catch (RemoteException e) {
@@ -219,7 +219,7 @@
try {
boolean enable = Boolean.parseBoolean(arg);
- mBmgr.setBackupEnabled(enable);
+ mBmgr.setBackupEnabledForUser(userId, enable);
System.out.println("Backup Manager now " + enableToString(enable));
} catch (NumberFormatException e) {
showUsage();
@@ -232,7 +232,7 @@
void doRun(@UserIdInt int userId) {
try {
- mBmgr.backupNow();
+ mBmgr.backupNowForUser(userId);
} catch (RemoteException e) {
System.err.println(e.toString());
System.err.println(BMGR_NOT_RUNNING_ERR);
@@ -416,7 +416,8 @@
(monitorState != Monitor.OFF)
? new BackupMonitor(monitorState == Monitor.VERBOSE)
: null;
- int err = mBmgr.requestBackup(
+ int err = mBmgr.requestBackupForUser(
+ userId,
packages.toArray(new String[packages.size()]),
observer,
monitor,
@@ -477,7 +478,7 @@
String arg = nextArg();
if ("backups".equals(arg)) {
try {
- mBmgr.cancelBackups();
+ mBmgr.cancelBackupsForUser(userId);
} catch (RemoteException e) {
System.err.println(e.toString());
System.err.println(BMGR_NOT_RUNNING_ERR);
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 78d8e29..f58caff 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -1217,6 +1217,12 @@
optional bool has_audio = 3;
optional bool has_hid = 4;
optional bool has_storage = 5;
+ enum State {
+ STATE_DISCONNECTED = 0;
+ STATE_CONNECTED = 1;
+ }
+ optional State state = 6;
+ optional int64 last_connect_duration_ms = 7;
}
@@ -1265,14 +1271,18 @@
* Logs when something is plugged into or removed from the USB-C connector.
*
* Logged from:
- * Vendor USB HAL.
+ * UsbService
*/
message UsbConnectorStateChanged {
enum State {
- DISCONNECTED = 0;
- CONNECTED = 1;
+ STATE_DISCONNECTED = 0;
+ STATE_CONNECTED = 1;
}
optional State state = 1;
+ optional string id = 2;
+ // Last active session in ms.
+ // 0 when the port is in connected state.
+ optional int64 last_connect_duration_millis = 3;
}
/**
diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.cpp b/cmds/statsd/src/metrics/ValueMetricProducer.cpp
index f25520b..1f22a6a 100644
--- a/cmds/statsd/src/metrics/ValueMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/ValueMetricProducer.cpp
@@ -432,6 +432,26 @@
return false;
}
+bool ValueMetricProducer::hitFullBucketGuardRailLocked(const MetricDimensionKey& newKey) {
+ // ===========GuardRail==============
+ // 1. Report the tuple count if the tuple count > soft limit
+ if (mCurrentFullBucket.find(newKey) != mCurrentFullBucket.end()) {
+ return false;
+ }
+ if (mCurrentFullBucket.size() > mDimensionSoftLimit - 1) {
+ size_t newTupleCount = mCurrentFullBucket.size() + 1;
+ // 2. Don't add more tuples, we are above the allowed threshold. Drop the data.
+ if (newTupleCount > mDimensionHardLimit) {
+ ALOGE("ValueMetric %lld dropping data for full bucket dimension key %s",
+ (long long)mMetricId,
+ newKey.toString().c_str());
+ return true;
+ }
+ }
+
+ return false;
+}
+
bool getDoubleOrLong(const LogEvent& event, const Matcher& matcher, Value& ret) {
for (const FieldValue& value : event.getValues()) {
if (value.mField.matches(matcher)) {
@@ -496,6 +516,7 @@
VLOG("Failed to get value %d from event %s", i, event.ToString().c_str());
return;
}
+ interval.seenNewData = true;
if (mUseDiff) {
if (!interval.hasBase) {
@@ -648,6 +669,9 @@
// Accumulate partial buckets with current value and then send to anomaly tracker.
if (mCurrentFullBucket.size() > 0) {
for (const auto& slice : mCurrentSlicedBucket) {
+ if (hitFullBucketGuardRailLocked(slice.first)) {
+ continue;
+ }
// TODO: fix this when anomaly can accept double values
mCurrentFullBucket[slice.first] += slice.second[0].value.long_value;
}
@@ -679,11 +703,21 @@
}
}
- // Reset counters
- for (auto& slice : mCurrentSlicedBucket) {
- for (auto& interval : slice.second) {
+ for (auto it = mCurrentSlicedBucket.begin(); it != mCurrentSlicedBucket.end();) {
+ bool obsolete = true;
+ for (auto& interval : it->second) {
interval.hasValue = false;
interval.sampleSize = 0;
+ if (interval.seenNewData) {
+ obsolete = false;
+ }
+ interval.seenNewData = false;
+ }
+
+ if (obsolete) {
+ it = mCurrentSlicedBucket.erase(it);
+ } else {
+ it++;
}
}
}
diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.h b/cmds/statsd/src/metrics/ValueMetricProducer.h
index 36ae214..4991af4 100644
--- a/cmds/statsd/src/metrics/ValueMetricProducer.h
+++ b/cmds/statsd/src/metrics/ValueMetricProducer.h
@@ -128,7 +128,9 @@
int sampleSize;
// If this dimension has any non-tainted value. If not, don't report the
// dimension.
- bool hasValue;
+ bool hasValue = false;
+ // Whether new data is seen in the bucket.
+ bool seenNewData = false;
} Interval;
std::unordered_map<MetricDimensionKey, std::vector<Interval>> mCurrentSlicedBucket;
@@ -146,6 +148,8 @@
// Util function to check whether the specified dimension hits the guardrail.
bool hitGuardRailLocked(const MetricDimensionKey& newKey);
+ bool hitFullBucketGuardRailLocked(const MetricDimensionKey& newKey);
+
void pullAndMatchEventsLocked(const int64_t timestampNs);
// Reset diff base and mHasGlobalBase
@@ -202,6 +206,7 @@
FRIEND_TEST(ValueMetricProducerTest, TestSkipZeroDiffOutput);
FRIEND_TEST(ValueMetricProducerTest, TestUseZeroDefaultBase);
FRIEND_TEST(ValueMetricProducerTest, TestUseZeroDefaultBaseWithPullFailures);
+ FRIEND_TEST(ValueMetricProducerTest, TestTrimUnusedDimensionKey);
};
} // namespace statsd
diff --git a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
index 1bd34f5..5524503 100644
--- a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
@@ -1469,7 +1469,7 @@
}
/*
- * Tests pulled atoms with no conditions
+ * Tests zero default base.
*/
TEST(ValueMetricProducerTest, TestUseZeroDefaultBase) {
ValueMetric metric;
@@ -1554,7 +1554,7 @@
}
/*
- * Tests pulled atoms with no conditions
+ * Tests using zero default base with failed pull.
*/
TEST(ValueMetricProducerTest, TestUseZeroDefaultBaseWithPullFailures) {
ValueMetric metric;
@@ -1681,6 +1681,129 @@
EXPECT_EQ(2UL, valueProducer.mPastBuckets.size());
}
+/*
+ * Tests trim unused dimension key if no new data is seen in an entire bucket.
+ */
+TEST(ValueMetricProducerTest, TestTrimUnusedDimensionKey) {
+ ValueMetric metric;
+ metric.set_id(metricId);
+ metric.set_bucket(ONE_MINUTE);
+ metric.mutable_value_field()->set_field(tagId);
+ metric.mutable_value_field()->add_child()->set_field(2);
+ metric.mutable_dimensions_in_what()->set_field(tagId);
+ metric.mutable_dimensions_in_what()->add_child()->set_field(1);
+
+ UidMap uidMap;
+ SimpleAtomMatcher atomMatcher;
+ atomMatcher.set_atom_id(tagId);
+ sp<EventMatcherWizard> eventMatcherWizard =
+ new EventMatcherWizard({new SimpleLogMatchingTracker(
+ atomMatcherId, logEventMatcherIndex, atomMatcher, uidMap)});
+ sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
+ sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>();
+ EXPECT_CALL(*pullerManager, RegisterReceiver(tagId, _, _, _)).WillOnce(Return());
+ EXPECT_CALL(*pullerManager, UnRegisterReceiver(tagId, _)).WillOnce(Return());
+ EXPECT_CALL(*pullerManager, Pull(tagId, _, _))
+ .WillOnce(Invoke([](int tagId, int64_t timeNs,
+ vector<std::shared_ptr<LogEvent>>* data) {
+ data->clear();
+ shared_ptr<LogEvent> event = make_shared<LogEvent>(tagId, bucketStartTimeNs);
+ event->write(1);
+ event->write(3);
+ event->init();
+ data->push_back(event);
+ return true;
+ }));
+
+ ValueMetricProducer valueProducer(kConfigKey, metric, -1 /*-1 meaning no condition*/, wizard,
+ logEventMatcherIndex, eventMatcherWizard, tagId,
+ bucketStartTimeNs, bucketStartTimeNs, pullerManager);
+
+ EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
+ auto iter = valueProducer.mCurrentSlicedBucket.begin();
+ auto& interval1 = iter->second[0];
+ EXPECT_EQ(1, iter->first.getDimensionKeyInWhat().getValues()[0].mValue.int_value);
+ EXPECT_EQ(true, interval1.hasBase);
+ EXPECT_EQ(3, interval1.base.long_value);
+ EXPECT_EQ(false, interval1.hasValue);
+ EXPECT_EQ(0UL, valueProducer.mPastBuckets.size());
+ vector<shared_ptr<LogEvent>> allData;
+
+ allData.clear();
+ shared_ptr<LogEvent> event1 = make_shared<LogEvent>(tagId, bucket2StartTimeNs + 1);
+ event1->write(2);
+ event1->write(4);
+ event1->init();
+ shared_ptr<LogEvent> event2 = make_shared<LogEvent>(tagId, bucket2StartTimeNs + 1);
+ event2->write(1);
+ event2->write(11);
+ event2->init();
+ allData.push_back(event1);
+ allData.push_back(event2);
+
+ valueProducer.onDataPulled(allData);
+ EXPECT_EQ(2UL, valueProducer.mCurrentSlicedBucket.size());
+ EXPECT_EQ(true, interval1.hasBase);
+ EXPECT_EQ(11, interval1.base.long_value);
+ EXPECT_EQ(true, interval1.hasValue);
+ EXPECT_EQ(8, interval1.value.long_value);
+ EXPECT_TRUE(interval1.seenNewData);
+
+ auto it = valueProducer.mCurrentSlicedBucket.begin();
+ for (; it != valueProducer.mCurrentSlicedBucket.end(); it++) {
+ if (it != iter) {
+ break;
+ }
+ }
+ EXPECT_TRUE(it != iter);
+ auto& interval2 = it->second[0];
+ EXPECT_EQ(2, it->first.getDimensionKeyInWhat().getValues()[0].mValue.int_value);
+ EXPECT_EQ(true, interval2.hasBase);
+ EXPECT_EQ(4, interval2.base.long_value);
+ EXPECT_EQ(false, interval2.hasValue);
+ EXPECT_TRUE(interval2.seenNewData);
+ EXPECT_EQ(0UL, valueProducer.mPastBuckets.size());
+
+ // next pull somehow did not happen, skip to end of bucket 3
+ allData.clear();
+ event1 = make_shared<LogEvent>(tagId, bucket4StartTimeNs + 1);
+ event1->write(2);
+ event1->write(5);
+ event1->init();
+ allData.push_back(event1);
+ valueProducer.onDataPulled(allData);
+
+ EXPECT_EQ(2UL, valueProducer.mCurrentSlicedBucket.size());
+
+ EXPECT_EQ(false, interval1.hasBase);
+ EXPECT_EQ(false, interval1.hasValue);
+ EXPECT_EQ(8, interval1.value.long_value);
+ // on probation now
+ EXPECT_FALSE(interval1.seenNewData);
+
+ EXPECT_EQ(true, interval2.hasBase);
+ EXPECT_EQ(5, interval2.base.long_value);
+ EXPECT_EQ(false, interval2.hasValue);
+ // back to good status
+ EXPECT_TRUE(interval2.seenNewData);
+ EXPECT_EQ(1UL, valueProducer.mPastBuckets.size());
+
+ allData.clear();
+ event1 = make_shared<LogEvent>(tagId, bucket5StartTimeNs + 1);
+ event1->write(2);
+ event1->write(13);
+ event1->init();
+ allData.push_back(event1);
+ valueProducer.onDataPulled(allData);
+
+ EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size());
+ EXPECT_EQ(true, interval2.hasBase);
+ EXPECT_EQ(13, interval2.base.long_value);
+ EXPECT_EQ(true, interval2.hasValue);
+ EXPECT_EQ(8, interval2.value.long_value);
+ EXPECT_EQ(1UL, valueProducer.mPastBuckets.size());
+}
+
} // namespace statsd
} // namespace os
} // namespace android
diff --git a/config/hiddenapi-greylist.txt b/config/hiddenapi-greylist.txt
index 56dc4c1..bacb991 100644
--- a/config/hiddenapi-greylist.txt
+++ b/config/hiddenapi-greylist.txt
@@ -1639,10 +1639,6 @@
Landroid/widget/QuickContactBadge$QueryHandler;-><init>(Landroid/widget/QuickContactBadge;Landroid/content/ContentResolver;)V
Landroid/widget/RelativeLayout$DependencyGraph$Node;-><init>()V
Landroid/widget/ScrollBarDrawable;-><init>()V
-Lcom/android/i18n/phonenumbers/Phonenumber$PhoneNumber$CountryCodeSource;->values()[Lcom/android/i18n/phonenumbers/Phonenumber$PhoneNumber$CountryCodeSource;
-Lcom/android/i18n/phonenumbers/PhoneNumberUtil$MatchType;->values()[Lcom/android/i18n/phonenumbers/PhoneNumberUtil$MatchType;
-Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberFormat;->values()[Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberFormat;
-Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberType;->values()[Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberType;
Lcom/android/ims/ImsCall;->deflect(Ljava/lang/String;)V
Lcom/android/ims/ImsCall;->isMultiparty()Z
Lcom/android/ims/ImsCall;->reject(I)V
diff --git a/core/java/android/annotation/UnsupportedAppUsage.java b/core/java/android/annotation/UnsupportedAppUsage.java
index 65e3f25..ac3daaf 100644
--- a/core/java/android/annotation/UnsupportedAppUsage.java
+++ b/core/java/android/annotation/UnsupportedAppUsage.java
@@ -26,16 +26,32 @@
import java.lang.annotation.Target;
/**
- * Indicates that a class member, that is not part of the SDK, is used by apps.
- * Since the member is not part of the SDK, such use is not supported.
+ * Indicates that this non-SDK interface is used by apps. A non-SDK interface is a
+ * class member (field or method) that is not part of the public SDK. Since the
+ * member is not part of the SDK, usage by apps is not supported.
*
- * <p>This annotation acts as a heads up that changing a given method or field
+ * <h2>If you are an Android App developer</h2>
+ *
+ * This annotation indicates that you may be able to access the member, but that
+ * this access is discouraged and not supported by Android. If there is a value
+ * for {@link #maxTargetSdk()} on the annotation, access will be restricted based
+ * on the {@code targetSdkVersion} value set in your manifest.
+ *
+ * <p>Fields and methods annotated with this are likely to be restricted, changed
+ * or removed in future Android releases. If you rely on these members for
+ * functionality that is not otherwise supported by Android, consider filing a
+ * <a href="http://g.co/dev/appcompat">feature request</a>.
+ *
+ * <h2>If you are an Android OS developer</h2>
+ *
+ * This annotation acts as a heads up that changing a given method or field
* may affect apps, potentially breaking them when the next Android version is
* released. In some cases, for members that are heavily used, this annotation
* may imply restrictions on changes to the member.
*
* <p>This annotation also results in access to the member being permitted by the
- * runtime, with a warning being generated in debug builds.
+ * runtime, with a warning being generated in debug builds. Which apps can access
+ * the member is determined by the value of {@link #maxTargetSdk()}.
*
* <p>For more details, see go/UnsupportedAppUsage.
*
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 61b9d55..5e445d14 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -6755,8 +6755,8 @@
case "--autofill":
dumpAutofillManager(prefix, writer);
return;
- case "--intelligence":
- dumpIntelligenceManager(prefix, writer);
+ case "--contentcapture":
+ dumpContentCaptureManager(prefix, writer);
return;
}
}
@@ -6788,7 +6788,7 @@
mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);
dumpAutofillManager(prefix, writer);
- dumpIntelligenceManager(prefix, writer);
+ dumpContentCaptureManager(prefix, writer);
ResourcesManager.getInstance().dump(prefix, writer);
}
@@ -6804,12 +6804,12 @@
}
}
- void dumpIntelligenceManager(String prefix, PrintWriter writer) {
- final ContentCaptureManager im = getContentCaptureManager();
- if (im != null) {
- im.dump(prefix, writer);
+ void dumpContentCaptureManager(String prefix, PrintWriter writer) {
+ final ContentCaptureManager cm = getContentCaptureManager();
+ if (cm != null) {
+ cm.dump(prefix, writer);
} else {
- writer.print(prefix); writer.println("No IntelligenceManager");
+ writer.print(prefix); writer.println("No ContentCaptureManager");
}
}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 8e54961..81eac5a 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -10239,4 +10239,73 @@
}
return Collections.emptySet();
}
+
+ /**
+ * Returns whether the device is being used as a managed kiosk, as defined in the CDD. As of
+ * this release, these requirements are as follows:
+ * <ul>
+ * <li>The device is in Lock Task (therefore there is also a Device Owner app on the
+ * device)</li>
+ * <li>The Lock Task feature {@link DevicePolicyManager#LOCK_TASK_FEATURE_SYSTEM_INFO} is
+ * not enabled, so the system info in the status bar is not visible</li>
+ * <li>The device does not have a secure lock screen (e.g. it has no lock screen or has
+ * swipe-to-unlock)</li>
+ * <li>The device is not in the middle of an ephemeral user session</li>
+ * </ul>
+ *
+ * <p>Publicly-accessible dedicated devices don't have the same privacy model as
+ * personally-used devices. In particular, user consent popups don't make sense as a barrier to
+ * accessing persistent data on these devices since the user giving consent and the user whose
+ * data is on the device are unlikely to be the same. These consent popups prevent the true
+ * remote management of these devices.
+ *
+ * <p>This condition is not sufficient to cover APIs that would access data that only lives for
+ * the duration of the user's session, since the user has an expectation of privacy in these
+ * conditions that more closely resembles use of a personal device. In those cases, see {@link
+ * #isUnattendedManagedKiosk()}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+ public boolean isManagedKiosk() {
+ throwIfParentInstance("isManagedKiosk");
+ if (mService != null) {
+ try {
+ return mService.isManagedKiosk();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether the device is being used as an unattended managed kiosk, as defined in the
+ * CDD. As of this release, these requirements are as follows:
+ * <ul>
+ * <li>The device is being used as a managed kiosk, as defined in the CDD and verified at
+ * {@link #isManagedKiosk()}</li>
+ * <li>The device has not received user input for at least 30 minutes</li>
+ * </ul>
+ *
+ * <p>See {@link #isManagedKiosk()} for context. This is a stronger requirement that also
+ * ensures that the device hasn't been interacted with recently, making it an appropriate check
+ * for privacy-sensitive APIs that wouldn't be appropriate during an active user session.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+ public boolean isUnattendedManagedKiosk() {
+ throwIfParentInstance("isUnattendedManagedKiosk");
+ if (mService != null) {
+ try {
+ return mService.isUnattendedManagedKiosk();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
}
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 1148685..1ff4146 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -428,4 +428,7 @@
List<String> getCrossProfileCalendarPackages(in ComponentName admin);
boolean isPackageAllowedToAccessCalendarForUser(String packageName, int userHandle);
List<String> getCrossProfileCalendarPackagesForUser(int userHandle);
+
+ boolean isManagedKiosk();
+ boolean isUnattendedManagedKiosk();
}
diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl
index 3e20748..0afb98f 100644
--- a/core/java/android/app/backup/IBackupManager.aidl
+++ b/core/java/android/app/backup/IBackupManager.aidl
@@ -91,6 +91,15 @@
* at some point in the future.
*
* <p>Callers must hold the android.permission.BACKUP permission to use this method.
+ * If {@code userId} is different from the calling user id, then the caller must hold the
+ * android.permission.INTERACT_ACROSS_USERS_FULL permission.
+ *
+ * @param userId User id for which backup service should be enabled/disabled.
+ */
+ void setBackupEnabledForUser(int userId, boolean isEnabled);
+
+ /**
+ * {@link android.app.backup.IBackupManager.setBackupEnabledForUser} for the calling user id.
*/
void setBackupEnabled(boolean isEnabled);
@@ -120,6 +129,15 @@
* Report whether the backup mechanism is currently enabled.
*
* <p>Callers must hold the android.permission.BACKUP permission to use this method.
+ * If {@code userId} is different from the calling user id, then the caller must hold the
+ * android.permission.INTERACT_ACROSS_USERS_FULL permission.
+ *
+ * @param userId User id for which the backup service status should be reported.
+ */
+ boolean isBackupEnabledForUser(int userId);
+
+ /**
+ * {@link android.app.backup.IBackupManager.isBackupEnabledForUser} for the calling user id.
*/
boolean isBackupEnabled();
@@ -149,6 +167,15 @@
* method.
*
* <p>Callers must hold the android.permission.BACKUP permission to use this method.
+ * If {@code userId} is different from the calling user id, then the caller must hold the
+ * android.permission.INTERACT_ACROSS_USERS_FULL permission.
+ *
+ * @param userId User id for which an immediate backup should be scheduled.
+ */
+ void backupNowForUser(int userId);
+
+ /**
+ * {@link android.app.backup.IBackupManager.backupNowForUser} for the calling user id.
*/
void backupNow();
@@ -432,6 +459,12 @@
* <p>If this method returns zero (meaning success), the OS will attempt to backup all provided
* packages using the remote transport.
*
+ * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+ * If {@code userId} is different from the calling user id, then the caller must hold the
+ * android.permission.INTERACT_ACROSS_USERS_FULL permission.
+ *
+ * @param userId User id for which an immediate backup should be requested.
+
* @param observer The {@link BackupObserver} to receive callbacks during the backup
* operation.
*
@@ -442,12 +475,29 @@
*
* @return Zero on success; nonzero on error.
*/
+ int requestBackupForUser(int userId, in String[] packages, IBackupObserver observer,
+ IBackupManagerMonitor monitor, int flags);
+
+ /**
+ * {@link android.app.backup.IBackupManager.requestBackupForUser} for the calling user id.
+ */
int requestBackup(in String[] packages, IBackupObserver observer, IBackupManagerMonitor monitor,
int flags);
/**
* Cancel all running backups. After this call returns, no currently running backups will
* interact with the selected transport.
+ *
+ * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+ * If {@code userId} is different from the calling user id, then the caller must hold the
+ * android.permission.INTERACT_ACROSS_USERS_FULL permission.
+ *
+ * @param userId User id for which backups should be cancelled.
+ */
+ void cancelBackupsForUser(int userId);
+
+ /**
+ * {@link android.app.backup.IBackupManager.cancelBackups} for the calling user id.
*/
void cancelBackups();
}
diff --git a/core/java/android/app/role/IOnRoleHoldersChangedListener.aidl b/core/java/android/app/role/IOnRoleHoldersChangedListener.aidl
new file mode 100644
index 0000000..6cf961f
--- /dev/null
+++ b/core/java/android/app/role/IOnRoleHoldersChangedListener.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.role;
+
+/**
+ * @hide
+ */
+oneway interface IOnRoleHoldersChangedListener {
+
+ void onRoleHoldersChanged(String roleName, int userId);
+}
diff --git a/core/java/android/app/role/IRoleManager.aidl b/core/java/android/app/role/IRoleManager.aidl
index 3ca8ec0..4ce0f31 100644
--- a/core/java/android/app/role/IRoleManager.aidl
+++ b/core/java/android/app/role/IRoleManager.aidl
@@ -16,6 +16,7 @@
package android.app.role;
+import android.app.role.IOnRoleHoldersChangedListener;
import android.app.role.IRoleManagerCallback;
/**
@@ -37,6 +38,11 @@
void clearRoleHoldersAsUser(in String roleName, int userId, in IRoleManagerCallback callback);
+ void addOnRoleHoldersChangedListenerAsUser(IOnRoleHoldersChangedListener listener, int userId);
+
+ void removeOnRoleHoldersChangedListenerAsUser(IOnRoleHoldersChangedListener listener,
+ int userId);
+
void setRoleNamesFromController(in List<String> roleNames);
boolean addRoleHolderFromController(in String roleName, in String packageName);
diff --git a/core/java/android/app/role/OnRoleHoldersChangedListener.java b/core/java/android/app/role/OnRoleHoldersChangedListener.java
new file mode 100644
index 0000000..5958deb
--- /dev/null
+++ b/core/java/android/app/role/OnRoleHoldersChangedListener.java
@@ -0,0 +1,38 @@
+/*
+ * 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.role;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.UserHandle;
+
+/**
+ * Listener for role holder changes.
+ *
+ * @hide
+ */
+@SystemApi
+public interface OnRoleHoldersChangedListener {
+
+ /**
+ * Called when the holders of roles are changed.
+ *
+ * @param roleName the name of the role whose holders are changed
+ * @param user the user for this role holder change
+ */
+ void onRoleHoldersChanged(@NonNull String roleName, @NonNull UserHandle user);
+}
diff --git a/core/java/android/app/role/RoleManager.java b/core/java/android/app/role/RoleManager.java
index f3b2153..5d101ab 100644
--- a/core/java/android/app/role/RoleManager.java
+++ b/core/java/android/app/role/RoleManager.java
@@ -23,6 +23,7 @@
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
+import android.annotation.UserIdInt;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
@@ -30,8 +31,12 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
+import com.android.internal.util.function.pooled.PooledLambda;
import java.util.List;
import java.util.concurrent.Executor;
@@ -125,6 +130,13 @@
@NonNull
private final IRoleManager mService;
+ @GuardedBy("mListenersLock")
+ @NonNull
+ private final SparseArray<ArrayMap<OnRoleHoldersChangedListener,
+ OnRoleHoldersChangedListenerDelegate>> mListeners = new SparseArray<>();
+ @NonNull
+ private final Object mListenersLock = new Object();
+
/**
* @hide
*/
@@ -146,8 +158,6 @@
* @param roleName the name of requested role
*
* @return the {@code Intent} to prompt user to grant the role
- *
- * @throws IllegalArgumentException if {@code role} is {@code null} or empty
*/
@NonNull
public Intent createRequestRoleIntent(@NonNull String roleName) {
@@ -164,8 +174,6 @@
* @param roleName the name of role to checking for
*
* @return whether the role is available in the system
- *
- * @throws IllegalArgumentException if the role name is {@code null} or empty
*/
public boolean isRoleAvailable(@NonNull String roleName) {
Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
@@ -182,8 +190,6 @@
* @param roleName the name of the role to check for
*
* @return whether the calling application is holding the role
- *
- * @throws IllegalArgumentException if the role name is {@code null} or empty.
*/
public boolean isRoleHeld(@NonNull String roleName) {
Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
@@ -204,8 +210,6 @@
*
* @return a list of package names of the role holders, or an empty list if none.
*
- * @throws IllegalArgumentException if the role name is {@code null} or empty.
- *
* @see #getRoleHoldersAsUser(String, UserHandle)
*
* @hide
@@ -230,8 +234,6 @@
*
* @return a list of package names of the role holders, or an empty list if none.
*
- * @throws IllegalArgumentException if the role name is {@code null} or empty.
- *
* @see #addRoleHolderAsUser(String, String, UserHandle, Executor, RoleManagerCallback)
* @see #removeRoleHolderAsUser(String, String, UserHandle, Executor, RoleManagerCallback)
* @see #clearRoleHoldersAsUser(String, UserHandle, Executor, RoleManagerCallback)
@@ -266,8 +268,6 @@
* @param executor the {@code Executor} to run the callback on.
* @param callback the callback for whether this call is successful
*
- * @throws IllegalArgumentException if the role name or package name is {@code null} or empty.
- *
* @see #getRoleHoldersAsUser(String, UserHandle)
* @see #removeRoleHolderAsUser(String, String, UserHandle, Executor, RoleManagerCallback)
* @see #clearRoleHoldersAsUser(String, UserHandle, Executor, RoleManagerCallback)
@@ -306,8 +306,6 @@
* @param executor the {@code Executor} to run the callback on.
* @param callback the callback for whether this call is successful
*
- * @throws IllegalArgumentException if the role name or package name is {@code null} or empty.
- *
* @see #getRoleHoldersAsUser(String, UserHandle)
* @see #addRoleHolderAsUser(String, String, UserHandle, Executor, RoleManagerCallback)
* @see #clearRoleHoldersAsUser(String, UserHandle, Executor, RoleManagerCallback)
@@ -345,8 +343,6 @@
* @param executor the {@code Executor} to run the callback on.
* @param callback the callback for whether this call is successful
*
- * @throws IllegalArgumentException if the role name is {@code null} or empty.
- *
* @see #getRoleHoldersAsUser(String, UserHandle)
* @see #addRoleHolderAsUser(String, String, UserHandle, Executor, RoleManagerCallback)
* @see #removeRoleHolderAsUser(String, String, UserHandle, Executor, RoleManagerCallback)
@@ -371,6 +367,96 @@
}
/**
+ * Add a listener to observe role holder changes
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.OBSERVE_ROLE_HOLDERS} and if the user id is not the current user
+ * {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
+ *
+ * @param executor the {@code Executor} to call the listener on.
+ * @param listener the listener to be added
+ * @param user the user to add the listener for
+ *
+ * @see #removeOnRoleHoldersChangedListenerAsUser(OnRoleHoldersChangedListener, UserHandle)
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.OBSERVE_ROLE_HOLDERS)
+ @SystemApi
+ public void addOnRoleHoldersChangedListenerAsUser(@CallbackExecutor @NonNull Executor executor,
+ @NonNull OnRoleHoldersChangedListener listener, @NonNull UserHandle user) {
+ Preconditions.checkNotNull(executor, "executor cannot be null");
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ Preconditions.checkNotNull(user, "user cannot be null");
+ int userId = user.getIdentifier();
+ synchronized (mListenersLock) {
+ ArrayMap<OnRoleHoldersChangedListener, OnRoleHoldersChangedListenerDelegate> listeners =
+ mListeners.get(userId);
+ if (listeners == null) {
+ listeners = new ArrayMap<>();
+ mListeners.put(userId, listeners);
+ } else {
+ if (listeners.containsKey(listener)) {
+ return;
+ }
+ }
+ OnRoleHoldersChangedListenerDelegate listenerDelegate =
+ new OnRoleHoldersChangedListenerDelegate(executor, listener);
+ try {
+ mService.addOnRoleHoldersChangedListenerAsUser(listenerDelegate, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ listeners.put(listener, listenerDelegate);
+ }
+ }
+
+ /**
+ * Remove a listener observing role holder changes
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.OBSERVE_ROLE_HOLDERS} and if the user id is not the current user
+ * {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
+ *
+ * @param listener the listener to be removed
+ * @param user the user to remove the listener for
+ *
+ * @see #addOnRoleHoldersChangedListenerAsUser(Executor, OnRoleHoldersChangedListener,
+ * UserHandle)
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.OBSERVE_ROLE_HOLDERS)
+ @SystemApi
+ public void removeOnRoleHoldersChangedListenerAsUser(
+ @NonNull OnRoleHoldersChangedListener listener, @NonNull UserHandle user) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ Preconditions.checkNotNull(user, "user cannot be null");
+ int userId = user.getIdentifier();
+ synchronized (mListenersLock) {
+ ArrayMap<OnRoleHoldersChangedListener, OnRoleHoldersChangedListenerDelegate> listeners =
+ mListeners.get(userId);
+ if (listeners == null) {
+ return;
+ }
+ OnRoleHoldersChangedListenerDelegate listenerDelegate = listeners.get(listener);
+ if (listenerDelegate == null) {
+ return;
+ }
+ try {
+ mService.removeOnRoleHoldersChangedListenerAsUser(listenerDelegate,
+ user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ listeners.remove(listener);
+ if (listeners.isEmpty()) {
+ mListeners.remove(userId);
+ }
+ }
+ }
+
+ /**
* Set the names of all the available roles. Should only be called from
* {@link android.rolecontrollerservice.RoleControllerService}.
* <p>
@@ -379,8 +465,6 @@
*
* @param roleNames the names of all the available roles
*
- * @throws IllegalArgumentException if the list of role names is {@code null}.
- *
* @hide
*/
@RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER)
@@ -408,8 +492,6 @@
* @return whether the operation was successful, and will also be {@code true} if a matching
* role holder is already found.
*
- * @throws IllegalArgumentException if the role name or package name is {@code null} or empty.
- *
* @see #getRoleHolders(String)
* @see #removeRoleHolderFromController(String, String)
*
@@ -442,8 +524,6 @@
* @return whether the operation was successful, and will also be {@code true} if no matching
* role holder was found to remove.
*
- * @throws IllegalArgumentException if the role name or package name is {@code null} or empty.
- *
* @see #getRoleHolders(String)
* @see #addRoleHolderFromController(String, String)
*
@@ -495,4 +575,31 @@
}
}
}
+
+ private static class OnRoleHoldersChangedListenerDelegate
+ extends IOnRoleHoldersChangedListener.Stub {
+
+ @NonNull
+ private final Executor mExecutor;
+ @NonNull
+ private final OnRoleHoldersChangedListener mListener;
+
+ OnRoleHoldersChangedListenerDelegate(@NonNull Executor executor,
+ @NonNull OnRoleHoldersChangedListener listener) {
+ mExecutor = executor;
+ mListener = listener;
+ }
+
+ @Override
+ public void onRoleHoldersChanged(@NonNull String roleName, @UserIdInt int userId) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(PooledLambda.obtainRunnable(
+ OnRoleHoldersChangedListener::onRoleHoldersChanged, mListener, roleName,
+ UserHandle.of(userId)));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
}
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index da65941..6704a45 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -21,6 +21,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.annotation.UnsupportedAppUsage;
import android.annotation.UserIdInt;
@@ -2978,7 +2979,12 @@
}
}
- /** {@hide} */
+ /**
+ * Put the cache with the key.
+ *
+ * @param key the key to add
+ * @param value the value to add
+ */
public void putCache(Uri key, Bundle value) {
try {
getContentService().putCache(mContext.getPackageName(), key, value,
@@ -2988,7 +2994,13 @@
}
}
- /** {@hide} */
+ /**
+ * Get the cache with the key.
+ *
+ * @param key the key to get the value
+ * @return the matched value. If the key doesn't exist, will return null.
+ * @see #putCache(Uri, Bundle)
+ */
public Bundle getCache(Uri key) {
try {
final Bundle bundle = getContentService().getCache(mContext.getPackageName(), key,
@@ -3176,7 +3188,14 @@
return mContext.getUserId();
}
- /** @hide */
+ /**
+ * Get the system drawable of the mime type.
+ *
+ * @param mimeType the requested mime type
+ * @return the matched drawable
+ * @hide
+ */
+ @SystemApi
public Drawable getTypeDrawable(String mimeType) {
return MimeIconUtils.loadMimeIcon(mContext, mimeType);
}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index b6f9d15..2f0618c 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5267,8 +5267,6 @@
* Used as a boolean extra field in {@link #ACTION_CHOOSER} intents to specify
* whether to show the chooser or not when there is only one application available
* to choose from.
- *
- * @hide
*/
public static final String EXTRA_AUTO_LAUNCH_SINGLE_CHOICE =
"android.intent.extra.AUTO_LAUNCH_SINGLE_CHOICE";
diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java
index 1c564f3..740fd7f 100644
--- a/core/java/android/content/pm/CrossProfileApps.java
+++ b/core/java/android/content/pm/CrossProfileApps.java
@@ -16,6 +16,8 @@
package android.content.pm;
import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.res.Resources;
@@ -66,7 +68,30 @@
mContext.getIApplicationThread(),
mContext.getPackageName(),
component,
- targetUser);
+ targetUser.getIdentifier(),
+ true);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Starts the specified activity of the caller package in the specified profile if the caller
+ * has {@link android.Manifest.permission#INTERACT_ACROSS_PROFILES} permission and
+ * both the caller and target user profiles are in the same user group.
+ *
+ * @param component The ComponentName of the activity to launch. It must be exported.
+ * @param targetUser The UserHandle of the profile, must be one of the users returned by
+ * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will
+ * be thrown.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES)
+ public void startAnyActivity(@NonNull ComponentName component, @NonNull UserHandle targetUser) {
+ try {
+ mService.startActivityAsUser(mContext.getIApplicationThread(),
+ mContext.getPackageName(), component, targetUser.getIdentifier(), false);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
diff --git a/core/java/android/content/pm/ICrossProfileApps.aidl b/core/java/android/content/pm/ICrossProfileApps.aidl
index bc2f92a..d2d66cb 100644
--- a/core/java/android/content/pm/ICrossProfileApps.aidl
+++ b/core/java/android/content/pm/ICrossProfileApps.aidl
@@ -28,6 +28,6 @@
*/
interface ICrossProfileApps {
void startActivityAsUser(in IApplicationThread caller, in String callingPackage,
- in ComponentName component, in UserHandle user);
+ in ComponentName component, int userId, boolean launchMainActivity);
List<UserHandle> getTargetUserProfiles(in String callingPackage);
}
\ No newline at end of file
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 0150f6a..da39b63 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -1399,6 +1399,16 @@
public static final int DELETE_DONT_KILL_APP = 0x00000008;
/**
+ * Flag parameter for {@link #deletePackage} to indicate that any
+ * contributed media should also be deleted during this uninstall. The
+ * meaning of "contributed" means it won't automatically be deleted when the
+ * app is uninstalled.
+ *
+ * @hide
+ */
+ public static final int DELETE_CONTRIBUTED_MEDIA = 0x00000010;
+
+ /**
* Flag parameter for {@link #deletePackage} to indicate that package deletion
* should be chatty.
*
diff --git a/core/java/android/content/pm/PackageManagerInternal.java b/core/java/android/content/pm/PackageManagerInternal.java
index 43c0222..5db9f50 100644
--- a/core/java/android/content/pm/PackageManagerInternal.java
+++ b/core/java/android/content/pm/PackageManagerInternal.java
@@ -54,6 +54,7 @@
public static final int PACKAGE_SYSTEM_TEXT_CLASSIFIER = 5;
public static final int PACKAGE_PERMISSION_CONTROLLER = 6;
public static final int PACKAGE_WELLBEING = 7;
+ public static final int PACKAGE_DOCUMENTER = 8;
@IntDef(value = {
PACKAGE_SYSTEM,
PACKAGE_SETUP_WIZARD,
@@ -63,6 +64,7 @@
PACKAGE_SYSTEM_TEXT_CLASSIFIER,
PACKAGE_PERMISSION_CONTROLLER,
PACKAGE_WELLBEING,
+ PACKAGE_DOCUMENTER,
})
@Retention(RetentionPolicy.SOURCE)
public @interface KnownPackage {}
diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java
index 20997d6..bb8c92d 100644
--- a/core/java/android/content/pm/PermissionInfo.java
+++ b/core/java/android/content/pm/PermissionInfo.java
@@ -192,6 +192,17 @@
@TestApi
public static final int PROTECTION_FLAG_WELLBEING = 0x20000;
+ /**
+ * Additional flag for {@link #protectionLevel}, corresponding to the
+ * {@code documenter} value of {@link android.R.attr#protectionLevel}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public static final int PROTECTION_FLAG_DOCUMENTER = 0x40000;
+
+
/** @hide */
@IntDef(flag = true, prefix = { "PROTECTION_FLAG_" }, value = {
PROTECTION_FLAG_PRIVILEGED,
@@ -209,6 +220,7 @@
PROTECTION_FLAG_VENDOR_PRIVILEGED,
PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER,
PROTECTION_FLAG_WELLBEING,
+ PROTECTION_FLAG_DOCUMENTER,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ProtectionFlags {}
@@ -401,6 +413,9 @@
if ((level & PermissionInfo.PROTECTION_FLAG_WELLBEING) != 0) {
protLevel += "|wellbeing";
}
+ if ((level & PermissionInfo.PROTECTION_FLAG_DOCUMENTER) != 0) {
+ protLevel += "|documenter";
+ }
return protLevel;
}
diff --git a/core/java/android/content/pm/SharedLibraryNames.java b/core/java/android/content/pm/SharedLibraryNames.java
index 5afc8a9..a607a9f 100644
--- a/core/java/android/content/pm/SharedLibraryNames.java
+++ b/core/java/android/content/pm/SharedLibraryNames.java
@@ -22,15 +22,15 @@
*/
public class SharedLibraryNames {
- public static final String ANDROID_HIDL_BASE = "android.hidl.base-V1.0-java";
+ static final String ANDROID_HIDL_BASE = "android.hidl.base-V1.0-java";
- public static final String ANDROID_HIDL_MANAGER = "android.hidl.manager-V1.0-java";
+ static final String ANDROID_HIDL_MANAGER = "android.hidl.manager-V1.0-java";
- public static final String ANDROID_TEST_BASE = "android.test.base";
+ static final String ANDROID_TEST_BASE = "android.test.base";
- public static final String ANDROID_TEST_MOCK = "android.test.mock";
+ static final String ANDROID_TEST_MOCK = "android.test.mock";
- public static final String ANDROID_TEST_RUNNER = "android.test.runner";
+ static final String ANDROID_TEST_RUNNER = "android.test.runner";
public static final String ORG_APACHE_HTTP_LEGACY = "org.apache.http.legacy";
}
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 2ae796c..d45fa11 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -916,23 +916,49 @@
private static native long getNativeBBinderHolder();
private static native long getFinalizer();
+ /**
+ * By default, we use the calling uid since we can always trust it.
+ */
+ private static volatile BinderInternal.WorkSourceProvider sWorkSourceProvider =
+ Binder::getCallingUid;
+
+ /**
+ * Sets the work source provider.
+ *
+ * <li>The callback is global. Only fast operations should be done to avoid thread
+ * contentions.
+ * <li>The callback implementation needs to handle synchronization if needed. The methods on the
+ * callback can be called concurrently.
+ * <li>The callback is called on the critical path of the binder transaction so be careful about
+ * performance.
+ * <li>Never execute another binder transaction inside the callback.
+ * @hide
+ */
+ public static void setWorkSourceProvider(BinderInternal.WorkSourceProvider workSourceProvider) {
+ if (workSourceProvider == null) {
+ throw new IllegalArgumentException("workSourceProvider cannot be null");
+ }
+ sWorkSourceProvider = workSourceProvider;
+ }
+
// Entry point from android_util_Binder.cpp's onTransact
private boolean execTransact(int code, long dataObj, long replyObj,
int flags) {
- final long origWorkSource = ThreadLocalWorkSource.setUid(Binder.getCallingUid());
+ final int workSourceUid = sWorkSourceProvider.resolveWorkSourceUid();
+ final long origWorkSource = ThreadLocalWorkSource.setUid(workSourceUid);
try {
- return execTransactInternal(code, dataObj, replyObj, flags);
+ return execTransactInternal(code, dataObj, replyObj, flags, workSourceUid);
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
}
}
private boolean execTransactInternal(int code, long dataObj, long replyObj,
- int flags) {
+ int flags, int workSourceUid) {
// Make sure the observer won't change while processing a transaction.
final BinderInternal.Observer observer = sObserver;
final CallSession callSession =
- observer != null ? observer.callStarted(this, code) : null;
+ observer != null ? observer.callStarted(this, code, workSourceUid) : null;
Parcel data = Parcel.obtain(dataObj);
Parcel reply = Parcel.obtain(replyObj);
// theoretically, we should call transact, which will call onTransact,
@@ -971,10 +997,11 @@
if (tracingEnabled) {
Trace.traceEnd(Trace.TRACE_TAG_ALWAYS);
}
+ if (observer != null) {
+ observer.callEnded(callSession, data.dataSize(), reply.dataSize(), workSourceUid);
+ }
}
checkParcel(this, code, reply, "Unreasonably large binder reply buffer");
- int replySizeBytes = reply.dataSize();
- int requestSizeBytes = data.dataSize();
reply.recycle();
data.recycle();
@@ -984,9 +1011,6 @@
// to the main transaction loop to wait for another incoming transaction. Either
// way, strict mode begone!
StrictMode.clearGatheredViolations();
- if (observer != null) {
- observer.callEnded(callSession, requestSizeBytes, replySizeBytes);
- }
return res;
}
}
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
old mode 100644
new mode 100755
index 292543c..9fea873
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -1226,7 +1226,8 @@
* null (if, for instance, the radio is not currently on).
*/
public static String getRadioVersion() {
- return SystemProperties.get(TelephonyProperties.PROPERTY_BASEBAND_VERSION, null);
+ String propVal = SystemProperties.get(TelephonyProperties.PROPERTY_BASEBAND_VERSION);
+ return TextUtils.isEmpty(propVal) ? null : propVal;
}
private static String getString(String property) {
diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java
index 2cb5aee..d55489a 100644
--- a/core/java/android/os/PowerManagerInternal.java
+++ b/core/java/android/os/PowerManagerInternal.java
@@ -200,4 +200,7 @@
* PowerHint defined in android/hardware/power/<version 1.0 & up>/IPower.h
*/
public abstract void powerHint(int hintId, int data);
+
+ /** Returns whether there hasn't been a user activity event for the given number of ms. */
+ public abstract boolean wasDeviceIdleFor(long ms);
}
diff --git a/core/java/android/os/Temperature.java b/core/java/android/os/Temperature.java
index 5499181..eee2b52 100644
--- a/core/java/android/os/Temperature.java
+++ b/core/java/android/os/Temperature.java
@@ -70,6 +70,7 @@
TYPE_BCL_VOLTAGE,
TYPE_BCL_CURRENT,
TYPE_BCL_PERCENTAGE,
+ TYPE_NPU,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Type {}
@@ -85,6 +86,7 @@
public static final int TYPE_BCL_VOLTAGE = TemperatureType.BCL_VOLTAGE;
public static final int TYPE_BCL_CURRENT = TemperatureType.BCL_CURRENT;
public static final int TYPE_BCL_PERCENTAGE = TemperatureType.BCL_PERCENTAGE;
+ public static final int TYPE_NPU = TemperatureType.NPU;
/**
* Verify a valid temperature type.
@@ -92,7 +94,7 @@
* @return true if a temperature type is valid otherwise false.
*/
public static boolean isValidType(@Type int type) {
- return type >= TYPE_UNKNOWN && type <= TYPE_BCL_PERCENTAGE;
+ return type >= TYPE_UNKNOWN && type <= TYPE_NPU;
}
/**
diff --git a/core/java/android/permission/IRuntimePermissionPresenter.aidl b/core/java/android/permission/IRuntimePermissionPresenter.aidl
index 693d21b..e95428a 100644
--- a/core/java/android/permission/IRuntimePermissionPresenter.aidl
+++ b/core/java/android/permission/IRuntimePermissionPresenter.aidl
@@ -26,4 +26,6 @@
oneway interface IRuntimePermissionPresenter {
void getAppPermissions(String packageName, in RemoteCallback callback);
void revokeRuntimePermission(String packageName, String permissionName);
+ void countPermissionApps(in List<String> permissionNames, boolean countOnlyGranted,
+ boolean countSystem, in RemoteCallback callback);
}
diff --git a/core/java/android/permission/RuntimePermissionPresenter.java b/core/java/android/permission/RuntimePermissionPresenter.java
index ed9165e..3ce3c9d 100644
--- a/core/java/android/permission/RuntimePermissionPresenter.java
+++ b/core/java/android/permission/RuntimePermissionPresenter.java
@@ -16,6 +16,7 @@
package android.permission;
+import static com.android.internal.util.Preconditions.checkCollectionElementsNotNull;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
@@ -62,16 +63,31 @@
"android.permission.RuntimePermissionPresenter.key.result";
/**
- * Listener for delivering a result.
+ * Listener for delivering the result of {@link #getAppPermissions}.
*/
- public interface OnResultCallback {
+ public interface OnGetAppPermissionResultCallback {
/**
- * The result for {@link #getAppPermissions(String, OnResultCallback, Handler)}.
+ * The result for {@link #getAppPermissions(String, OnGetAppPermissionResultCallback,
+ * Handler)}.
+ *
* @param permissions The permissions list.
*/
void onGetAppPermissions(@NonNull List<RuntimePermissionPresentationInfo> permissions);
}
+ /**
+ * Listener for delivering the result of {@link #countPermissionApps}.
+ */
+ public interface OnCountPermissionAppsResultCallback {
+ /**
+ * The result for {@link #countPermissionApps(List, boolean,
+ * OnCountPermissionAppsResultCallback, Handler)}.
+ *
+ * @param numApps The number of apps that have one of the permissions
+ */
+ void onCountPermissionApps(int numApps);
+ }
+
private static final Object sLock = new Object();
@GuardedBy("sLock")
@@ -106,7 +122,7 @@
* @param handler Handler on which to invoke the callback.
*/
public void getAppPermissions(@NonNull String packageName,
- @NonNull OnResultCallback callback, @Nullable Handler handler) {
+ @NonNull OnGetAppPermissionResultCallback callback, @Nullable Handler handler) {
checkNotNull(packageName);
checkNotNull(callback);
@@ -129,6 +145,25 @@
mRemoteService, packageName, permissionName));
}
+ /**
+ * Count how many apps have one of a set of permissions.
+ *
+ * @param permissionNames The permissions the app might have
+ * @param countOnlyGranted Count an app only if the permission is granted to the app
+ * @param countSystem Also count system apps
+ * @param callback Callback to receive the result
+ * @param handler Handler on which to invoke the callback
+ */
+ public void countPermissionApps(@NonNull List<String> permissionNames,
+ boolean countOnlyGranted, boolean countSystem,
+ @NonNull OnCountPermissionAppsResultCallback callback, @Nullable Handler handler) {
+ checkCollectionElementsNotNull(permissionNames, "permissionNames");
+ checkNotNull(callback);
+
+ mRemoteService.processMessage(obtainMessage(RemoteService::countPermissionApps,
+ mRemoteService, permissionNames, countOnlyGranted, countSystem, callback, handler));
+ }
+
private static final class RemoteService
extends Handler implements ServiceConnection {
private static final long UNBIND_TIMEOUT_MILLIS = 10000;
@@ -184,7 +219,7 @@
}
private void getAppPermissions(@NonNull String packageName,
- @NonNull OnResultCallback callback, @Nullable Handler handler) {
+ @NonNull OnGetAppPermissionResultCallback callback, @Nullable Handler handler) {
final IRuntimePermissionPresenter remoteInstance;
synchronized (mLock) {
remoteInstance = mRemoteInstance;
@@ -241,6 +276,45 @@
}
}
+ private void countPermissionApps(@NonNull List<String> permissionNames,
+ boolean countOnlyGranted, boolean countSystem,
+ @NonNull OnCountPermissionAppsResultCallback callback, @Nullable Handler handler) {
+ final IRuntimePermissionPresenter remoteInstance;
+
+ synchronized (mLock) {
+ remoteInstance = mRemoteInstance;
+ }
+ if (remoteInstance == null) {
+ return;
+ }
+
+ try {
+ remoteInstance.countPermissionApps(permissionNames, countOnlyGranted, countSystem,
+ new RemoteCallback(result -> {
+ final int numApps;
+ if (result != null) {
+ numApps = result.getInt(KEY_RESULT);
+ } else {
+ numApps = 0;
+ }
+
+ if (handler != null) {
+ handler.post(() -> callback.onCountPermissionApps(numApps));
+ } else {
+ callback.onCountPermissionApps(numApps);
+ }
+ }, this));
+ } catch (RemoteException re) {
+ Log.e(TAG, "Error counting permission apps", re);
+ }
+
+ scheduleUnbind();
+
+ synchronized (mLock) {
+ scheduleNextMessageIfNeededLocked();
+ }
+ }
+
private void unbind() {
synchronized (mLock) {
if (mBound) {
diff --git a/core/java/android/permission/RuntimePermissionPresenterService.java b/core/java/android/permission/RuntimePermissionPresenterService.java
index e50fc5a..81ec7be 100644
--- a/core/java/android/permission/RuntimePermissionPresenterService.java
+++ b/core/java/android/permission/RuntimePermissionPresenterService.java
@@ -16,6 +16,7 @@
package android.permission;
+import static com.android.internal.util.Preconditions.checkCollectionElementsNotNull;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
@@ -81,6 +82,18 @@
public abstract void onRevokeRuntimePermission(@NonNull String packageName,
@NonNull String permissionName);
+ /**
+ * Count how many apps have one of a set of permissions.
+ *
+ * @param permissionNames The permissions the app might have
+ * @param countOnlyGranted Count an app only if the permission is granted to the app
+ * @param countSystem Also count system apps
+ *
+ * @return the number of apps that have one of the permissions
+ */
+ public abstract int onCountPermissionApps(@NonNull List<String> permissionNames,
+ boolean countOnlyGranted, boolean countSystem);
+
@Override
public final IBinder onBind(Intent intent) {
return new IRuntimePermissionPresenter.Stub() {
@@ -106,6 +119,19 @@
RuntimePermissionPresenterService.this, packageName,
permissionName));
}
+
+ @Override
+ public void countPermissionApps(List<String> permissionNames, boolean countOnlyGranted,
+ boolean countSystem, RemoteCallback callback) {
+ checkCollectionElementsNotNull(permissionNames, "permissionNames");
+ checkNotNull(callback, "callback");
+
+ mHandler.sendMessage(
+ obtainMessage(
+ RuntimePermissionPresenterService::countPermissionApps,
+ RuntimePermissionPresenterService.this, permissionNames,
+ countOnlyGranted, countSystem, callback));
+ }
};
}
@@ -119,4 +145,13 @@
callback.sendResult(null);
}
}
+
+ private void countPermissionApps(@NonNull List<String> permissionNames,
+ boolean countOnlyGranted, boolean countSystem, @NonNull RemoteCallback callback) {
+ int numApps = onCountPermissionApps(permissionNames, countOnlyGranted, countSystem);
+
+ Bundle result = new Bundle();
+ result.putInt(RuntimePermissionPresenter.KEY_RESULT, numApps);
+ callback.sendResult(result);
+ }
}
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index ff77228..d9c16a7 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -244,18 +244,34 @@
* Get string array identifies the type or types of metadata returned
* using DocumentsContract#getDocumentMetadata.
*
- * @see #getDocumentMetadata(ContentResolver, Uri)
+ * @see #getDocumentMetadata(ContentInterface, Uri)
*/
public static final String METADATA_TYPES = "android:documentMetadataTypes";
/**
* Get Exif information using DocumentsContract#getDocumentMetadata.
*
- * @see #getDocumentMetadata(ContentResolver, Uri)
+ * @see #getDocumentMetadata(ContentInterface, Uri)
*/
public static final String METADATA_EXIF = "android:documentExif";
/**
+ * Get total count of all documents currently stored under the given
+ * directory tree. Only valid for {@link Document#MIME_TYPE_DIR} documents.
+ *
+ * @see #getDocumentMetadata(ContentInterface, Uri)
+ */
+ public static final String METADATA_TREE_COUNT = "android:metadataTreeCount";
+
+ /**
+ * Get total size of all documents currently stored under the given
+ * directory tree. Only valid for {@link Document#MIME_TYPE_DIR} documents.
+ *
+ * @see #getDocumentMetadata(ContentInterface, Uri)
+ */
+ public static final String METADATA_TREE_SIZE = "android:metadataTreeSize";
+
+ /**
* Constants related to a document, including {@link Cursor} column names
* and flags.
* <p>
@@ -519,7 +535,7 @@
* using DocumentsContract#getDocumentMetadata
*
* @see #COLUMN_FLAGS
- * @see DocumentsContract#getDocumentMetadata(ContentResolver, Uri)
+ * @see DocumentsContract#getDocumentMetadata(ContentInterface, Uri)
*/
public static final int FLAG_SUPPORTS_METADATA = 1 << 14;
}
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
index 1451165..f38f740 100644
--- a/core/java/android/provider/MediaStore.java
+++ b/core/java/android/provider/MediaStore.java
@@ -23,7 +23,6 @@
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
-import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.annotation.UnsupportedAppUsage;
import android.app.Activity;
@@ -50,6 +49,7 @@
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.os.UserManager;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.os.storage.VolumeInfo;
@@ -70,6 +70,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.regex.Pattern;
/**
* The Media provider contains meta data for all available media on both internal
@@ -1046,6 +1047,20 @@
getContentUri("external");
/**
+ * The MIME type for this table.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/download";
+
+ /**
+ * Regex that matches paths that needs to be considered part of downloads collection.
+ * @hide
+ */
+ public static final Pattern PATTERN_DOWNLOADS_FILE = Pattern.compile(
+ "(?i)^/storage/[^/]+/(?:[0-9]+/)?(?:Android/sandbox/[^/]+/)?Download/.+");
+ private static final Pattern PATTERN_DOWNLOADS_DIRECTORY = Pattern.compile(
+ "(?i)^/storage/[^/]+/(?:[0-9]+/)?(?:Android/sandbox/[^/]+/)?Download/?");
+
+ /**
* Get the content:// style URI for the downloads table on the
* given volume.
*
@@ -1061,6 +1076,16 @@
public static Uri getContentUriForPath(@NonNull String path) {
return getContentUri(getVolumeNameForPath(path));
}
+
+ /** @hide */
+ public static boolean isDownload(@NonNull String path) {
+ return PATTERN_DOWNLOADS_FILE.matcher(path).matches();
+ }
+
+ /** @hide */
+ public static boolean isDownloadDir(@NonNull String path) {
+ return PATTERN_DOWNLOADS_DIRECTORY.matcher(path).matches();
+ }
}
private static String getVolumeNameForPath(@NonNull String path) {
@@ -2882,18 +2907,24 @@
*
* @hide
*/
- @SystemApi
@TestApi
@RequiresPermission(android.Manifest.permission.CLEAR_APP_USER_DATA)
- public static @BytesLong long getContributedMediaSize(Context context, String packageName) {
- try (ContentProviderClient client = context.getContentResolver()
- .acquireContentProviderClient(AUTHORITY)) {
- final Bundle in = new Bundle();
- in.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
- final Bundle out = client.call(GET_CONTRIBUTED_MEDIA_CALL, null, in);
- return out.getLong(Intent.EXTRA_INDEX);
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
+ public static @BytesLong long getContributedMediaSize(Context context, String packageName,
+ UserHandle user) throws IOException {
+ final UserManager um = context.getSystemService(UserManager.class);
+ if (um.isUserUnlocked(user) && um.isUserRunning(user)) {
+ try {
+ final ContentResolver resolver = context
+ .createPackageContextAsUser(packageName, 0, user).getContentResolver();
+ final Bundle in = new Bundle();
+ in.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
+ final Bundle out = resolver.call(AUTHORITY, GET_CONTRIBUTED_MEDIA_CALL, null, in);
+ return out.getLong(Intent.EXTRA_INDEX);
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ } else {
+ throw new IOException("User " + user + " must be unlocked and running");
}
}
@@ -2904,17 +2935,23 @@
*
* @hide
*/
- @SystemApi
@TestApi
@RequiresPermission(android.Manifest.permission.CLEAR_APP_USER_DATA)
- public static void deleteContributedMedia(Context context, String packageName) {
- try (ContentProviderClient client = context.getContentResolver()
- .acquireContentProviderClient(AUTHORITY)) {
- final Bundle in = new Bundle();
- in.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
- client.call(DELETE_CONTRIBUTED_MEDIA_CALL, null, in);
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
+ public static void deleteContributedMedia(Context context, String packageName,
+ UserHandle user) throws IOException {
+ final UserManager um = context.getSystemService(UserManager.class);
+ if (um.isUserUnlocked(user) && um.isUserRunning(user)) {
+ try {
+ final ContentResolver resolver = context
+ .createPackageContextAsUser(packageName, 0, user).getContentResolver();
+ final Bundle in = new Bundle();
+ in.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
+ resolver.call(AUTHORITY, DELETE_CONTRIBUTED_MEDIA_CALL, null, in);
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ } else {
+ throw new IOException("User " + user + " must be unlocked and running");
}
}
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index de6afcb..9380695 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -12852,6 +12852,13 @@
public static final String HIDDEN_API_POLICY = "hidden_api_policy";
/**
+ * Current version of signed configuration applied.
+ *
+ * @hide
+ */
+ public static final String SIGNED_CONFIG_VERSION = "signed_config_version";
+
+ /**
* Timeout for a single {@link android.media.soundtrigger.SoundTriggerDetectionService}
* operation (in ms).
*
diff --git a/core/java/android/service/autofill/AutofillFieldClassificationService.java b/core/java/android/service/autofill/AutofillFieldClassificationService.java
index bd3c3d3..2612917 100644
--- a/core/java/android/service/autofill/AutofillFieldClassificationService.java
+++ b/core/java/android/service/autofill/AutofillFieldClassificationService.java
@@ -20,6 +20,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
@@ -35,6 +36,7 @@
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
/**
* A service that calculates field classification scores.
@@ -51,6 +53,7 @@
* {@hide}
*/
@SystemApi
+@TestApi
public abstract class AutofillFieldClassificationService extends Service {
private static final String TAG = "AutofillFieldClassificationService";
@@ -75,17 +78,32 @@
public static final String SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS =
"android.autofill.field_classification.available_algorithms";
+ /**
+ * Field classification algorithm that computes the edit distance between two Strings.
+ *
+ * <p>Service implementation must provide this algorithm.</p>
+ */
+ public static final String REQUIRED_ALGORITHM_EDIT_DISTANCE = "EDIT_DISTANCE";
+
+ /**
+ * Field classification algorithm that computes whether the last four digits between two
+ * Strings match exactly.
+ *
+ * <p>Service implementation must provide this algorithm.</p>
+ */
+ public static final String REQUIRED_ALGORITHM_EXACT_MATCH = "EXACT_MATCH";
/** {@hide} **/
public static final String EXTRA_SCORES = "scores";
private AutofillFieldClassificationServiceWrapper mWrapper;
- private void getScores(RemoteCallback callback, String algorithmName, Bundle algorithmArgs,
- List<AutofillValue> actualValues, String[] userDataValues) {
+ private void calculateScores(RemoteCallback callback, List<AutofillValue> actualValues,
+ String[] userDataValues, String[] categoryIds, String defaultAlgorithm,
+ Bundle defaultArgs, Map algorithms, Map args) {
final Bundle data = new Bundle();
- final float[][] scores = onGetScores(algorithmName, algorithmArgs, actualValues,
- Arrays.asList(userDataValues));
+ final float[][] scores = onCalculateScores(actualValues, Arrays.asList(userDataValues),
+ Arrays.asList(categoryIds), defaultAlgorithm, defaultArgs, algorithms, args);
if (scores != null) {
data.putParcelable(EXTRA_SCORES, new Scores(scores));
}
@@ -169,9 +187,12 @@
* @return the calculated scores of {@code actualValues} x {@code userDataValues}.
*
* {@hide}
+ *
+ * @deprecated Use {@link AutofillFieldClassificationService#onCalculateScores} instead.
*/
@Nullable
@SystemApi
+ @Deprecated
public float[][] onGetScores(@Nullable String algorithm,
@Nullable Bundle algorithmOptions, @NonNull List<AutofillValue> actualValues,
@NonNull List<String> userDataValues) {
@@ -179,16 +200,98 @@
return null;
}
+ /**
+ * Calculates field classification scores in a batch.
+ *
+ * <p>A field classification score is a {@code float} representing how well an
+ * {@link AutofillValue} matches a expected value predicted by an autofill service
+ * —a full match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
+ *
+ * <p>The exact score depends on the algorithm used to calculate it—the service must
+ * provide at least one default algorithm (which is used when the algorithm is not specified
+ * or is invalid), but it could provide more (in which case the algorithm name should be
+ * specified by the caller when calculating the scores).
+ *
+ * <p>For example, if the service provides an algorithm named {@code EXACT_MATCH} that
+ * returns {@code 1.0} if all characters match or {@code 0.0} otherwise, a call to:
+ *
+ * <pre>
+ * HashMap algorithms = new HashMap<>();
+ * algorithms.put("email", "EXACT_MATCH");
+ * algorithms.put("phone", "EXACT_MATCH");
+ *
+ * HashMap args = new HashMap<>();
+ * args.put("email", null);
+ * args.put("phone", null);
+ *
+ * service.onCalculateScores(Arrays.asList(AutofillValue.forText("email1"),
+ * AutofillValue.forText("PHONE1")), Arrays.asList("email1", "phone1"),
+ * Array.asList("email", "phone"), algorithms, args);
+ * </pre>
+ *
+ * <p>Returns:
+ *
+ * <pre>
+ * [
+ * [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
+ * [0.0, 0.0] // "PHONE1" compared against ["email1", "phone1"]
+ * ];
+ * </pre>
+ *
+ * <p>If the same algorithm allows the caller to specify whether the comparisons should be
+ * case sensitive by passing a boolean option named {@code "case_sensitive"}, then a call to:
+ *
+ * <pre>
+ * Bundle algorithmOptions = new Bundle();
+ * algorithmOptions.putBoolean("case_sensitive", false);
+ * args.put("phone", algorithmOptions);
+ *
+ * service.onCalculateScores(Arrays.asList(AutofillValue.forText("email1"),
+ * AutofillValue.forText("PHONE1")), Arrays.asList("email1", "phone1"),
+ * Array.asList("email", "phone"), algorithms, args);
+ * </pre>
+ *
+ * <p>Returns:
+ *
+ * <pre>
+ * [
+ * [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
+ * [0.0, 1.0] // "PHONE1" compared against ["email1", "phone1"]
+ * ];
+ * </pre>
+ *
+ * @param actualValues values entered by the user.
+ * @param userDataValues values predicted from the user data.
+ * @param categoryIds category Ids correspoinding to userDataValues
+ * @param defaultAlgorithm default field classification algorithm
+ * @param algorithms array of field classification algorithms
+ * @return the calculated scores of {@code actualValues} x {@code userDataValues}.
+ *
+ * {@hide}
+ */
+ @Nullable
+ @SystemApi
+ public float[][] onCalculateScores(@NonNull List<AutofillValue> actualValues,
+ @NonNull List<String> userDataValues, @NonNull List<String> categoryIds,
+ @Nullable String defaultAlgorithm, @Nullable Bundle defaultArgs,
+ @Nullable Map algorithms, @Nullable Map args) {
+ Log.e(TAG, "service implementation (" + getClass()
+ + " does not implement onCalculateScore()");
+ return null;
+ }
+
private final class AutofillFieldClassificationServiceWrapper
extends IAutofillFieldClassificationService.Stub {
@Override
- public void getScores(RemoteCallback callback, String algorithmName, Bundle algorithmArgs,
- List<AutofillValue> actualValues, String[] userDataValues)
- throws RemoteException {
+ public void calculateScores(RemoteCallback callback, List<AutofillValue> actualValues,
+ String[] userDataValues, String[] categoryIds, String defaultAlgorithm,
+ Bundle defaultArgs, Map algorithms, Map args)
+ throws RemoteException {
mHandler.sendMessage(obtainMessage(
- AutofillFieldClassificationService::getScores,
+ AutofillFieldClassificationService::calculateScores,
AutofillFieldClassificationService.this,
- callback, algorithmName, algorithmArgs, actualValues, userDataValues));
+ callback, actualValues, userDataValues, categoryIds, defaultAlgorithm,
+ defaultArgs, algorithms, args));
}
}
diff --git a/core/java/android/service/autofill/IAutofillFieldClassificationService.aidl b/core/java/android/service/autofill/IAutofillFieldClassificationService.aidl
index 398557d..2cd24f9 100644
--- a/core/java/android/service/autofill/IAutofillFieldClassificationService.aidl
+++ b/core/java/android/service/autofill/IAutofillFieldClassificationService.aidl
@@ -20,6 +20,7 @@
import android.os.RemoteCallback;
import android.view.autofill.AutofillValue;
import java.util.List;
+import java.util.Map;
/**
* Service used to calculate match scores for Autofill Field Classification.
@@ -27,6 +28,8 @@
* @hide
*/
oneway interface IAutofillFieldClassificationService {
- void getScores(in RemoteCallback callback, String algorithmName, in Bundle algorithmArgs,
- in List<AutofillValue> actualValues, in String[] userDataValues);
+ void calculateScores(in RemoteCallback callback, in List<AutofillValue> actualValues,
+ in String[] userDataValues, in String[] categoryIds,
+ in String defaultAlgorithm, in Bundle defaultArgs,
+ in Map algorithms, in Map args);
}
diff --git a/core/java/android/service/autofill/UserData.java b/core/java/android/service/autofill/UserData.java
index fccb85b..37f1923 100644
--- a/core/java/android/service/autofill/UserData.java
+++ b/core/java/android/service/autofill/UserData.java
@@ -24,6 +24,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.TestApi;
import android.app.ActivityThread;
import android.content.ContentResolver;
import android.os.Bundle;
@@ -32,6 +33,7 @@
import android.provider.Settings;
import android.service.autofill.FieldClassification.Match;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.view.autofill.AutofillManager;
@@ -57,28 +59,57 @@
private static final int DEFAULT_MAX_VALUE_LENGTH = 100;
private final String mId;
- private final String mAlgorithm;
- private final Bundle mAlgorithmArgs;
private final String[] mCategoryIds;
private final String[] mValues;
+ private final String mDefaultAlgorithm;
+ private final Bundle mDefaultArgs;
+ private final ArrayMap<String, String> mCategoryAlgorithms;
+ private final ArrayMap<String, Bundle> mCategoryArgs;
+
private UserData(Builder builder) {
mId = builder.mId;
- mAlgorithm = builder.mAlgorithm;
- mAlgorithmArgs = builder.mAlgorithmArgs;
mCategoryIds = new String[builder.mCategoryIds.size()];
builder.mCategoryIds.toArray(mCategoryIds);
mValues = new String[builder.mValues.size()];
builder.mValues.toArray(mValues);
+ builder.mValues.toArray(mValues);
+
+ mDefaultAlgorithm = builder.mDefaultAlgorithm;
+ mDefaultArgs = builder.mDefaultArgs;
+ mCategoryAlgorithms = builder.mCategoryAlgorithms;
+ mCategoryArgs = builder.mCategoryArgs;
}
/**
- * Gets the name of the algorithm that is used to calculate
- * {@link Match#getScore() match scores}.
+ * Gets the name of the default algorithm that is used to calculate
+ * {@link Match#getScore()} match scores}.
*/
@Nullable
public String getFieldClassificationAlgorithm() {
- return mAlgorithm;
+ return mDefaultAlgorithm;
+ }
+
+ /** @hide */
+ public Bundle getDefaultFieldClassificationArgs() {
+ return mDefaultArgs;
+ }
+
+ /**
+ * Gets the name of the algorithm corresponding to the specific autofill category
+ * that is used to calculate {@link Match#getScore() match scores}
+ *
+ * @param categoryId autofill field category
+ *
+ * @return String name of algorithm, null if none found.
+ */
+ @Nullable
+ public String getFieldClassificationAlgorithmForCategory(@NonNull String categoryId) {
+ Preconditions.checkNotNull(categoryId);
+ if (mCategoryAlgorithms == null || !mCategoryAlgorithms.containsKey(categoryId)) {
+ return null;
+ }
+ return mCategoryAlgorithms.get(categoryId);
}
/**
@@ -89,11 +120,6 @@
}
/** @hide */
- public Bundle getAlgorithmArgs() {
- return mAlgorithmArgs;
- }
-
- /** @hide */
public String[] getCategoryIds() {
return mCategoryIds;
}
@@ -104,11 +130,29 @@
}
/** @hide */
+ @TestApi
+ public ArrayMap<String, String> getFieldClassificationAlgorithms() {
+ return mCategoryAlgorithms;
+ }
+
+ /** @hide */
+ public ArrayMap<String, Bundle> getFieldClassificationArgs() {
+ return mCategoryArgs;
+ }
+
+ /** @hide */
public void dump(String prefix, PrintWriter pw) {
pw.print(prefix); pw.print("id: "); pw.print(mId);
- pw.print(prefix); pw.print("Algorithm: "); pw.print(mAlgorithm);
- pw.print(" Args: "); pw.println(mAlgorithmArgs);
-
+ pw.print(prefix); pw.print("Default Algorithm: "); pw.print(mDefaultAlgorithm);
+ pw.print(prefix); pw.print("Default Args"); pw.print(mDefaultArgs);
+ if (mCategoryAlgorithms != null && mCategoryAlgorithms.size() > 0) {
+ pw.print(prefix); pw.print("Algorithms per category: ");
+ for (int i = 0; i < mCategoryAlgorithms.size(); i++) {
+ pw.print(prefix); pw.print(prefix); pw.print(mCategoryAlgorithms.keyAt(i));
+ pw.print(": "); pw.println(Helper.getRedacted(mCategoryAlgorithms.valueAt(i)));
+ pw.print("args="); pw.print(mCategoryArgs.get(mCategoryAlgorithms.keyAt(i)));
+ }
+ }
// Cannot disclose field ids or values because they could contain PII
pw.print(prefix); pw.print("Field ids size: "); pw.println(mCategoryIds.length);
for (int i = 0; i < mCategoryIds.length; i++) {
@@ -139,8 +183,13 @@
private final String mId;
private final ArrayList<String> mCategoryIds;
private final ArrayList<String> mValues;
- private String mAlgorithm;
- private Bundle mAlgorithmArgs;
+ private String mDefaultAlgorithm;
+ private Bundle mDefaultArgs;
+
+ // Map of autofill field categories to fleid classification algorithms and args
+ private ArrayMap<String, String> mCategoryAlgorithms;
+ private ArrayMap<String, Bundle> mCategoryArgs;
+
private boolean mDestroyed;
// Non-persistent array used to limit the number of unique ids.
@@ -148,7 +197,6 @@
// Non-persistent array used to ignore duplaicated value/category pairs.
private final ArraySet<String> mUniqueValueCategoryPairs;
-
/**
* Creates a new builder for the user data used for <a href="#FieldClassification">field
* classification</a>.
@@ -169,7 +217,7 @@
* {@link AutofillManager#getUserData()}).
*
* @param value value of the user data.
- * @param categoryId string used to identify the category the value is associated with.
+ * @param categoryId autofill field category.
*
* @throws IllegalArgumentException if any of the following occurs:
* <ul>
@@ -189,13 +237,15 @@
mCategoryIds = new ArrayList<>(maxUserDataSize);
mValues = new ArrayList<>(maxUserDataSize);
mUniqueValueCategoryPairs = new ArraySet<>(maxUserDataSize);
+
mUniqueCategoryIds = new ArraySet<>(getMaxCategoryCount());
addMapping(value, categoryId);
}
/**
- * Sets the algorithm used for <a href="#FieldClassification">field classification</a>.
+ * Sets the default algorithm used for
+ * <a href="#FieldClassification">field classification</a>.
*
* <p>The currently available algorithms can be retrieve through
* {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
@@ -212,8 +262,40 @@
public Builder setFieldClassificationAlgorithm(@Nullable String name,
@Nullable Bundle args) {
throwIfDestroyed();
- mAlgorithm = name;
- mAlgorithmArgs = args;
+ mDefaultAlgorithm = name;
+ mDefaultArgs = args;
+ return this;
+ }
+
+ /**
+ * Sets the algorithm used for <a href="#FieldClassification">field classification</a>
+ * for the specified category.
+ *
+ * <p>The currently available algorithms can be retrieved through
+ * {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
+ *
+ * <p>If not set, the
+ * {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is
+ * used instead.
+ *
+ * @param categoryId autofill field category.
+ * @param name name of the algorithm or {@code null} to used default.
+ * @param args optional arguments to the algorithm.
+ *
+ * @return this builder
+ */
+ public Builder setFieldClassificationAlgorithmForCategory(@NonNull String categoryId,
+ @Nullable String name, @Nullable Bundle args) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(categoryId);
+ if (mCategoryAlgorithms == null) {
+ mCategoryAlgorithms = new ArrayMap<>(getMaxCategoryCount());
+ }
+ if (mCategoryArgs == null) {
+ mCategoryArgs = new ArrayMap<>(getMaxCategoryCount());
+ }
+ mCategoryAlgorithms.put(categoryId, name);
+ mCategoryArgs.put(categoryId, args);
return this;
}
@@ -317,8 +399,7 @@
public String toString() {
if (!sDebug) return super.toString();
- final StringBuilder builder = new StringBuilder("UserData: [id=").append(mId)
- .append(", algorithm=").append(mAlgorithm);
+ final StringBuilder builder = new StringBuilder("UserData: [id=").append(mId);
// Cannot disclose category ids or values because they could contain PII
builder.append(", categoryIds=");
Helper.appendRedacted(builder, mCategoryIds);
@@ -341,8 +422,10 @@
parcel.writeString(mId);
parcel.writeStringArray(mCategoryIds);
parcel.writeStringArray(mValues);
- parcel.writeString(mAlgorithm);
- parcel.writeBundle(mAlgorithmArgs);
+ parcel.writeString(mDefaultAlgorithm);
+ parcel.writeBundle(mDefaultArgs);
+ parcel.writeMap(mCategoryAlgorithms);
+ parcel.writeMap(mCategoryArgs);
}
public static final Parcelable.Creator<UserData> CREATOR =
@@ -355,10 +438,28 @@
final String id = parcel.readString();
final String[] categoryIds = parcel.readStringArray();
final String[] values = parcel.readStringArray();
+ final String defaultAlgorithm = parcel.readString();
+ final Bundle defaultArgs = parcel.readBundle();
+ final ArrayMap<String, String> categoryAlgorithms = new ArrayMap<>();
+ parcel.readMap(categoryAlgorithms, String.class.getClassLoader());
+ final ArrayMap<String, Bundle> categoryArgs = new ArrayMap<>();
+ parcel.readMap(categoryArgs, Bundle.class.getClassLoader());
+
final Builder builder = new Builder(id, values[0], categoryIds[0])
- .setFieldClassificationAlgorithm(parcel.readString(), parcel.readBundle());
+ .setFieldClassificationAlgorithm(defaultAlgorithm, defaultArgs);
+
for (int i = 1; i < categoryIds.length; i++) {
- builder.add(values[i], categoryIds[i]);
+ String categoryId = categoryIds[i];
+ builder.add(values[i], categoryId);
+ }
+
+ final int size = categoryAlgorithms.size();
+ if (size > 0) {
+ for (int i = 0; i < size; i++) {
+ final String categoryId = categoryAlgorithms.keyAt(i);
+ builder.setFieldClassificationAlgorithmForCategory(categoryId,
+ categoryAlgorithms.valueAt(i), categoryArgs.get(categoryId));
+ }
}
return builder.build();
}
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index dee6d90..ade7577 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -42,12 +42,15 @@
static {
DEFAULT_FLAGS = new HashMap<>();
DEFAULT_FLAGS.put("settings_audio_switcher", "true");
- DEFAULT_FLAGS.put("settings_systemui_theme", "true");
DEFAULT_FLAGS.put("settings_dynamic_homepage", "true");
DEFAULT_FLAGS.put("settings_mobile_network_v2", "true");
- DEFAULT_FLAGS.put("settings_seamless_transfer", "false");
- DEFAULT_FLAGS.put(HEARING_AID_SETTINGS, "false");
DEFAULT_FLAGS.put("settings_network_and_internet_v2", "false");
+ DEFAULT_FLAGS.put("settings_seamless_transfer", "false");
+ DEFAULT_FLAGS.put("settings_systemui_theme", "true");
+ DEFAULT_FLAGS.put("settings_wifi_dpp", "false");
+ DEFAULT_FLAGS.put("settings_wifi_mac_randomization", "false");
+ DEFAULT_FLAGS.put("settings_wifi_sharing", "false");
+ DEFAULT_FLAGS.put(HEARING_AID_SETTINGS, "false");
DEFAULT_FLAGS.put(SAFETY_HUB, "false");
DEFAULT_FLAGS.put(SCREENRECORD_LONG_PRESS, "false");
}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index ba5340c..fb4f9c0 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -38,9 +38,14 @@
private final InsetsState mState = new InsetsState();
private final Rect mFrame = new Rect();
private final SparseArray<InsetsSourceConsumer> mSourceConsumers = new SparseArray<>();
+ private final ViewRootImpl mViewRoot;
private final SparseArray<InsetsSourceControl> mTmpControlArray = new SparseArray<>();
+ public InsetsController(ViewRootImpl viewRoot) {
+ mViewRoot = viewRoot;
+ }
+
void onFrameChanged(Rect frame) {
mFrame.set(frame);
}
@@ -49,8 +54,14 @@
return mState;
}
- public void setState(InsetsState state) {
+ boolean onStateChanged(InsetsState state) {
+ if (mState.equals(state)) {
+ return false;
+ }
mState.set(state);
+ applyLocalVisibilityOverride();
+ mViewRoot.notifyInsetsChanged();
+ return true;
}
/**
@@ -105,17 +116,28 @@
}
}
+ private void applyLocalVisibilityOverride() {
+ for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
+ final InsetsSourceConsumer controller = mSourceConsumers.valueAt(i);
+ controller.applyLocalVisibilityOverride();
+ }
+ }
+
@VisibleForTesting
public @NonNull InsetsSourceConsumer getSourceConsumer(@InternalInsetType int type) {
InsetsSourceConsumer controller = mSourceConsumers.get(type);
if (controller != null) {
return controller;
}
- controller = new InsetsSourceConsumer(type, mState, Transaction::new);
+ controller = new InsetsSourceConsumer(type, mState, Transaction::new, this);
mSourceConsumers.put(type, controller);
return controller;
}
+ void notifyVisibilityChanged() {
+ mViewRoot.notifyInsetsChanged();
+ }
+
void dump(String prefix, PrintWriter pw) {
pw.println(prefix); pw.println("InsetsController:");
mState.dump(prefix + " ", pw);
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index e74aa8d..ec85c4c 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -33,27 +33,31 @@
private final Supplier<Transaction> mTransactionSupplier;
private final @InternalInsetType int mType;
private final InsetsState mState;
- private @Nullable InsetsSourceControl mControl;
+ private final InsetsController mController;
+ private @Nullable InsetsSourceControl mSourceControl;
private boolean mHidden;
public InsetsSourceConsumer(@InternalInsetType int type, InsetsState state,
- Supplier<Transaction> transactionSupplier) {
+ Supplier<Transaction> transactionSupplier, InsetsController controller) {
mType = type;
mState = state;
mTransactionSupplier = transactionSupplier;
+ mController = controller;
}
public void setControl(@Nullable InsetsSourceControl control) {
- if (mControl == control) {
+ if (mSourceControl == control) {
return;
}
- mControl = control;
+ mSourceControl = control;
applyHiddenToControl();
+ applyLocalVisibilityOverride();
+ mController.notifyVisibilityChanged();
}
@VisibleForTesting
public InsetsSourceControl getControl() {
- return mControl;
+ return mSourceControl;
}
int getType() {
@@ -70,25 +74,36 @@
setHidden(true);
}
+ void applyLocalVisibilityOverride() {
+
+ // If we don't have control, we are not able to change the visibility.
+ if (mSourceControl == null) {
+ return;
+ }
+ mState.getSource(mType).setVisible(!mHidden);
+ }
+
private void setHidden(boolean hidden) {
if (mHidden == hidden) {
return;
}
mHidden = hidden;
applyHiddenToControl();
+ applyLocalVisibilityOverride();
+ mController.notifyVisibilityChanged();
}
private void applyHiddenToControl() {
- if (mControl == null) {
+ if (mSourceControl == null) {
return;
}
// TODO: Animation
final Transaction t = mTransactionSupplier.get();
if (mHidden) {
- t.hide(mControl.getLeash());
+ t.hide(mSourceControl.getLeash());
} else {
- t.show(mControl.getLeash());
+ t.show(mSourceControl.getLeash());
}
t.apply();
}
diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java
index bf1a005..34d076f 100644
--- a/core/java/android/view/ThreadedRenderer.java
+++ b/core/java/android/view/ThreadedRenderer.java
@@ -299,6 +299,8 @@
private boolean mEnabled;
private boolean mRequested = true;
+ private FrameDrawingCallback mNextRtFrameCallback;
+
ThreadedRenderer(Context context, boolean translucent, String name) {
super();
setName(name);
@@ -432,6 +434,17 @@
}
/**
+ * Registers a callback to be executed when the next frame is being drawn on RenderThread. This
+ * callback will be executed on a RenderThread worker thread, and only used for the next frame
+ * and thus it will only fire once.
+ *
+ * @param callback The callback to register.
+ */
+ void registerRtFrameCallback(FrameDrawingCallback callback) {
+ mNextRtFrameCallback = callback;
+ }
+
+ /**
* Destroys all hardware rendering resources associated with the specified
* view hierarchy.
*
@@ -562,6 +575,15 @@
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw()");
updateViewTreeDisplayList(view);
+ // Consume and set the frame callback after we dispatch draw to the view above, but before
+ // onPostDraw below which may reset the callback for the next frame. This ensures that
+ // updates to the frame callback during scroll handling will also apply in this frame.
+ final FrameDrawingCallback callback = mNextRtFrameCallback;
+ mNextRtFrameCallback = null;
+ if (callback != null) {
+ setFrameCallback(callback);
+ }
+
if (mRootNodeNeedsUpdate || !mRootNode.hasDisplayList()) {
RecordingCanvas canvas = mRootNode.startRecording(mSurfaceWidth, mSurfaceHeight);
try {
@@ -619,10 +641,8 @@
*
* @param view The view to draw.
* @param attachInfo AttachInfo tied to the specified view.
- * @param callbacks Callbacks invoked when drawing happens.
*/
- void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks,
- FrameDrawingCallback frameDrawingCallback) {
+ void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
choreographer.mFrameInfo.markDrawStart();
@@ -642,9 +662,6 @@
attachInfo.mPendingAnimatingRenderNodes = null;
}
- if (frameDrawingCallback != null) {
- setFrameCallback(frameDrawingCallback);
- }
int syncResult = syncAndDrawFrame(choreographer.mFrameInfo);
if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
setEnabled(false);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index cb47886..e36e258 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -200,8 +200,6 @@
static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList();
static boolean sFirstDrawComplete = false;
- private FrameDrawingCallback mNextRtFrameCallback;
-
/**
* Callback for notifying about global configuration changes.
*/
@@ -464,7 +462,7 @@
final DisplayCutout.ParcelableWrapper mPendingDisplayCutout =
new DisplayCutout.ParcelableWrapper(DisplayCutout.NO_CUTOUT);
boolean mPendingAlwaysConsumeNavBar;
- private InsetsState mPendingInsets = new InsetsState();
+ private InsetsState mTempInsets = new InsetsState();
final ViewTreeObserver.InternalInsetsInfo mLastGivenInsets
= new ViewTreeObserver.InternalInsetsInfo();
@@ -552,7 +550,7 @@
InputEventConsistencyVerifier.isInstrumentationEnabled() ?
new InputEventConsistencyVerifier(this, 0) : null;
- private final InsetsController mInsetsController = new InsetsController();
+ private final InsetsController mInsetsController = new InsetsController(this);
static final class SystemUiVisibilityInfo {
int seq;
@@ -823,7 +821,7 @@
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
- mInsetsController.getState());
+ mTempInsets);
setFrame(mTmpFrame);
} catch (RemoteException e) {
mAdded = false;
@@ -851,7 +849,7 @@
mAttachInfo.mAlwaysConsumeNavBar =
(res & WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_NAV_BAR) != 0;
mPendingAlwaysConsumeNavBar = mAttachInfo.mAlwaysConsumeNavBar;
- mPendingInsets = mInsetsController.getState();
+ mInsetsController.onStateChanged(mTempInsets);
if (DEBUG_LAYOUT) Log.v(mTag, "Added window " + mWindow);
if (res < WindowManagerGlobal.ADD_OKAY) {
mAttachInfo.mRootView = null;
@@ -1052,7 +1050,9 @@
* @param callback The callback to register.
*/
public void registerRtFrameCallback(FrameDrawingCallback callback) {
- mNextRtFrameCallback = callback;
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mThreadedRenderer.registerRtFrameCallback(callback);
+ }
}
@UnsupportedAppUsage
@@ -1342,6 +1342,19 @@
scheduleTraversals();
}
+ void notifyInsetsChanged() {
+ if (!USE_NEW_INSETS) {
+ return;
+ }
+ mApplyInsetsRequested = true;
+
+ // If this changes during traversal, no need to schedule another one as it will dispatch it
+ // during the current traversal.
+ if (!mIsInTraversal) {
+ scheduleTraversals();
+ }
+ }
+
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
@@ -2027,9 +2040,6 @@
if (mPendingAlwaysConsumeNavBar != mAttachInfo.mAlwaysConsumeNavBar) {
insetsChanged = true;
}
- if (!mPendingInsets.equals(mInsetsController.getState())) {
- insetsChanged = true;
- }
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
windowSizeMayChange = true;
@@ -2223,8 +2233,6 @@
mAttachInfo.mStableInsets);
final boolean cutoutChanged = !mPendingDisplayCutout.equals(
mAttachInfo.mDisplayCutout);
- final boolean insetsStateChanged = !mPendingInsets.equals(
- mInsetsController.getState());
final boolean outsetsChanged = !mPendingOutsets.equals(mAttachInfo.mOutsets);
final boolean surfaceSizeChanged = (relayoutResult
& WindowManagerGlobal.RELAYOUT_RES_SURFACE_RESIZED) != 0;
@@ -2262,10 +2270,6 @@
mAttachInfo.mAlwaysConsumeNavBar = mPendingAlwaysConsumeNavBar;
contentInsetsChanged = true;
}
- if (insetsStateChanged) {
- mInsetsController.setState(mPendingInsets);
- contentInsetsChanged = true;
- }
if (contentInsetsChanged || mLastSystemUiVisibility !=
mAttachInfo.mSystemUiVisibility || mApplyInsetsRequested
|| mLastOverscanRequested != mAttachInfo.mOverscanRequested
@@ -3534,10 +3538,7 @@
useAsyncReport = true;
- // draw(...) might invoke post-draw, which might register the next callback already.
- final FrameDrawingCallback callback = mNextRtFrameCallback;
- mNextRtFrameCallback = null;
- mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback);
+ mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
// If we get here with a disabled & requested hardware renderer, something went
// wrong (an invalidate posted right before we destroyed the hardware surface
@@ -4378,22 +4379,12 @@
}
break;
case MSG_INSETS_CHANGED:
- mPendingInsets = (InsetsState) msg.obj;
-
- // TODO: Full traversal not needed here.
- if (USE_NEW_INSETS) {
- requestLayout();
- }
+ mInsetsController.onStateChanged((InsetsState) msg.obj);
break;
case MSG_INSETS_CONTROL_CHANGED: {
SomeArgs args = (SomeArgs) msg.obj;
- mPendingInsets = (InsetsState) args.arg1;
mInsetsController.onControlsChanged((InsetsSourceControl[]) args.arg2);
-
- // TODO: Full traversal not necessarily needed here.
- if (USE_NEW_INSETS) {
- requestLayout();
- }
+ mInsetsController.onStateChanged((InsetsState) args.arg1);
break;
}
case MSG_WINDOW_MOVED:
@@ -6794,7 +6785,7 @@
insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
mTmpFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
- mPendingMergedConfiguration, mSurface, mPendingInsets);
+ mPendingMergedConfiguration, mSurface, mTempInsets);
mPendingAlwaysConsumeNavBar =
(relayoutResult & WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_NAV_BAR) != 0;
@@ -6811,7 +6802,7 @@
mTranslator.translateRectInScreenToAppWindow(mPendingStableInsets);
}
setFrame(mTmpFrame);
-
+ mInsetsController.onStateChanged(mTempInsets);
return relayoutResult;
}
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 48831da..1889692 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -422,7 +422,7 @@
/** @hide */
public void dump(String prefix, PrintWriter pw) {
- pw.print(prefix); pw.println("IntelligenceManager");
+ pw.print(prefix); pw.println("ContentCaptureManager");
final String prefix2 = prefix + " ";
pw.print(prefix2); pw.print("mContext: "); pw.println(mContext);
pw.print(prefix2); pw.print("user: "); pw.println(mContext.getUserId());
diff --git a/core/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java
index 8bc90a8..a27dbea 100644
--- a/core/java/com/android/internal/content/FileSystemProvider.java
+++ b/core/java/com/android/internal/content/FileSystemProvider.java
@@ -39,6 +39,7 @@
import android.provider.DocumentsProvider;
import android.provider.MediaStore;
import android.provider.MetadataReader;
+import android.system.Int64Ref;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
@@ -53,6 +54,12 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.FileSystems;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -122,18 +129,56 @@
throw new FileNotFoundException("Can't find the file for documentId: " + documentId);
}
+ final String mimeType = getDocumentType(documentId);
+ if (Document.MIME_TYPE_DIR.equals(mimeType)) {
+ final Int64Ref treeCount = new Int64Ref(0);
+ final Int64Ref treeSize = new Int64Ref(0);
+ try {
+ final Path path = FileSystems.getDefault().getPath(file.getAbsolutePath());
+ Files.walkFileTree(path, new FileVisitor<Path>() {
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
+ treeCount.value += 1;
+ treeSize.value += attrs.size();
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFileFailed(Path file, IOException exc) {
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ } catch (IOException e) {
+ Log.e(TAG, "An error occurred retrieving the metadata", e);
+ return null;
+ }
+
+ final Bundle res = new Bundle();
+ res.putLong(DocumentsContract.METADATA_TREE_COUNT, treeCount.value);
+ res.putLong(DocumentsContract.METADATA_TREE_SIZE, treeSize.value);
+ return res;
+ }
+
if (!file.isFile()) {
Log.w(TAG, "Can't stream non-regular file. Returning empty metadata.");
return null;
}
-
if (!file.canRead()) {
Log.w(TAG, "Can't stream non-readable file. Returning empty metadata.");
return null;
}
-
- String mimeType = getDocumentType(documentId);
if (!MetadataReader.isSupportedMimeType(mimeType)) {
+ Log.w(TAG, "Unsupported type " + mimeType + ". Returning empty metadata.");
return null;
}
@@ -562,7 +607,8 @@
}
protected boolean typeSupportsMetadata(String mimeType) {
- return MetadataReader.isSupportedMimeType(mimeType);
+ return MetadataReader.isSupportedMimeType(mimeType)
+ || Document.MIME_TYPE_DIR.equals(mimeType);
}
protected final File getFileForDocId(String docId) throws FileNotFoundException {
diff --git a/core/java/com/android/internal/os/AppIdToPackageMap.java b/core/java/com/android/internal/os/AppIdToPackageMap.java
new file mode 100644
index 0000000..65aa989
--- /dev/null
+++ b/core/java/com/android/internal/os/AppIdToPackageMap.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 com.android.internal.os;
+
+
+import android.app.AppGlobals;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Maps AppIds to their package names. */
+public final class AppIdToPackageMap {
+ private final Map<Integer, String> mAppIdToPackageMap;
+
+ @VisibleForTesting
+ public AppIdToPackageMap(Map<Integer, String> appIdToPackageMap) {
+ mAppIdToPackageMap = appIdToPackageMap;
+ }
+
+ /** Creates a new {@link AppIdToPackageMap} for currently installed packages. */
+ public static AppIdToPackageMap getSnapshot() {
+ List<PackageInfo> packages;
+ try {
+ packages = AppGlobals.getPackageManager()
+ .getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE,
+ UserHandle.USER_SYSTEM).getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ final Map<Integer, String> map = new HashMap<>();
+ for (PackageInfo pkg : packages) {
+ final int uid = pkg.applicationInfo.uid;
+ if (pkg.sharedUserId != null && map.containsKey(uid)) {
+ // Use sharedUserId string as package name if there are collisions
+ map.put(uid, "shared:" + pkg.sharedUserId);
+ } else {
+ map.put(uid, pkg.packageName);
+ }
+ }
+ return new AppIdToPackageMap(map);
+ }
+
+ /** Maps the AppId to a package name. */
+ public String mapAppId(int appId) {
+ String pkgName = mAppIdToPackageMap.get(appId);
+ return pkgName == null ? String.valueOf(appId) : pkgName;
+ }
+
+ /** Maps the UID to a package name. */
+ public String mapUid(int uid) {
+ final int appId = UserHandle.getAppId(uid);
+ final String pkgName = mAppIdToPackageMap.get(appId);
+ final String uidStr = UserHandle.formatUid(uid);
+ return pkgName == null ? uidStr : pkgName + '/' + uidStr;
+ }
+}
diff --git a/core/java/com/android/internal/os/BinderCallsStats.java b/core/java/com/android/internal/os/BinderCallsStats.java
index ff34036..5465485 100644
--- a/core/java/com/android/internal/os/BinderCallsStats.java
+++ b/core/java/com/android/internal/os/BinderCallsStats.java
@@ -21,8 +21,6 @@
import android.os.Binder;
import android.os.Process;
import android.os.SystemClock;
-import android.os.ThreadLocalWorkSource;
-import android.os.UserHandle;
import android.text.format.DateFormat;
import android.util.ArrayMap;
import android.util.Pair;
@@ -62,7 +60,11 @@
private static final int CALL_SESSIONS_POOL_SIZE = 100;
private static final int MAX_EXCEPTION_COUNT_SIZE = 50;
private static final String EXCEPTION_COUNT_OVERFLOW_NAME = "overflow";
+ // Default values for overflow entry. The work source uid does not use a default value in order
+ // to have on overflow entry per work source uid.
private static final Class<? extends Binder> OVERFLOW_BINDER = OverflowBinder.class;
+ private static final boolean OVERFLOW_SCREEN_INTERACTIVE = false;
+ private static final int OVERFLOW_DIRECT_CALLING_UID = -1;
private static final int OVERFLOW_TRANSACTION_CODE = -1;
// Whether to collect all the data: cpu + exceptions + reply/request sizes.
@@ -106,7 +108,7 @@
@Override
@Nullable
- public CallSession callStarted(Binder binder, int code) {
+ public CallSession callStarted(Binder binder, int code, int workSourceUid) {
if (mDeviceState == null || mDeviceState.isCharging()) {
return null;
}
@@ -130,19 +132,21 @@
}
@Override
- public void callEnded(@Nullable CallSession s, int parcelRequestSize, int parcelReplySize) {
+ public void callEnded(@Nullable CallSession s, int parcelRequestSize,
+ int parcelReplySize, int workSourceUid) {
if (s == null) {
return;
}
- processCallEnded(s, parcelRequestSize, parcelReplySize);
+ processCallEnded(s, parcelRequestSize, parcelReplySize, workSourceUid);
if (mCallSessionsPool.size() < CALL_SESSIONS_POOL_SIZE) {
mCallSessionsPool.add(s);
}
}
- private void processCallEnded(CallSession s, int parcelRequestSize, int parcelReplySize) {
+ private void processCallEnded(CallSession s,
+ int parcelRequestSize, int parcelReplySize, int workSourceUid) {
// Non-negative time signals we need to record data for this call.
final boolean recordCall = s.cpuTimeStarted >= 0;
final long duration;
@@ -155,7 +159,6 @@
latencyDuration = 0;
}
final int callingUid = getCallingUid();
- final int workSourceUid = getWorkSourceUid();
synchronized (mLock) {
// This was already checked in #callStart but check again while synchronized.
@@ -356,14 +359,13 @@
}
/** Writes the collected statistics to the supplied {@link PrintWriter}.*/
- public void dump(PrintWriter pw, Map<Integer, String> appIdToPkgNameMap, boolean verbose) {
+ public void dump(PrintWriter pw, AppIdToPackageMap packageMap, boolean verbose) {
synchronized (mLock) {
- dumpLocked(pw, appIdToPkgNameMap, verbose);
+ dumpLocked(pw, packageMap, verbose);
}
}
- private void dumpLocked(PrintWriter pw, Map<Integer, String> appIdToPkgNameMap,
- boolean verbose) {
+ private void dumpLocked(PrintWriter pw, AppIdToPackageMap packageMap, boolean verbose) {
long totalCallsCount = 0;
long totalRecordedCallsCount = 0;
long totalCpuTime = 0;
@@ -397,9 +399,9 @@
for (ExportedCallStat e : exportedCallStats) {
sb.setLength(0);
sb.append(" ")
- .append(uidToString(e.callingUid, appIdToPkgNameMap))
+ .append(packageMap.mapUid(e.callingUid))
.append(',')
- .append(uidToString(e.workSourceUid, appIdToPkgNameMap))
+ .append(packageMap.mapUid(e.workSourceUid))
.append(',').append(e.className)
.append('#').append(e.methodName)
.append(',').append(e.screenInteractive)
@@ -420,7 +422,7 @@
final List<UidEntry> summaryEntries = verbose ? entries
: getHighestValues(entries, value -> value.cpuTimeMicros, 0.9);
for (UidEntry entry : summaryEntries) {
- String uidStr = uidToString(entry.workSourceUid, appIdToPkgNameMap);
+ String uidStr = packageMap.mapUid(entry.workSourceUid);
pw.println(String.format(" %10d %3.0f%% %8d %8d %s",
entry.cpuTimeMicros, 100d * entry.cpuTimeMicros / totalCpuTime,
entry.recordedCallCount, entry.callCount, uidStr));
@@ -448,13 +450,6 @@
}
}
- private static String uidToString(int uid, Map<Integer, String> pkgNameMap) {
- final int appId = UserHandle.getAppId(uid);
- final String pkgName = pkgNameMap == null ? null : pkgNameMap.get(appId);
- final String uidStr = UserHandle.formatUid(uid);
- return pkgName == null ? uidStr : pkgName + '/' + uidStr;
- }
-
protected long getThreadTimeMicro() {
return SystemClock.currentThreadTimeMicro();
}
@@ -463,10 +458,6 @@
return Binder.getCallingUid();
}
- protected int getWorkSourceUid() {
- return ThreadLocalWorkSource.getUid();
- }
-
protected long getElapsedRealtimeMicro() {
return SystemClock.elapsedRealtimeNanos() / 1000;
}
@@ -669,14 +660,16 @@
// Only create CallStat if it's a new entry, otherwise update existing instance.
if (mapCallStat == null) {
if (maxCallStatsReached) {
- mapCallStat = get(callingUid, OVERFLOW_BINDER, OVERFLOW_TRANSACTION_CODE,
- screenInteractive);
+ mapCallStat = get(OVERFLOW_DIRECT_CALLING_UID, OVERFLOW_BINDER,
+ OVERFLOW_TRANSACTION_CODE, OVERFLOW_SCREEN_INTERACTIVE);
if (mapCallStat != null) {
return mapCallStat;
}
+ callingUid = OVERFLOW_DIRECT_CALLING_UID;
binderClass = OVERFLOW_BINDER;
transactionCode = OVERFLOW_TRANSACTION_CODE;
+ screenInteractive = OVERFLOW_SCREEN_INTERACTIVE;
}
mapCallStat = new CallStat(callingUid, binderClass, transactionCode,
diff --git a/core/java/com/android/internal/os/BinderInternal.java b/core/java/com/android/internal/os/BinderInternal.java
index 0155067..5b69979 100644
--- a/core/java/com/android/internal/os/BinderInternal.java
+++ b/core/java/com/android/internal/os/BinderInternal.java
@@ -22,7 +22,6 @@
import android.os.IBinder;
import android.os.SystemClock;
import android.util.EventLog;
-import android.util.Log;
import android.util.SparseIntArray;
import com.android.internal.util.Preconditions;
@@ -86,6 +85,22 @@
boolean exceptionThrown;
}
+
+ /**
+ * Responsible for resolving a work source.
+ */
+ @FunctionalInterface
+ public interface WorkSourceProvider {
+ /**
+ * <p>This method is called in a critical path of the binder transaction.
+ * <p>The implementation should never execute a binder call since it is called during a
+ * binder transaction.
+ *
+ * @return the uid of the process to attribute the binder transaction to.
+ */
+ int resolveWorkSourceUid();
+ }
+
/**
* Allows to track various steps of an API call.
*/
@@ -95,14 +110,16 @@
*
* @return a CallSession to pass to the callEnded method.
*/
- CallSession callStarted(Binder binder, int code);
+ CallSession callStarted(Binder binder, int code, int workSourceUid);
/**
* Called when a binder call stops.
*
- * <li>This method will be called even when an exception is thrown.
+ * <li>This method will be called even when an exception is thrown by the binder stub
+ * implementation.
*/
- void callEnded(CallSession s, int parcelRequestSize, int parcelReplySize);
+ void callEnded(CallSession s, int parcelRequestSize, int parcelReplySize,
+ int workSourceUid);
/**
* Called if an exception is thrown while executing the binder transaction.
diff --git a/core/java/com/android/server/SystemConfig.java b/core/java/com/android/server/SystemConfig.java
index b97a9fa..b00e6fd 100644
--- a/core/java/com/android/server/SystemConfig.java
+++ b/core/java/com/android/server/SystemConfig.java
@@ -25,7 +25,6 @@
import android.os.Build;
import android.os.Environment;
import android.os.Process;
-import android.os.SystemProperties;
import android.os.storage.StorageManager;
import android.permission.PermissionManager.SplitPermissionInfo;
import android.text.TextUtils;
@@ -78,10 +77,23 @@
final ArrayList<SplitPermissionInfo> mSplitPermissions = new ArrayList<>();
+ public static final class SharedLibraryEntry {
+ public final String name;
+ public final String filename;
+ public final String[] dependencies;
+
+ SharedLibraryEntry(String name, String filename, String[] dependencies) {
+ this.name = name;
+ this.filename = filename;
+ this.dependencies = dependencies;
+ }
+ }
+
// These are the built-in shared libraries that were read from the
- // system configuration files. Keys are the library names; strings are the
- // paths to the libraries.
- final ArrayMap<String, String> mSharedLibraries = new ArrayMap<>();
+ // system configuration files. Keys are the library names; values are
+ // the individual entries that contain information such as filename
+ // and dependencies.
+ final ArrayMap<String, SharedLibraryEntry> mSharedLibraries = new ArrayMap<>();
// These are the features this devices supports that were read from the
// system configuration files.
@@ -200,7 +212,7 @@
return mSplitPermissions;
}
- public ArrayMap<String, String> getSharedLibraries() {
+ public ArrayMap<String, SharedLibraryEntry> getSharedLibraries() {
return mSharedLibraries;
}
@@ -497,6 +509,7 @@
} else if ("library".equals(name) && allowLibs) {
String lname = parser.getAttributeValue(null, "name");
String lfile = parser.getAttributeValue(null, "file");
+ String ldependency = parser.getAttributeValue(null, "dependency");
if (lname == null) {
Slog.w(TAG, "<library> without name in " + permFile + " at "
+ parser.getPositionDescription());
@@ -505,11 +518,12 @@
+ parser.getPositionDescription());
} else {
//Log.i(TAG, "Got library " + lname + " in " + lfile);
- mSharedLibraries.put(lname, lfile);
+ SharedLibraryEntry entry = new SharedLibraryEntry(lname, lfile,
+ ldependency == null ? new String[0] : ldependency.split(":"));
+ mSharedLibraries.put(lname, entry);
}
XmlUtils.skipCurrentTag(parser);
continue;
-
} else if ("feature".equals(name) && allowFeatures) {
String fname = parser.getAttributeValue(null, "name");
int fversion = XmlUtils.readIntAttribute(parser, "version", 0);
diff --git a/core/jni/android_hardware_SoundTrigger.cpp b/core/jni/android_hardware_SoundTrigger.cpp
index 98bc735..8a28034 100644
--- a/core/jni/android_hardware_SoundTrigger.cpp
+++ b/core/jni/android_hardware_SoundTrigger.cpp
@@ -225,7 +225,8 @@
gAudioFormatCstor,
audioFormatFromNative(event->audio_config.format),
event->audio_config.sample_rate,
- inChannelMaskFromNative(event->audio_config.channel_mask));
+ inChannelMaskFromNative(event->audio_config.channel_mask),
+ (jint)0 /* channelIndexMask */);
}
if (event->type == SOUND_MODEL_TYPE_KEYPHRASE) {
diff --git a/core/jni/android_media_AudioTrack.cpp b/core/jni/android_media_AudioTrack.cpp
index 7ebb2b2..516093e 100644
--- a/core/jni/android_media_AudioTrack.cpp
+++ b/core/jni/android_media_AudioTrack.cpp
@@ -479,6 +479,24 @@
}
// ----------------------------------------------------------------------------
+static jboolean
+android_media_AudioTrack_is_direct_output_supported(JNIEnv *env, jobject thiz,
+ jint encoding, jint sampleRate,
+ jint channelMask, jint channelIndexMask,
+ jint contentType, jint usage, jint flags) {
+ audio_config_base_t config = {};
+ audio_attributes_t attributes = {};
+ config.format = static_cast<audio_format_t>(audioFormatToNative(encoding));
+ config.sample_rate = static_cast<uint32_t>(sampleRate);
+ config.channel_mask = nativeChannelMaskFromJavaChannelMasks(channelMask, channelIndexMask);
+ attributes.content_type = static_cast<audio_content_type_t>(contentType);
+ attributes.usage = static_cast<audio_usage_t>(usage);
+ attributes.flags = static_cast<audio_flags_mask_t>(flags);
+ // ignore source and tags attributes as they don't affect querying whether output is supported
+ return AudioTrack::isDirectOutputSupported(config, attributes);
+}
+
+// ----------------------------------------------------------------------------
static void
android_media_AudioTrack_start(JNIEnv *env, jobject thiz)
{
@@ -1297,6 +1315,9 @@
// ----------------------------------------------------------------------------
static const JNINativeMethod gMethods[] = {
// name, signature, funcPtr
+ {"native_is_direct_output_supported",
+ "(IIIIIII)Z",
+ (void *)android_media_AudioTrack_is_direct_output_supported},
{"native_start", "()V", (void *)android_media_AudioTrack_start},
{"native_stop", "()V", (void *)android_media_AudioTrack_stop},
{"native_pause", "()V", (void *)android_media_AudioTrack_pause},
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 0286730..4aa88e7 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -276,15 +276,17 @@
}
}
- // We don't want core dumps, though, so set the soft limit on core dump size
- // to 0 without changing the hard limit.
- rlimit rl;
- if (getrlimit(RLIMIT_CORE, &rl) == -1) {
- ALOGE("getrlimit(RLIMIT_CORE) failed");
- } else {
- rl.rlim_cur = 0;
- if (setrlimit(RLIMIT_CORE, &rl) == -1) {
- ALOGE("setrlimit(RLIMIT_CORE) failed");
+ // Set the core dump size to zero unless wanted (see also coredump_setup in build/envsetup.sh).
+ if (!GetBoolProperty("persist.zygote.core_dump", false)) {
+ // Set the soft limit on core dump size to 0 without changing the hard limit.
+ rlimit rl;
+ if (getrlimit(RLIMIT_CORE, &rl) == -1) {
+ ALOGE("getrlimit(RLIMIT_CORE) failed");
+ } else {
+ rl.rlim_cur = 0;
+ if (setrlimit(RLIMIT_CORE, &rl) == -1) {
+ ALOGE("setrlimit(RLIMIT_CORE) failed");
+ }
}
}
}
diff --git a/core/proto/android/service/usb.proto b/core/proto/android/service/usb.proto
index ed040f4..f7dcee2 100644
--- a/core/proto/android/service/usb.proto
+++ b/core/proto/android/service/usb.proto
@@ -206,6 +206,8 @@
optional bool can_change_mode = 3;
optional bool can_change_power_role = 4;
optional bool can_change_data_role = 5;
+ optional int64 connected_at_millis = 6;
+ optional int64 last_connect_duration_millis = 7;
}
message UsbPortProto {
diff --git a/core/proto/android/stats/devicepolicy/device_policy_enums.proto b/core/proto/android/stats/devicepolicy/device_policy_enums.proto
index 5726d9a..c7a6b68 100644
--- a/core/proto/android/stats/devicepolicy/device_policy_enums.proto
+++ b/core/proto/android/stats/devicepolicy/device_policy_enums.proto
@@ -51,7 +51,7 @@
SET_ALWAYS_ON_VPN_PACKAGE = 26;
SET_PERMITTED_INPUT_METHODS = 27;
SET_PERMITTED_ACCESSIBILITY_SERVICES = 28;
- SET_SCREEN_CAPTURE_DISABLE = 29;
+ SET_SCREEN_CAPTURE_DISABLED = 29;
SET_CAMERA_DISABLED = 30;
QUERY_SUMMARY_FOR_USER = 31;
QUERY_SUMMARY = 32;
@@ -64,7 +64,7 @@
SET_ORGANIZATION_COLOR = 39;
SET_PROFILE_NAME = 40;
SET_USER_ICON = 41;
- SET_DEVICE_OWNER_LOCKSCREEN_INFO = 42;
+ SET_DEVICE_OWNER_LOCK_SCREEN_INFO = 42;
SET_SHORT_SUPPORT_MESSAGE = 43;
SET_LONG_SUPPORT_MESSAGE = 44;
SET_CROSS_PROFILE_CONTACTS_SEARCH_DISABLED = 45;
@@ -139,4 +139,6 @@
SET_GLOBAL_SETTING = 111;
PM_IS_INSTALLER_DEVICE_OWNER_OR_AFFILIATED_PROFILE_OWNER = 112;
PM_UNINSTALL = 113;
+ WIFI_SERVICE_ADD_NETWORK_SUGGESTIONS = 114;
+ WIFI_SERVICE_ADD_OR_UPDATE_NETWORK = 115;
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 4dedd49..9c55c75 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -897,7 +897,7 @@
android:protectionLevel="dangerous"
android:usageInfoRequired="true" />
- <!-- @hide @SystemApi
+ <!-- @hide @SystemApi @TestApi
Allows an application to modify OBB files visible to other apps. -->
<permission android:name="android.permission.WRITE_OBB"
android:protectionLevel="signature|privileged" />
@@ -2087,10 +2087,9 @@
<p>This permission should <em>only</em> be requested by the platform
document management app. This permission cannot be granted to
third-party apps.
- <p>Protection level: signature
-->
<permission android:name="android.permission.MANAGE_DOCUMENTS"
- android:protectionLevel="signature" />
+ android:protectionLevel="signature|documenter" />
<!-- @hide Allows an application to cache content.
<p>Not for use by third-party applications.
@@ -2171,6 +2170,13 @@
<permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"
android:protectionLevel="signature|installer" />
+ <!-- @SystemApi Allows an application to start an activity within its managed profile from
+ the personal profile.
+ This permission is not available to third party applications.
+ @hide -->
+ <permission android:name="android.permission.INTERACT_ACROSS_PROFILES"
+ android:protectionLevel="signature|privileged" />
+
<!-- @SystemApi @hide Allows an application to call APIs that allow it to query and manage
users on the device. This permission is not available to
third party applications. -->
@@ -2206,9 +2212,9 @@
android:description="@string/permdesc_reorderTasks"
android:protectionLevel="normal" />
- <!-- @hide Allows an application to change to remove/kill tasks -->
+ <!-- @SystemApi @TestApi @hide Allows an application to change to remove/kill tasks -->
<permission android:name="android.permission.REMOVE_TASKS"
- android:protectionLevel="signature" />
+ android:protectionLevel="signature|documenter" />
<!-- @SystemApi @TestApi @hide Allows an application to create/manage/remove stacks -->
<permission android:name="android.permission.MANAGE_ACTIVITY_STACKS"
@@ -3344,6 +3350,11 @@
<permission android:name="android.permission.MANAGE_ROLE_HOLDERS"
android:protectionLevel="signature|installer" />
+ <!-- @SystemApi Allows an application to observe role holder changes.
+ @hide -->
+ <permission android:name="android.permission.OBSERVE_ROLE_HOLDERS"
+ android:protectionLevel="signature|installer" />
+
<!-- @SystemApi Allows an application to use SurfaceFlinger's low level features.
<p>Not for use by third-party applications.
@hide
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index f8004ea..ab4bd05 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -271,6 +271,9 @@
<!-- Additional flag from base permission type: this permission will be granted to the
wellbeing app, as defined by the OEM. -->
<flag name="wellbeing" value="0x20000" />
+ <!-- Additional flag from base permission type: this permission can be automatically
+ granted to the document manager -->
+ <flag name="documenter" value="0x40000" />
</attr>
<!-- Flags indicating more context for a permission group. -->
diff --git a/core/tests/coretests/src/android/os/BinderWorkSourceService.java b/core/tests/coretests/src/android/os/BinderWorkSourceService.java
index ac8d7ab..3bca5fb 100644
--- a/core/tests/coretests/src/android/os/BinderWorkSourceService.java
+++ b/core/tests/coretests/src/android/os/BinderWorkSourceService.java
@@ -25,9 +25,25 @@
public class BinderWorkSourceService extends Service {
private final IBinderWorkSourceService.Stub mBinder =
new IBinderWorkSourceService.Stub() {
+ public int getBinderCallingUid() {
+ return Binder.getCallingUid();
+ }
+
public int getIncomingWorkSourceUid() {
return Binder.getCallingWorkSourceUid();
}
+
+ public int getThreadLocalWorkSourceUid() {
+ return ThreadLocalWorkSource.getUid();
+ }
+
+ public void setWorkSourceProvider(int uid) {
+ Binder.setWorkSourceProvider(() -> uid);
+ }
+
+ public void clearWorkSourceProvider() {
+ Binder.setWorkSourceProvider(Binder::getCallingUid);
+ }
};
public IBinder onBind(Intent intent) {
diff --git a/core/tests/coretests/src/android/os/BinderWorkSourceTest.java b/core/tests/coretests/src/android/os/BinderWorkSourceTest.java
index ec17803..d1dbd3c 100644
--- a/core/tests/coretests/src/android/os/BinderWorkSourceTest.java
+++ b/core/tests/coretests/src/android/os/BinderWorkSourceTest.java
@@ -163,4 +163,24 @@
// Initial work source restored.
assertEquals(UID, Binder.getCallingWorkSourceUid());
}
+
+ @Test
+ public void workSourceProvider_default() throws Exception {
+ Binder.clearCallingWorkSource();
+ mService.clearWorkSourceProvider();
+ assertEquals(Process.myUid(), mService.getThreadLocalWorkSourceUid());
+ }
+
+ @Test
+ public void workSourceProvider_customProvider() throws Exception {
+ Binder.clearCallingWorkSource();
+ mService.clearWorkSourceProvider();
+ // Calling uid should not be used.
+ mService.setWorkSourceProvider(SECOND_UID);
+ try {
+ assertEquals(SECOND_UID, mService.getThreadLocalWorkSourceUid());
+ } finally {
+ mService.clearWorkSourceProvider();
+ }
+ }
}
diff --git a/core/tests/coretests/src/android/os/IBinderWorkSourceService.aidl b/core/tests/coretests/src/android/os/IBinderWorkSourceService.aidl
index 05d4e82..9322400 100644
--- a/core/tests/coretests/src/android/os/IBinderWorkSourceService.aidl
+++ b/core/tests/coretests/src/android/os/IBinderWorkSourceService.aidl
@@ -18,4 +18,8 @@
interface IBinderWorkSourceService {
int getIncomingWorkSourceUid();
+ int getBinderCallingUid();
+ int getThreadLocalWorkSourceUid();
+ void setWorkSourceProvider(int uid);
+ void clearWorkSourceProvider();
}
diff --git a/core/tests/coretests/src/android/os/PowerManagerTest.java b/core/tests/coretests/src/android/os/PowerManagerTest.java
index da17b56..a828f44 100644
--- a/core/tests/coretests/src/android/os/PowerManagerTest.java
+++ b/core/tests/coretests/src/android/os/PowerManagerTest.java
@@ -16,13 +16,32 @@
package android.os;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
+import org.junit.After;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
public class PowerManagerTest extends AndroidTestCase {
private PowerManager mPm;
+ private UiDevice mUiDevice;
+ private Executor mExec = Executors.newSingleThreadExecutor();
+ @Mock
+ private PowerManager.ThermalStatusCallback mCallback;
+ private static final long CALLBACK_TIMEOUT_MILLI_SEC = 5000;
/**
* Setup any common data for the upcoming tests.
@@ -30,7 +49,18 @@
@Override
public void setUp() throws Exception {
super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
mPm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ mUiDevice.executeShellCommand("cmd thermalservice override-status 0");
+ }
+
+ /**
+ * Reset data for the upcoming tests.
+ */
+ @After
+ public void tearDown() throws Exception {
+ mUiDevice.executeShellCommand("cmd thermalservice reset");
}
/**
@@ -137,4 +167,35 @@
// TODO: Threaded test (needs handler) to make sure timed wakelocks work too
}
+
+ @Test
+ public void testGetThermalStatus() throws Exception {
+ int status = 0;
+ assertEquals(status, mPm.getCurrentThermalStatus());
+ status = 3;
+ mUiDevice.executeShellCommand("cmd thermalservice override-status "
+ + Integer.toString(status));
+ assertEquals(status, mPm.getCurrentThermalStatus());
+ }
+
+ @Test
+ public void testThermalStatusCallback() throws Exception {
+ mPm.registerThermalStatusCallback(mCallback, mExec);
+ verify(mCallback, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
+ .times(1)).onStatusChange(0);
+ reset(mCallback);
+ int status = 3;
+ mUiDevice.executeShellCommand("cmd thermalservice override-status "
+ + Integer.toString(status));
+ verify(mCallback, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
+ .times(1)).onStatusChange(status);
+ reset(mCallback);
+ mPm.unregisterThermalStatusCallback(mCallback);
+ status = 2;
+ mUiDevice.executeShellCommand("cmd thermalservice override-status "
+ + Integer.toString(status));
+ verify(mCallback, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
+ .times(0)).onStatusChange(status);
+
+ }
}
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 49a7555..3a37fb6 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -415,6 +415,7 @@
Settings.Global.SHOW_NOTIFICATION_CHANNEL_WARNINGS,
Settings.Global.SHOW_RESTART_IN_CRASH_DIALOG,
Settings.Global.SHOW_TEMPERATURE_WARNING,
+ Settings.Global.SIGNED_CONFIG_VERSION,
Settings.Global.SMART_SELECTION_UPDATE_CONTENT_URL,
Settings.Global.SMART_SELECTION_UPDATE_METADATA_URL,
Settings.Global.SMS_ACCESS_RESTRICTION_ENABLED,
diff --git a/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java b/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java
index 8d6fbd5..61ab152 100644
--- a/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java
+++ b/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java
@@ -28,7 +28,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-// TODO(b/73862682): Add tests for RecoveryCertPath
+import java.security.cert.CertPath;
+
@RunWith(AndroidJUnit4.class)
@SmallTest
public class KeyChainSnapshotTest {
@@ -43,35 +44,41 @@
private static final int USER_SECRET_TYPE = KeyChainProtectionParams.TYPE_LOCKSCREEN;
private static final String KEY_ALIAS = "steph";
private static final byte[] KEY_MATERIAL = new byte[] { 3, 5, 7, 9, 1 };
+ private static final CertPath CERT_PATH = TestData.getThmCertPath();
@Test
- public void build_setsCounterId() {
+ public void build_setsCounterId() throws Exception {
assertEquals(COUNTER_ID, createKeyChainSnapshot().getCounterId());
}
@Test
- public void build_setsSnapshotVersion() {
+ public void build_setsSnapshotVersion() throws Exception {
assertEquals(SNAPSHOT_VERSION, createKeyChainSnapshot().getSnapshotVersion());
}
@Test
- public void build_setsMaxAttempts() {
+ public void build_setsMaxAttempts() throws Exception {
assertEquals(MAX_ATTEMPTS, createKeyChainSnapshot().getMaxAttempts());
}
@Test
- public void build_setsServerParams() {
+ public void build_setsServerParams() throws Exception {
assertArrayEquals(SERVER_PARAMS, createKeyChainSnapshot().getServerParams());
}
@Test
- public void build_setsRecoveryKeyBlob() {
+ public void build_setsRecoveryKeyBlob() throws Exception {
assertArrayEquals(RECOVERY_KEY_BLOB,
createKeyChainSnapshot().getEncryptedRecoveryKeyBlob());
}
@Test
- public void build_setsKeyChainProtectionParams() {
+ public void build_setsCertPath() throws Exception {
+ assertEquals(CERT_PATH, createKeyChainSnapshot().getTrustedHardwareCertPath());
+ }
+
+ @Test
+ public void build_setsKeyChainProtectionParams() throws Exception {
KeyChainSnapshot snapshot = createKeyChainSnapshot();
assertEquals(1, snapshot.getKeyChainProtectionParams().size());
@@ -85,7 +92,7 @@
}
@Test
- public void build_setsWrappedApplicationKeys() {
+ public void build_setsWrappedApplicationKeys() throws Exception {
KeyChainSnapshot snapshot = createKeyChainSnapshot();
assertEquals(1, snapshot.getWrappedApplicationKeys().size());
@@ -95,42 +102,49 @@
}
@Test
- public void writeToParcel_writesCounterId() {
+ public void writeToParcel_writesCounterId() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertEquals(COUNTER_ID, snapshot.getCounterId());
}
@Test
- public void writeToParcel_writesSnapshotVersion() {
+ public void writeToParcel_writesSnapshotVersion() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertEquals(SNAPSHOT_VERSION, snapshot.getSnapshotVersion());
}
@Test
- public void writeToParcel_writesMaxAttempts() {
+ public void writeToParcel_writesMaxAttempts() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertEquals(MAX_ATTEMPTS, snapshot.getMaxAttempts());
}
@Test
- public void writeToParcel_writesServerParams() {
+ public void writeToParcel_writesServerParams() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertArrayEquals(SERVER_PARAMS, snapshot.getServerParams());
}
@Test
- public void writeToParcel_writesKeyRecoveryBlob() {
+ public void writeToParcel_writesKeyRecoveryBlob() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertArrayEquals(RECOVERY_KEY_BLOB, snapshot.getEncryptedRecoveryKeyBlob());
}
@Test
- public void writeToParcel_writesKeyChainProtectionParams() {
+ public void writeToParcel_writesCertPath() throws Exception {
+ KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
+
+ assertEquals(CERT_PATH, snapshot.getTrustedHardwareCertPath());
+ }
+
+ @Test
+ public void writeToParcel_writesKeyChainProtectionParams() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertEquals(1, snapshot.getKeyChainProtectionParams().size());
@@ -144,7 +158,7 @@
}
@Test
- public void writeToParcel_writesWrappedApplicationKeys() {
+ public void writeToParcel_writesWrappedApplicationKeys() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertEquals(1, snapshot.getWrappedApplicationKeys().size());
@@ -153,7 +167,7 @@
assertArrayEquals(KEY_MATERIAL, wrappedApplicationKey.getEncryptedKeyMaterial());
}
- private static KeyChainSnapshot createKeyChainSnapshot() {
+ private static KeyChainSnapshot createKeyChainSnapshot() throws Exception {
return new KeyChainSnapshot.Builder()
.setCounterId(COUNTER_ID)
.setSnapshotVersion(SNAPSHOT_VERSION)
@@ -162,6 +176,7 @@
.setEncryptedRecoveryKeyBlob(RECOVERY_KEY_BLOB)
.setKeyChainProtectionParams(Lists.newArrayList(createKeyChainProtectionParams()))
.setWrappedApplicationKeys(Lists.newArrayList(createWrappedApplicationKey()))
+ .setTrustedHardwareCertPath(CERT_PATH)
.build();
}
diff --git a/core/tests/coretests/src/android/security/keystore/recovery/RecoveryCertPathTest.java b/core/tests/coretests/src/android/security/keystore/recovery/RecoveryCertPathTest.java
new file mode 100644
index 0000000..dd8cd8d
--- /dev/null
+++ b/core/tests/coretests/src/android/security/keystore/recovery/RecoveryCertPathTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.security.keystore.recovery;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.cert.CertificateException;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RecoveryCertPathTest {
+
+ @Test
+ public void createRecoveryCertPath_getCertPath_succeeds() throws Exception {
+ RecoveryCertPath recoveryCertPath = RecoveryCertPath.createRecoveryCertPath(
+ TestData.getThmCertPath());
+ assertEquals(TestData.getThmCertPath(), recoveryCertPath.getCertPath());
+ }
+
+ @Test
+ public void getCertPath_throwsIfCannnotDecode() {
+ Parcel parcel = Parcel.obtain();
+ parcel.writeByteArray(new byte[]{0, 1, 2, 3});
+ parcel.setDataPosition(0);
+ RecoveryCertPath recoveryCertPath = RecoveryCertPath.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ try {
+ recoveryCertPath.getCertPath();
+ fail("Did not throw when attempting to decode invalid cert path");
+ } catch (CertificateException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void writeToParcel_writesCertPath() throws Exception {
+ RecoveryCertPath recoveryCertPath =
+ writeToThenReadFromParcel(
+ RecoveryCertPath.createRecoveryCertPath(TestData.getThmCertPath()));
+ assertEquals(TestData.getThmCertPath(), recoveryCertPath.getCertPath());
+ }
+
+ private RecoveryCertPath writeToThenReadFromParcel(RecoveryCertPath recoveryCertPath) {
+ Parcel parcel = Parcel.obtain();
+ recoveryCertPath.writeToParcel(parcel, /*flags=*/ 0);
+ parcel.setDataPosition(0);
+ RecoveryCertPath fromParcel = RecoveryCertPath.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ return fromParcel;
+ }
+}
diff --git a/core/tests/coretests/src/android/security/keystore/recovery/TestData.java b/core/tests/coretests/src/android/security/keystore/recovery/TestData.java
new file mode 100644
index 0000000..829a92a
--- /dev/null
+++ b/core/tests/coretests/src/android/security/keystore/recovery/TestData.java
@@ -0,0 +1,88 @@
+/*
+ * 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.security.keystore.recovery;
+
+import java.io.ByteArrayInputStream;
+import java.security.cert.CertPath;
+import java.security.cert.CertificateFactory;
+import java.util.Base64;
+
+/** This class provides data for testing purposes. */
+class TestData {
+
+ private static final String THM_CERT_PATH_BASE64 = ""
+ + "MIIIXTCCBRowggMCoAMCAQICEB35ZwzVpI9ssXg9SAehnU0wDQYJKoZIhvcNAQEL"
+ + "BQAwMTEvMC0GA1UEAxMmR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIFJv"
+ + "b3QgQ0EwHhcNMTgwNTA3MTg1ODEwWhcNMjgwNTA4MTg1ODEwWjA5MTcwNQYDVQQD"
+ + "Ey5Hb29nbGUgQ2xvdWQgS2V5IFZhdWx0IFNlcnZpY2UgSW50ZXJtZWRpYXRlIENB"
+ + "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA73TrvH3j6zEimpcc32tx"
+ + "2iupWwfyzdE5l4Ejc5EBYzx0aZH6b/KDuutwustk0IoyjlGySMBz/21YgWejIm+n"
+ + "duAlpk7WY5kYHp0XWtzdmxZknmWTqugPeNZeiKEjoDmpyIbY6N+f13hQ2RVh+WDT"
+ + "EowQ/i04WBL75chshlIG+3A42g5Qr7DZEKdT9oJQqkntzj0cGyJ5X8BwjeTiJrvY"
+ + "k2Kn/0555/Kpp65G3Rf29VPPU3i67kthAT3SavLBpH03S4WZ+QlfrAiGQziydtz9"
+ + "t7mSk1xefjax5ZWAuJAfCbKfI3VWAcaUr4P57BzmDcSi0jgs1aM3t2BrPfAMRxWv"
+ + "35yDZnrC+HipzkjyDGBfHmFgoglyhc9e/Kj3mSusO0Rq1wguVXKs2hKXRoaGJuHt"
+ + "e3YIwTC1pLznqvolhD1nPoXf8rMzgHRzlc9H8iXsgB1p7975nh5WCPrMDX2eAmYd"
+ + "a0xTMccTeBzIM2ohxQsxlh5rsjXVNU3ihbWkHquzIiwFcAtldP3dMksj0dn/DnYD"
+ + "yokjEgU/z2I216E93x9hmKkEk6Pp7o8t/z6lwMT9FJIuzp7NREnWCSi+e5s2E7FD"
+ + "j6S7xY2zEIUHrmwuuJc0jzJnwdZ+0myinaTmBDvBXR5cU1cmEAZoheCAoRv9Z/6o"
+ + "ASczLF0C4uuVfA5GXcAm14cCAwEAAaMmMCQwDgYDVR0PAQH/BAQDAgGGMBIGA1Ud"
+ + "EwEB/wQIMAYBAf8CAQEwDQYJKoZIhvcNAQELBQADggIBAEPht79yQm8woQbPB1Bs"
+ + "eotkzJtTWTO9fnIWwNiRfQ3vJFXf69ghE77wUS13Ez3FlgNPj0Qxmg5ouE0d2yYV"
+ + "4AUrXnEGZELcyN2XHRXyNK0zXgnr3x6eZyY7QfgGKJgkyja5TS6ZPWGyaLKhClS0"
+ + "AYZSzWJtz0+AkGCdTbmyy7ShdXJ+GfnmssbndZA62VhcjeQmHsDq7V3PKAsp4/B9"
+ + "PzcnTrgkUFNnP1F1pr7JpUUX3xyRFy6gjIrUx1fcOFRxFYPWGLLMZ6P41rafm+M/"
+ + "CbBNr5CY7NrZjr34jLqWycfYes49o9OK44X/wPrxj0Sjg+VrW21+AJ9vrM7DS5hE"
+ + "QX1lDbDtQGkk3N1vgCTo6xt9LXsEu4xUT5bk7YAfpJqM0ltDFPwYAGCbjSkVT/M5"
+ + "JVZkKiUW668Us67x8yZc/5bxbvTA+5xrYhak/VYIBY6qub4J+bKwadw6uBgxnq4P"
+ + "hwgwjfaoJy9YAXCswjCtaE9GwkVmRnJE9vFjJ33IGf37hFTYEHBFy4FomVmQwRFZ"
+ + "TIe7tkKDq9i18F7lzBPJPO6wEG8bxi4csatrjcVHR9erpY5u6ebtkKG8qsan9qzh"
+ + "iWAgSytiT++HejZeoQ+RRgQWjupjdDo5/0oSdQqvaN8Ah6C2J+ecCZ12Lu0FwF+t"
+ + "t9Ie3pF6W8TzxzuMdFWq+afvMIIDOzCCASOgAwIBAgIRAOTj/iNQb6/Qit7zAW9n"
+ + "cL0wDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBW"
+ + "YXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTAeFw0xODA1MDcyMjE4MTFaFw0y"
+ + "MzA1MDgyMjE4MTFaMDIxMDAuBgNVBAMTJ0dvb2dsZSBDbG91ZCBLZXkgVmF1bHQg"
+ + "U2VydmljZSBFbmRwb2ludDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI4MEUp5"
+ + "IHwATNfpBuJYIUX6JMsHZt798YO0JlWYy6nVVa1lxf9c+xxONJh+T5aio370RlIE"
+ + "uiq5R7vCHt0VGsCjEDAOMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB"
+ + "AGf6QU58lU+gGzy8hnp0suR/ixC++CExtf39pDHkdfU/e3ui4ROR+pjQ5F7okDFW"
+ + "eKSCNcJZ7tyXMJ9g7/I0qVY8Bj/gRnlVokdl/wD5PiL9GIzqWfnHNe3T+xrAAAgO"
+ + "D0bEmjgwNYmekfUIYQczd04d7ZMGnmAkpVH/0O2mf9q5x9fMlbKuAygUqQ/gmnlg"
+ + "xKfl9DSRWi4oMBOqlKlCRP1XAh3anu92+M/EhsFbyc07CWZY0SByX5M/cHVMLhUX"
+ + "jZHvcYLyOmJWJmXznidgyNeIR6t9yDB55iCt7WSn3qMY+9vA9ELzt8jYpBNaKc0G"
+ + "bWQkRzYWegkf4kMis98eQ3SnAKbRz6669nmuAdxKs9/LK6BlFOFw1xvsTRQ96dBa"
+ + "oiX2XGhou+Im0Td/AMs0Aigz2N+Ujq/yW//35GZQfdGGIYtFbkcltStygjIJyAM1"
+ + "pBhyBBkJhOhRpO4fXh98aq8H5J7R9i5A9WpnDstAxPxcNCDWn0O/WxhPvVZkFTpi"
+ + "NXh9dnlJ/kZe+j+z5ZMaxW435drLPx2AQKjXA9GgGrFPltTUyGycmEGtuxLvSnm/"
+ + "zPlmk5FUk7x2wEr0+bZ3cx0JHHgAtgXpe0jkDi8Bw8O3X7mUOjxVhYU6auiYJezW"
+ + "9LGmweaKwYvS04UCWOReolUVexob9LI/VX1JrrwD3s7k";
+
+ static CertPath getThmCertPath() {
+ try {
+ return decodeCertPath(THM_CERT_PATH_BASE64);
+ } catch (Exception e) {
+ // Should never happen
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static CertPath decodeCertPath(String base64CertPath) throws Exception {
+ byte[] certPathBytes = Base64.getDecoder().decode(base64CertPath);
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ return certFactory.generateCertPath(new ByteArrayInputStream(certPathBytes), "PkiPath");
+ }
+}
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index ed80cd7..2ad6028 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -19,6 +19,7 @@
import static android.view.InsetsState.TYPE_TOP_BAR;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
+import static org.mockito.Mockito.mock;
import android.platform.test.annotations.Presubmit;
import android.support.test.filters.FlakyTest;
@@ -33,7 +34,7 @@
@RunWith(AndroidJUnit4.class)
public class InsetsControllerTest {
- private InsetsController mController = new InsetsController();
+ private InsetsController mController = new InsetsController(mock(ViewRootImpl.class));
private SurfaceSession mSession = new SurfaceSession();
private SurfaceControl mLeash;
diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
index 5a20ba2..6d0f984 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
@@ -53,7 +53,7 @@
.setName("testSurface")
.build();
mConsumer = new InsetsSourceConsumer(TYPE_TOP_BAR, new InsetsState(),
- () -> mMockTransaction);
+ () -> mMockTransaction, mMockController);
mConsumer.setControl(new InsetsSourceControl(TYPE_TOP_BAR, mLeash));
}
diff --git a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
index e261819..97f02cb 100644
--- a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
@@ -57,9 +57,9 @@
bcs.setSamplingInterval(5);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
assertEquals(1, uidEntries.size());
@@ -73,17 +73,17 @@
assertEquals(1, callStatsList.get(0).transactionCode);
// CPU usage is sampled, should not be tracked here.
- callSession = bcs.callStarted(binder, 1);
+ callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 20;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
assertEquals(2, uidEntry.callCount);
assertEquals(1, uidEntry.recordedCallCount);
assertEquals(10, uidEntry.cpuTimeMicros);
assertEquals(1, callStatsList.size());
- callSession = bcs.callStarted(binder, 2);
+ callSession = bcs.callStarted(binder, 2, WORKSOURCE_UID);
bcs.time += 50;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
uidEntry = bcs.getUidEntries().get(WORKSOURCE_UID);
assertEquals(3, uidEntry.callCount);
assertEquals(1, uidEntry.recordedCallCount);
@@ -98,9 +98,9 @@
bcs.setDetailedTracking(true);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
assertEquals(1, uidEntries.size());
@@ -116,9 +116,9 @@
assertEquals(binder.getClass(), callStatsList.get(0).binderClass);
assertEquals(1, callStatsList.get(0).transactionCode);
- callSession = bcs.callStarted(binder, 1);
+ callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 20;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
uidEntry = bcs.getUidEntries().get(WORKSOURCE_UID);
assertEquals(2, uidEntry.callCount);
@@ -126,9 +126,9 @@
callStatsList = new ArrayList(uidEntry.getCallStatsList());
assertEquals(1, callStatsList.size());
- callSession = bcs.callStarted(binder, 2);
+ callSession = bcs.callStarted(binder, 2, WORKSOURCE_UID);
bcs.time += 50;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
uidEntry = bcs.getUidEntries().get(WORKSOURCE_UID);
assertEquals(3, uidEntry.callCount);
@@ -142,7 +142,7 @@
public void testEnableInBetweenCall() {
TestBinderCallsStats bcs = new TestBinderCallsStats();
Binder binder = new Binder();
- bcs.callEnded(null, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(null, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
assertEquals(0, uidEntries.size());
@@ -153,7 +153,7 @@
TestBinderCallsStats bcs = new TestBinderCallsStats();
Binder binder = new Binder();
bcs.callThrewException(null, new IllegalStateException());
- bcs.callEnded(null, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(null, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
assertEquals(0, uidEntries.size());
@@ -166,17 +166,17 @@
bcs.setSamplingInterval(2);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
- callSession = bcs.callStarted(binder, 1);
+ callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 1000; // shoud be ignored.
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
- callSession = bcs.callStarted(binder, 1);
+ callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 50;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
assertEquals(1, uidEntries.size());
@@ -203,13 +203,13 @@
bcs.setSamplingInterval(2);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
- callSession = bcs.callStarted(binder, 2 /* another method */);
+ callSession = bcs.callStarted(binder, 2 /* another method */, WORKSOURCE_UID);
bcs.time += 1000; // shoud be ignored.
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
@@ -246,9 +246,9 @@
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
Binder binder = new BinderWithGetTransactionName();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
List<BinderCallsStats.ExportedCallStat> callStatsList =
bcs.getExportedCallStats();
@@ -261,18 +261,18 @@
bcs.setDetailedTracking(true);
Binder binder = new AnotherBinderWithGetTransactionName();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
Binder binder2 = new BinderWithGetTransactionName();
- callSession = bcs.callStarted(binder2, 1);
+ callSession = bcs.callStarted(binder2, 1, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
- callSession = bcs.callStarted(binder, 2);
+ callSession = bcs.callStarted(binder, 2, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
List<BinderCallsStats.ExportedCallStat> callStatsList =
bcs.getExportedCallStats();
@@ -292,9 +292,9 @@
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
List<BinderCallsStats.ExportedCallStat> callStatsList =
bcs.getExportedCallStats();
@@ -306,9 +306,9 @@
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
List<BinderCallsStats.CallStat> callStatsList =
new ArrayList(bcs.getUidEntries().get(WORKSOURCE_UID).getCallStatsList());
@@ -322,13 +322,13 @@
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 50;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
- callSession = bcs.callStarted(binder, 1);
+ callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
List<BinderCallsStats.CallStat> callStatsList =
new ArrayList(bcs.getUidEntries().get(WORKSOURCE_UID).getCallStatsList());
@@ -341,13 +341,13 @@
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.elapsedTime += 5;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
- callSession = bcs.callStarted(binder, 1);
+ callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.elapsedTime += 1;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
List<BinderCallsStats.CallStat> callStatsList =
new ArrayList(bcs.getUidEntries().get(WORKSOURCE_UID).getCallStatsList());
@@ -368,17 +368,17 @@
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.callThrewException(callSession, new IllegalStateException());
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
- callSession = bcs.callStarted(binder, 1);
+ callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.callThrewException(callSession, new IllegalStateException());
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
- callSession = bcs.callStarted(binder, 1);
+ callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.callThrewException(callSession, new RuntimeException());
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
ArrayMap<String, Integer> expected = new ArrayMap<>();
expected.put("java.lang.IllegalStateException", 2);
@@ -391,9 +391,9 @@
TestBinderCallsStats bcs = new TestBinderCallsStats(null);
bcs.setDetailedTracking(true);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
bcs.setDeviceState(mDeviceState.getReadonlyClient());
@@ -408,8 +408,8 @@
mDeviceState.setCharging(true);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
assertEquals(0, bcs.getUidEntries().size());
}
@@ -420,8 +420,8 @@
bcs.setDetailedTracking(true);
mDeviceState.setScreenInteractive(false);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
assertEquals(1, uidEntries.size());
@@ -437,8 +437,8 @@
bcs.setDetailedTracking(true);
mDeviceState.setScreenInteractive(true);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
assertEquals(1, uidEntries.size());
@@ -455,8 +455,8 @@
mDeviceState.setCharging(true);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
assertEquals(0, bcs.getExportedCallStats().size());
}
@@ -468,8 +468,8 @@
mDeviceState.setCharging(false);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
assertEquals(1, bcs.getExportedCallStats().size());
}
@@ -479,12 +479,12 @@
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.callThrewException(callSession, new IllegalStateException());
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
PrintWriter pw = new PrintWriter(new StringWriter());
- bcs.dump(pw, new HashMap<>(), true);
+ bcs.dump(pw, new AppIdToPackageMap(new HashMap<>()), true);
}
@Test
@@ -492,8 +492,8 @@
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(false);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
assertEquals(0, bcs.getExportedCallStats().size());
}
@@ -504,10 +504,10 @@
bcs.setDetailedTracking(true);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 10;
bcs.elapsedTime += 20;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
assertEquals(1, bcs.getExportedCallStats().size());
BinderCallsStats.ExportedCallStat stat = bcs.getExportedCallStats().get(0);
@@ -549,15 +549,15 @@
bcs.setMaxBinderCallStats(2);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
- callSession = bcs.callStarted(binder, 1);
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
- callSession = bcs.callStarted(binder, 1);
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
BinderCallsStats.UidEntry uidEntry = bcs.getUidEntries().get(WORKSOURCE_UID);
List<BinderCallsStats.CallStat> callStatsList = new ArrayList(uidEntry.getCallStatsList());
@@ -574,12 +574,15 @@
bcs.setMaxBinderCallStats(1);
Binder binder = new Binder();
- CallSession callSession = bcs.callStarted(binder, 1);
- bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
- callSession = bcs.callStarted(binder, 2);
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+ callSession = bcs.callStarted(binder, 2, WORKSOURCE_UID);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
+
+ // Should use the same overflow entry.
+ callSession = bcs.callStarted(binder, 3, WORKSOURCE_UID);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
List<BinderCallsStats.ExportedCallStat> callStatsList = bcs.getExportedCallStats();
assertEquals(2, callStatsList.size());
@@ -590,11 +593,46 @@
assertEquals(CALLING_UID, callStats.callingUid);
callStats = callStatsList.get(1);
- assertEquals(1, callStats.callCount);
+ assertEquals(2, callStats.callCount);
assertEquals("-1", callStats.methodName);
assertEquals("com.android.internal.os.BinderCallsStats$OverflowBinder",
callStats.className);
- assertEquals(CALLING_UID, callStats.callingUid);
+ assertEquals(false , callStats.screenInteractive);
+ assertEquals(-1 , callStats.callingUid);
+ }
+
+ @Test
+ public void testOverflow_oneOverflowEntryPerUid() {
+ TestBinderCallsStats bcs = new TestBinderCallsStats();
+ bcs.setDetailedTracking(true);
+ bcs.setSamplingInterval(1);
+ bcs.setMaxBinderCallStats(1);
+
+ Binder binder = new Binder();
+ CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID);
+
+ callSession = bcs.callStarted(binder, 2, WORKSOURCE_UID + 1);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID + 1);
+
+ // Different uids have different overflow entries.
+ callSession = bcs.callStarted(binder, 2, WORKSOURCE_UID + 2);
+ bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID + 2);
+
+ List<BinderCallsStats.ExportedCallStat> callStatsList = bcs.getExportedCallStats();
+ assertEquals(3, callStatsList.size());
+
+ BinderCallsStats.ExportedCallStat callStats = callStatsList.get(1);
+ assertEquals(WORKSOURCE_UID + 1, callStats.workSourceUid);
+ assertEquals(1, callStats.callCount);
+ assertEquals("com.android.internal.os.BinderCallsStats$OverflowBinder",
+ callStats.className);
+
+ callStats = callStatsList.get(2);
+ assertEquals(WORKSOURCE_UID + 2, callStats.workSourceUid);
+ assertEquals(1, callStats.callCount);
+ assertEquals("com.android.internal.os.BinderCallsStats$OverflowBinder",
+ callStats.className);
}
@Test
@@ -620,7 +658,6 @@
class TestBinderCallsStats extends BinderCallsStats {
public int callingUid = CALLING_UID;
- public int workSourceUid = WORKSOURCE_UID;
public long time = 1234;
public long elapsedTime = 0;
@@ -662,11 +699,6 @@
protected int getCallingUid() {
return callingUid;
}
-
- @Override
- protected int getWorkSourceUid() {
- return workSourceUid;
- }
}
}
diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index 141948f..4a2db0a 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -225,15 +225,18 @@
<library name="android.test.base"
file="/system/framework/android.test.base.impl.jar" />
<library name="android.test.mock"
- file="/system/framework/android.test.mock.impl.jar" />
+ file="/system/framework/android.test.mock.impl.jar"
+ dependency="android.test.base" />
<library name="android.test.runner"
- file="/system/framework/android.test.runner.impl.jar" />
+ file="/system/framework/android.test.runner.impl.jar"
+ dependency="android.test.base:android.test.mock" />
<!-- In BOOT_JARS historically, and now added to legacy applications. -->
<library name="android.hidl.base-V1.0-java"
file="/system/framework/android.hidl.base-V1.0-java.jar" />
<library name="android.hidl.manager-V1.0-java"
- file="/system/framework/android.hidl.manager-V1.0-java.jar" />
+ file="/system/framework/android.hidl.manager-V1.0-java.jar"
+ dependency="android.hidl.base-V1.0-java" />
<!-- These are the standard packages that are white-listed to always have internet
access while in power save mode, even if they aren't in the foreground. -->
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index 2abb3d5..4be8bd9 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -831,6 +831,11 @@
createBuffers(surface, surfaceFormat, extent);
+ // The window content is not updated (frozen) until a buffer of the window size is received.
+ // This prevents temporary stretching of the window after it is resized, but before the first
+ // buffer with new size is enqueued.
+ native_window_set_scaling_mode(surface->mNativeWindow, NATIVE_WINDOW_SCALING_MODE_FREEZE);
+
return true;
}
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index f1d9397..89d3cc4 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -14,7 +14,6 @@
cc_library_shared {
name: "libinputservice",
-
srcs: [
"PointerController.cpp",
"SpriteController.cpp",
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 0a90f85..80d8e72 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -89,10 +89,6 @@
mLocked.animationPending = false;
- mLocked.displayWidth = -1;
- mLocked.displayHeight = -1;
- mLocked.displayOrientation = DISPLAY_ORIENTATION_0;
-
mLocked.presentation = PRESENTATION_POINTER;
mLocked.presentationChanged = false;
@@ -110,15 +106,6 @@
mLocked.lastFrameUpdatedTime = 0;
mLocked.buttonState = 0;
-
- mPolicy->loadPointerIcon(&mLocked.pointerIcon);
-
- loadResources();
-
- if (mLocked.pointerIcon.isValid()) {
- mLocked.pointerIconChanged = true;
- updatePointerLocked();
- }
}
PointerController::~PointerController() {
@@ -144,23 +131,15 @@
bool PointerController::getBoundsLocked(float* outMinX, float* outMinY,
float* outMaxX, float* outMaxY) const {
- if (mLocked.displayWidth <= 0 || mLocked.displayHeight <= 0) {
+
+ if (!mLocked.viewport.isValid()) {
return false;
}
- *outMinX = 0;
- *outMinY = 0;
- switch (mLocked.displayOrientation) {
- case DISPLAY_ORIENTATION_90:
- case DISPLAY_ORIENTATION_270:
- *outMaxX = mLocked.displayHeight - 1;
- *outMaxY = mLocked.displayWidth - 1;
- break;
- default:
- *outMaxX = mLocked.displayWidth - 1;
- *outMaxY = mLocked.displayHeight - 1;
- break;
- }
+ *outMinX = mLocked.viewport.logicalLeft;
+ *outMinY = mLocked.viewport.logicalTop;
+ *outMaxX = mLocked.viewport.logicalRight - 1;
+ *outMaxY = mLocked.viewport.logicalBottom - 1;
return true;
}
@@ -231,6 +210,12 @@
*outY = mLocked.pointerY;
}
+int32_t PointerController::getDisplayId() const {
+ AutoMutex _l(mLock);
+
+ return mLocked.viewport.displayId;
+}
+
void PointerController::fade(Transition transition) {
AutoMutex _l(mLock);
@@ -355,48 +340,57 @@
void PointerController::reloadPointerResources() {
AutoMutex _l(mLock);
- loadResources();
-
- if (mLocked.presentation == PRESENTATION_POINTER) {
- mLocked.additionalMouseResources.clear();
- mLocked.animationResources.clear();
- mPolicy->loadPointerIcon(&mLocked.pointerIcon);
- mPolicy->loadAdditionalMouseResources(&mLocked.additionalMouseResources,
- &mLocked.animationResources);
- }
-
- mLocked.presentationChanged = true;
+ loadResourcesLocked();
updatePointerLocked();
}
-void PointerController::setDisplayViewport(int32_t width, int32_t height, int32_t orientation) {
- AutoMutex _l(mLock);
+/**
+ * The viewport values for deviceHeight and deviceWidth have already been adjusted for rotation,
+ * so here we are getting the dimensions in the original, unrotated orientation (orientation 0).
+ */
+static void getNonRotatedSize(const DisplayViewport& viewport, int32_t& width, int32_t& height) {
+ if (viewport.orientation == DISPLAY_ORIENTATION_90
+ || viewport.orientation == DISPLAY_ORIENTATION_270) {
+ width = viewport.deviceHeight;
+ height = viewport.deviceWidth;
+ } else {
+ width = viewport.deviceWidth;
+ height = viewport.deviceHeight;
+ }
+}
- // Adjust to use the display's unrotated coordinate frame.
- if (orientation == DISPLAY_ORIENTATION_90
- || orientation == DISPLAY_ORIENTATION_270) {
- int32_t temp = height;
- height = width;
- width = temp;
+void PointerController::setDisplayViewport(const DisplayViewport& viewport) {
+ AutoMutex _l(mLock);
+ if (viewport == mLocked.viewport) {
+ return;
}
- if (mLocked.displayWidth != width || mLocked.displayHeight != height) {
- mLocked.displayWidth = width;
- mLocked.displayHeight = height;
+ const DisplayViewport oldViewport = mLocked.viewport;
+ mLocked.viewport = viewport;
+
+ int32_t oldDisplayWidth, oldDisplayHeight;
+ getNonRotatedSize(oldViewport, oldDisplayWidth, oldDisplayHeight);
+ int32_t newDisplayWidth, newDisplayHeight;
+ getNonRotatedSize(viewport, newDisplayWidth, newDisplayHeight);
+
+ // Reset cursor position to center if size or display changed.
+ if (oldViewport.displayId != viewport.displayId
+ || oldDisplayWidth != newDisplayWidth
+ || oldDisplayHeight != newDisplayHeight) {
float minX, minY, maxX, maxY;
if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) {
mLocked.pointerX = (minX + maxX) * 0.5f;
mLocked.pointerY = (minY + maxY) * 0.5f;
+ // Reload icon resources for density may be changed.
+ loadResourcesLocked();
} else {
mLocked.pointerX = 0;
mLocked.pointerY = 0;
}
fadeOutAndReleaseAllSpotsLocked();
- }
-
- if (mLocked.displayOrientation != orientation) {
+ } else if (oldViewport.orientation != viewport.orientation) {
// Apply offsets to convert from the pixel top-left corner position to the pixel center.
// This creates an invariant frame of reference that we can easily rotate when
// taking into account that the pointer may be located at fractional pixel offsets.
@@ -405,37 +399,37 @@
float temp;
// Undo the previous rotation.
- switch (mLocked.displayOrientation) {
+ switch (oldViewport.orientation) {
case DISPLAY_ORIENTATION_90:
temp = x;
- x = mLocked.displayWidth - y;
+ x = oldViewport.deviceHeight - y;
y = temp;
break;
case DISPLAY_ORIENTATION_180:
- x = mLocked.displayWidth - x;
- y = mLocked.displayHeight - y;
+ x = oldViewport.deviceWidth - x;
+ y = oldViewport.deviceHeight - y;
break;
case DISPLAY_ORIENTATION_270:
temp = x;
x = y;
- y = mLocked.displayHeight - temp;
+ y = oldViewport.deviceWidth - temp;
break;
}
// Perform the new rotation.
- switch (orientation) {
+ switch (viewport.orientation) {
case DISPLAY_ORIENTATION_90:
temp = x;
x = y;
- y = mLocked.displayWidth - temp;
+ y = viewport.deviceHeight - temp;
break;
case DISPLAY_ORIENTATION_180:
- x = mLocked.displayWidth - x;
- y = mLocked.displayHeight - y;
+ x = viewport.deviceWidth - x;
+ y = viewport.deviceHeight - y;
break;
case DISPLAY_ORIENTATION_270:
temp = x;
- x = mLocked.displayHeight - y;
+ x = viewport.deviceWidth - y;
y = temp;
break;
}
@@ -444,7 +438,6 @@
// and save the results.
mLocked.pointerX = x - 0.5f;
mLocked.pointerY = y - 0.5f;
- mLocked.displayOrientation = orientation;
}
updatePointerLocked();
@@ -614,11 +607,16 @@
mLooper->removeMessages(mHandler, MSG_INACTIVITY_TIMEOUT);
}
-void PointerController::updatePointerLocked() {
+void PointerController::updatePointerLocked() REQUIRES(mLock) {
+ if (!mLocked.viewport.isValid()) {
+ return;
+ }
+
mSpriteController->openTransaction();
mLocked.pointerSprite->setLayer(Sprite::BASE_LAYER_POINTER);
mLocked.pointerSprite->setPosition(mLocked.pointerX, mLocked.pointerY);
+ mLocked.pointerSprite->setDisplayId(mLocked.viewport.displayId);
if (mLocked.pointerAlpha > 0) {
mLocked.pointerSprite->setAlpha(mLocked.pointerAlpha);
@@ -729,8 +727,18 @@
}
}
-void PointerController::loadResources() {
+void PointerController::loadResourcesLocked() REQUIRES(mLock) {
mPolicy->loadPointerResources(&mResources);
+
+ if (mLocked.presentation == PRESENTATION_POINTER) {
+ mLocked.additionalMouseResources.clear();
+ mLocked.animationResources.clear();
+ mPolicy->loadPointerIcon(&mLocked.pointerIcon);
+ mPolicy->loadAdditionalMouseResources(&mLocked.additionalMouseResources,
+ &mLocked.animationResources);
+ }
+
+ mLocked.pointerIconChanged = true;
}
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index 7f4e5a5..a32cc42 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -23,6 +23,7 @@
#include <vector>
#include <ui/DisplayInfo.h>
+#include <input/DisplayViewport.h>
#include <input/Input.h>
#include <PointerControllerInterface.h>
#include <utils/BitSet.h>
@@ -96,6 +97,7 @@
virtual int32_t getButtonState() const;
virtual void setPosition(float x, float y);
virtual void getPosition(float* outX, float* outY) const;
+ virtual int32_t getDisplayId() const;
virtual void fade(Transition transition);
virtual void unfade(Transition transition);
@@ -106,7 +108,7 @@
void updatePointerIcon(int32_t iconId);
void setCustomPointerIcon(const SpriteIcon& icon);
- void setDisplayViewport(int32_t width, int32_t height, int32_t orientation);
+ void setDisplayViewport(const DisplayViewport& viewport);
void setInactivityTimeout(InactivityTimeout inactivityTimeout);
void reloadPointerResources();
@@ -156,9 +158,7 @@
size_t animationFrameIndex;
nsecs_t lastFrameUpdatedTime;
- int32_t displayWidth;
- int32_t displayHeight;
- int32_t displayOrientation;
+ DisplayViewport viewport;
InactivityTimeout inactivityTimeout;
@@ -182,7 +182,7 @@
Vector<Spot*> spots;
Vector<sp<Sprite> > recycledSprites;
- } mLocked;
+ } mLocked GUARDED_BY(mLock);
bool getBoundsLocked(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const;
void setPositionLocked(float x, float y);
@@ -207,7 +207,7 @@
void fadeOutAndReleaseSpotLocked(Spot* spot);
void fadeOutAndReleaseAllSpotsLocked();
- void loadResources();
+ void loadResourcesLocked();
};
} // namespace android
diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp
index eb2bc98..c1868d3 100644
--- a/libs/input/SpriteController.cpp
+++ b/libs/input/SpriteController.cpp
@@ -144,13 +144,16 @@
}
}
- // Resize sprites if needed.
+ // Resize and/or reparent sprites if needed.
SurfaceComposerClient::Transaction t;
bool needApplyTransaction = false;
for (size_t i = 0; i < numSprites; i++) {
SpriteUpdate& update = updates.editItemAt(i);
+ if (update.state.surfaceControl == nullptr) {
+ continue;
+ }
- if (update.state.surfaceControl != NULL && update.state.wantSurfaceVisible()) {
+ if (update.state.wantSurfaceVisible()) {
int32_t desiredWidth = update.state.icon.bitmap.width();
int32_t desiredHeight = update.state.icon.bitmap.height();
if (update.state.surfaceWidth < desiredWidth
@@ -170,6 +173,12 @@
}
}
}
+
+ // If surface is a new one, we have to set right layer stack.
+ if (update.surfaceChanged || update.state.dirty & DIRTY_DISPLAY_ID) {
+ t.setLayerStack(update.state.surfaceControl, update.state.displayId);
+ needApplyTransaction = true;
+ }
}
if (needApplyTransaction) {
t.apply();
@@ -236,7 +245,7 @@
if (update.state.surfaceControl != NULL && (becomingVisible || becomingHidden
|| (wantSurfaceVisibleAndDrawn && (update.state.dirty & (DIRTY_ALPHA
| DIRTY_POSITION | DIRTY_TRANSFORMATION_MATRIX | DIRTY_LAYER
- | DIRTY_VISIBILITY | DIRTY_HOTSPOT))))) {
+ | DIRTY_VISIBILITY | DIRTY_HOTSPOT | DIRTY_DISPLAY_ID))))) {
needApplyTransaction = true;
if (wantSurfaceVisibleAndDrawn
@@ -445,6 +454,15 @@
}
}
+void SpriteController::SpriteImpl::setDisplayId(int32_t displayId) {
+ AutoMutex _l(mController->mLock);
+
+ if (mLocked.state.displayId != displayId) {
+ mLocked.state.displayId = displayId;
+ invalidateLocked(DIRTY_DISPLAY_ID);
+ }
+}
+
void SpriteController::SpriteImpl::invalidateLocked(uint32_t dirty) {
bool wasDirty = mLocked.state.dirty;
mLocked.state.dirty |= dirty;
diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h
index 31e43e9..5b216f5 100644
--- a/libs/input/SpriteController.h
+++ b/libs/input/SpriteController.h
@@ -125,6 +125,9 @@
/* Sets the sprite transformation matrix. */
virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix) = 0;
+
+ /* Sets the id of the display where the sprite should be shown. */
+ virtual void setDisplayId(int32_t displayId) = 0;
};
/*
@@ -170,6 +173,7 @@
DIRTY_LAYER = 1 << 4,
DIRTY_VISIBILITY = 1 << 5,
DIRTY_HOTSPOT = 1 << 6,
+ DIRTY_DISPLAY_ID = 1 << 7,
};
/* Describes the state of a sprite.
@@ -180,7 +184,7 @@
struct SpriteState {
inline SpriteState() :
dirty(0), visible(false),
- positionX(0), positionY(0), layer(0), alpha(1.0f),
+ positionX(0), positionY(0), layer(0), alpha(1.0f), displayId(ADISPLAY_ID_DEFAULT),
surfaceWidth(0), surfaceHeight(0), surfaceDrawn(false), surfaceVisible(false) {
}
@@ -193,6 +197,7 @@
int32_t layer;
float alpha;
SpriteTransformationMatrix transformationMatrix;
+ int32_t displayId;
sp<SurfaceControl> surfaceControl;
int32_t surfaceWidth;
@@ -225,6 +230,7 @@
virtual void setLayer(int32_t layer);
virtual void setAlpha(float alpha);
virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix);
+ virtual void setDisplayId(int32_t displayId);
inline const SpriteState& getStateLocked() const {
return mLocked.state;
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index e4d4795..5e77fdf 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -995,6 +995,35 @@
}
}
+ /**
+ * Returns whether direct playback of an audio format with the provided attributes is
+ * currently supported on the system.
+ * <p>Direct playback means that the audio stream is not resampled or downmixed
+ * by the framework. Checking for direct support can help the app select the representation
+ * of audio content that most closely matches the capabilities of the device and peripherials
+ * (e.g. A/V receiver) connected to it. Note that the provided stream can still be re-encoded
+ * or mixed with other streams, if needed.
+ * <p>Also note that this query only provides information about the support of an audio format.
+ * It does not indicate whether the resources necessary for the playback are available
+ * at that instant.
+ * @param format a non-null {@link AudioFormat} instance describing the format of
+ * the audio data.
+ * @param attributes a non-null {@link AudioAttributes} instance.
+ * @return true if the given audio format can be played directly.
+ */
+ public static boolean isDirectPlaybackSupported(@NonNull AudioFormat format,
+ @NonNull AudioAttributes attributes) {
+ if (format == null) {
+ throw new IllegalArgumentException("Illegal null AudioFormat argument");
+ }
+ if (attributes == null) {
+ throw new IllegalArgumentException("Illegal null AudioAttributes argument");
+ }
+ return native_is_direct_output_supported(format.getEncoding(), format.getSampleRate(),
+ format.getChannelMask(), format.getChannelIndexMask(),
+ attributes.getContentType(), attributes.getUsage(), attributes.getFlags());
+ }
+
// mask of all the positional channels supported, however the allowed combinations
// are further restricted by the matching left/right rule and
// AudioSystem.OUT_CHANNEL_COUNT_MAX
@@ -3328,6 +3357,9 @@
// Native methods called from the Java side
//--------------------
+ private static native boolean native_is_direct_output_supported(int encoding, int sampleRate,
+ int channelMask, int channelIndexMask, int contentType, int usage, int flags);
+
// post-condition: mStreamType is overwritten with a value
// that reflects the audio attributes (e.g. an AudioAttributes object with a usage of
// AudioAttributes.USAGE_MEDIA will map to AudioManager.STREAM_MUSIC
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index 95e3df2..9025829 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -1135,6 +1135,10 @@
maxChannels = 6;
} else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3)) {
maxChannels = 16;
+ } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3_JOC)) {
+ sampleRates = new int[] { 48000 };
+ bitRates = Range.create(32000, 6144000);
+ maxChannels = 16;
} else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC4)) {
sampleRates = new int[] { 44100, 48000, 96000, 192000 };
bitRates = Range.create(16000, 2688000);
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index b62108f..594a224 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -147,6 +147,7 @@
public static final String MIMETYPE_AUDIO_MSGSM = "audio/gsm";
public static final String MIMETYPE_AUDIO_AC3 = "audio/ac3";
public static final String MIMETYPE_AUDIO_EAC3 = "audio/eac3";
+ public static final String MIMETYPE_AUDIO_EAC3_JOC = "audio/eac3-joc";
public static final String MIMETYPE_AUDIO_AC4 = "audio/ac4";
public static final String MIMETYPE_AUDIO_SCRAMBLED = "audio/scrambled";
diff --git a/media/jni/soundpool/SoundPool.cpp b/media/jni/soundpool/SoundPool.cpp
index ed2afdd..5f51320 100644
--- a/media/jni/soundpool/SoundPool.cpp
+++ b/media/jni/soundpool/SoundPool.cpp
@@ -513,7 +513,8 @@
static status_t decode(int fd, int64_t offset, int64_t length,
uint32_t *rate, int *numChannels, audio_format_t *audioFormat,
- sp<MemoryHeapBase> heap, size_t *memsize) {
+ audio_channel_mask_t *channelMask, sp<MemoryHeapBase> heap,
+ size_t *memsize) {
ALOGV("fd %d, offset %" PRId64 ", size %" PRId64, fd, offset, length);
AMediaExtractor *ex = AMediaExtractor_new();
@@ -650,6 +651,10 @@
(void)AMediaFormat_delete(format);
return UNKNOWN_ERROR;
}
+ if (!AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_CHANNEL_MASK,
+ (int32_t*) channelMask)) {
+ *channelMask = AUDIO_CHANNEL_NONE;
+ }
(void)AMediaFormat_delete(format);
*memsize = written;
return OK;
@@ -665,12 +670,13 @@
uint32_t sampleRate;
int numChannels;
audio_format_t format;
+ audio_channel_mask_t channelMask;
status_t status;
mHeap = new MemoryHeapBase(kDefaultHeapSize);
ALOGV("Start decode");
status = decode(mFd, mOffset, mLength, &sampleRate, &numChannels, &format,
- mHeap, &mSize);
+ &channelMask, mHeap, &mSize);
ALOGV("close(%d)", mFd);
::close(mFd);
mFd = -1;
@@ -697,6 +703,7 @@
mSampleRate = sampleRate;
mNumChannels = numChannels;
mFormat = format;
+ mChannelMask = channelMask;
mState = READY;
return NO_ERROR;
@@ -781,7 +788,11 @@
// wrong audio audio buffer size (mAudioBufferSize)
unsigned long toggle = mToggle ^ 1;
void *userData = (void *)((unsigned long)this | toggle);
- audio_channel_mask_t channelMask = audio_channel_out_mask_from_count(numChannels);
+ audio_channel_mask_t sampleChannelMask = sample->channelMask();
+ // When sample contains a not none channel mask, use it as is.
+ // Otherwise, use channel count to calculate channel mask.
+ audio_channel_mask_t channelMask = sampleChannelMask != AUDIO_CHANNEL_NONE
+ ? sampleChannelMask : audio_channel_out_mask_from_count(numChannels);
// do not create a new audio track if current track is compatible with sample parameters
#ifdef USE_SHARED_MEM_BUFFER
diff --git a/media/jni/soundpool/SoundPool.h b/media/jni/soundpool/SoundPool.h
index 5c48a90..9d74103 100644
--- a/media/jni/soundpool/SoundPool.h
+++ b/media/jni/soundpool/SoundPool.h
@@ -58,6 +58,7 @@
int numChannels() { return mNumChannels; }
int sampleRate() { return mSampleRate; }
audio_format_t format() { return mFormat; }
+ audio_channel_mask_t channelMask() { return mChannelMask; }
size_t size() { return mSize; }
int state() { return mState; }
uint8_t* data() { return static_cast<uint8_t*>(mData->pointer()); }
@@ -68,18 +69,19 @@
private:
void init();
- size_t mSize;
- volatile int32_t mRefCount;
- uint16_t mSampleID;
- uint16_t mSampleRate;
- uint8_t mState;
- uint8_t mNumChannels;
- audio_format_t mFormat;
- int mFd;
- int64_t mOffset;
- int64_t mLength;
- sp<IMemory> mData;
- sp<MemoryHeapBase> mHeap;
+ size_t mSize;
+ volatile int32_t mRefCount;
+ uint16_t mSampleID;
+ uint16_t mSampleRate;
+ uint8_t mState;
+ uint8_t mNumChannels;
+ audio_format_t mFormat;
+ audio_channel_mask_t mChannelMask;
+ int mFd;
+ int64_t mOffset;
+ int64_t mLength;
+ sp<IMemory> mData;
+ sp<MemoryHeapBase> mHeap;
};
// stores pending events for stolen channels
diff --git a/packages/ExtServices/res/values/strings.xml b/packages/ExtServices/res/values/strings.xml
index 617e49a..a9a5450 100644
--- a/packages/ExtServices/res/values/strings.xml
+++ b/packages/ExtServices/res/values/strings.xml
@@ -22,5 +22,6 @@
<string name="autofill_field_classification_default_algorithm">EDIT_DISTANCE</string>
<string-array name="autofill_field_classification_available_algorithms">
<item>EDIT_DISTANCE</item>
+ <item>EXACT_MATCH</item>
</string-array>
</resources>
diff --git a/packages/ExtServices/src/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java b/packages/ExtServices/src/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java
index 9ba7e09..e379db8 100644
--- a/packages/ExtServices/src/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java
+++ b/packages/ExtServices/src/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java
@@ -15,8 +15,6 @@
*/
package android.ext.services.autofill;
-import static android.ext.services.autofill.EditDistanceScorer.DEFAULT_ALGORITHM;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
@@ -26,27 +24,72 @@
import com.android.internal.util.ArrayUtils;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
public class AutofillFieldClassificationServiceImpl extends AutofillFieldClassificationService {
private static final String TAG = "AutofillFieldClassificationServiceImpl";
+ private static final String DEFAULT_ALGORITHM = REQUIRED_ALGORITHM_EDIT_DISTANCE;
+
@Nullable
@Override
- public float[][] onGetScores(@Nullable String algorithmName,
- @Nullable Bundle algorithmArgs, @NonNull List<AutofillValue> actualValues,
- @NonNull List<String> userDataValues) {
+ /** @hide */
+ public float[][] onCalculateScores(@NonNull List<AutofillValue> actualValues,
+ @NonNull List<String> userDataValues, @NonNull List<String> categoryIds,
+ @Nullable String defaultAlgorithm, @Nullable Bundle defaultArgs,
+ @Nullable Map algorithms, @Nullable Map args) {
if (ArrayUtils.isEmpty(actualValues) || ArrayUtils.isEmpty(userDataValues)) {
- Log.w(TAG, "getScores(): empty currentvalues (" + actualValues + ") or userValues ("
- + userDataValues + ")");
+ Log.w(TAG, "calculateScores(): empty currentvalues (" + actualValues
+ + ") or userValues (" + userDataValues + ")");
return null;
}
- if (algorithmName != null && !algorithmName.equals(DEFAULT_ALGORITHM)) {
- Log.w(TAG, "Ignoring invalid algorithm (" + algorithmName + ") and using "
- + DEFAULT_ALGORITHM + " instead");
- }
- return EditDistanceScorer.getScores(actualValues, userDataValues);
+ return calculateScores(actualValues, userDataValues, categoryIds, defaultAlgorithm,
+ defaultArgs, (HashMap<String, String>) algorithms,
+ (HashMap<String, Bundle>) args);
+ }
+
+ /** @hide */
+ public float[][] calculateScores(@NonNull List<AutofillValue> actualValues,
+ @NonNull List<String> userDataValues, @NonNull List<String> categoryIds,
+ @Nullable String defaultAlgorithm, @Nullable Bundle defaultArgs,
+ @Nullable HashMap<String, String> algorithms,
+ @Nullable HashMap<String, Bundle> args) {
+ final int actualValuesSize = actualValues.size();
+ final int userDataValuesSize = userDataValues.size();
+ final float[][] scores = new float[actualValuesSize][userDataValuesSize];
+
+ for (int j = 0; j < userDataValuesSize; j++) {
+ final String categoryId = categoryIds.get(j);
+ String algorithmName = defaultAlgorithm;
+ Bundle arg = defaultArgs;
+ if (algorithms != null && algorithms.containsKey(categoryId)) {
+ algorithmName = algorithms.get(categoryId);
+ }
+ if (args != null && args.containsKey(categoryId)) {
+ arg = args.get(categoryId);
+ }
+
+ if (algorithmName == null || (!algorithmName.equals(DEFAULT_ALGORITHM)
+ && !algorithmName.equals(REQUIRED_ALGORITHM_EXACT_MATCH))) {
+ Log.w(TAG, "algorithmName is " + algorithmName + ", defaulting to "
+ + DEFAULT_ALGORITHM);
+ algorithmName = DEFAULT_ALGORITHM;
+ }
+
+ for (int i = 0; i < actualValuesSize; i++) {
+ if (algorithmName.equals(DEFAULT_ALGORITHM)) {
+ scores[i][j] = EditDistanceScorer.calculateScore(actualValues.get(i),
+ userDataValues.get(j));
+ } else {
+ scores[i][j] = ExactMatch.calculateScore(actualValues.get(i),
+ userDataValues.get(j), arg);
+ }
+ }
+ }
+ return scores;
}
}
diff --git a/packages/ExtServices/src/android/ext/services/autofill/EditDistanceScorer.java b/packages/ExtServices/src/android/ext/services/autofill/EditDistanceScorer.java
index 302b160..6a47901 100644
--- a/packages/ExtServices/src/android/ext/services/autofill/EditDistanceScorer.java
+++ b/packages/ExtServices/src/android/ext/services/autofill/EditDistanceScorer.java
@@ -17,13 +17,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.util.Log;
import android.view.autofill.AutofillValue;
import com.android.internal.annotations.VisibleForTesting;
-import java.util.List;
-
final class EditDistanceScorer {
private static final String TAG = "EditDistanceScorer";
@@ -31,15 +28,14 @@
// TODO(b/70291841): STOPSHIP - set to false before launching
private static final boolean DEBUG = true;
- static final String DEFAULT_ALGORITHM = "EDIT_DISTANCE";
-
/**
* Gets the field classification score of 2 values based on the edit distance between them.
*
* <p>The score is defined as: @(max_length - edit_distance) / max_length
*/
@VisibleForTesting
- static float getScore(@Nullable AutofillValue actualValue, @Nullable String userDataValue) {
+ static float calculateScore(@Nullable AutofillValue actualValue,
+ @Nullable String userDataValue) {
if (actualValue == null || !actualValue.isText() || userDataValue == null) return 0;
final String actualValueText = actualValue.getTextValue().toString();
@@ -123,26 +119,5 @@
return d[m][n];
}
- /**
- * Gets the scores in a batch.
- */
- static float[][] getScores(@NonNull List<AutofillValue> actualValues,
- @NonNull List<String> userDataValues) {
- final int actualValuesSize = actualValues.size();
- final int userDataValuesSize = userDataValues.size();
- if (DEBUG) {
- Log.d(TAG, "getScores() will return a " + actualValuesSize + "x"
- + userDataValuesSize + " matrix for " + DEFAULT_ALGORITHM);
- }
- final float[][] scores = new float[actualValuesSize][userDataValuesSize];
-
- for (int i = 0; i < actualValuesSize; i++) {
- for (int j = 0; j < userDataValuesSize; j++) {
- final float score = getScore(actualValues.get(i), userDataValues.get(j));
- scores[i][j] = score;
- }
- }
- return scores;
- }
}
diff --git a/packages/ExtServices/src/android/ext/services/autofill/ExactMatch.java b/packages/ExtServices/src/android/ext/services/autofill/ExactMatch.java
new file mode 100644
index 0000000..3e55c5c
--- /dev/null
+++ b/packages/ExtServices/src/android/ext/services/autofill/ExactMatch.java
@@ -0,0 +1,67 @@
+/*
+ * 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.ext.services.autofill;
+
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+final class ExactMatch {
+
+ /**
+ * Gets the field classification score of 2 values based on whether they are an exact match
+ *
+ * @return {@code 1.0} if the two values are an exact match, {@code 0.0} otherwise.
+ */
+ @VisibleForTesting
+ static float calculateScore(@Nullable AutofillValue actualValue,
+ @Nullable String userDataValue, @Nullable Bundle args) {
+ if (actualValue == null || !actualValue.isText() || userDataValue == null) return 0;
+
+ final String actualValueText = actualValue.getTextValue().toString();
+
+ final int suffixLength;
+ if (args != null) {
+ suffixLength = args.getInt("suffix", -1);
+
+ if (suffixLength < 0) {
+ throw new IllegalArgumentException("suffix argument is invalid");
+ }
+
+ final String actualValueSuffix;
+ if (suffixLength < actualValueText.length()) {
+ actualValueSuffix = actualValueText.substring(actualValueText.length()
+ - suffixLength);
+ } else {
+ actualValueSuffix = actualValueText;
+ }
+
+ final String userDataValueSuffix;
+ if (suffixLength < userDataValue.length()) {
+ userDataValueSuffix = userDataValue.substring(userDataValue.length()
+ - suffixLength);
+ } else {
+ userDataValueSuffix = userDataValue;
+ }
+
+ return (actualValueSuffix.equalsIgnoreCase(userDataValueSuffix)) ? 1 : 0;
+ } else {
+ return actualValueText.equalsIgnoreCase(userDataValue) ? 1 : 0;
+ }
+ }
+}
diff --git a/packages/ExtServices/src/android/ext/services/notification/Assistant.java b/packages/ExtServices/src/android/ext/services/notification/Assistant.java
index 0cad5af..133d8ba 100644
--- a/packages/ExtServices/src/android/ext/services/notification/Assistant.java
+++ b/packages/ExtServices/src/android/ext/services/notification/Assistant.java
@@ -27,20 +27,15 @@
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.IPackageManager;
-import android.database.ContentObserver;
import android.ext.services.notification.AgingHelper.Callback;
-import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
-import android.os.Handler;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.storage.StorageManager;
-import android.provider.Settings;
import android.service.notification.Adjustment;
import android.service.notification.NotificationAssistantService;
import android.service.notification.NotificationStats;
@@ -92,8 +87,6 @@
PREJUDICAL_DISMISSALS.add(REASON_LISTENER_CANCEL);
}
- private float mDismissToViewRatioLimit;
- private int mStreakLimit;
private SmartActionsHelper mSmartActionsHelper;
private NotificationCategorizer mNotificationCategorizer;
private AgingHelper mAgingHelper;
@@ -107,7 +100,11 @@
private Ranking mFakeRanking = null;
private AtomicFile mFile = null;
private IPackageManager mPackageManager;
- protected SettingsObserver mSettingsObserver;
+
+ @VisibleForTesting
+ protected AssistantSettings.Factory mSettingsFactory = AssistantSettings.FACTORY;
+ @VisibleForTesting
+ protected AssistantSettings mSettings;
public Assistant() {
}
@@ -118,7 +115,8 @@
// Contexts are correctly hooked up by the creation step, which is required for the observer
// to be hooked up/initialized.
mPackageManager = ActivityThread.getPackageManager();
- mSettingsObserver = new SettingsObserver(mHandler);
+ mSettings = mSettingsFactory.createAndRegister(mHandler,
+ getApplicationContext().getContentResolver(), getUserId(), this::updateThresholds);
mSmartActionsHelper = new SmartActionsHelper();
mNotificationCategorizer = new NotificationCategorizer();
mAgingHelper = new AgingHelper(getContext(),
@@ -216,11 +214,11 @@
if (!isForCurrentUser(sbn)) {
return null;
}
- NotificationEntry entry = new NotificationEntry(
- ActivityThread.getPackageManager(), sbn, channel);
+ NotificationEntry entry = new NotificationEntry(mPackageManager, sbn, channel);
ArrayList<Notification.Action> actions =
- mSmartActionsHelper.suggestActions(this, entry);
- ArrayList<CharSequence> replies = mSmartActionsHelper.suggestReplies(this, entry);
+ mSmartActionsHelper.suggestActions(this, entry, mSettings);
+ ArrayList<CharSequence> replies =
+ mSmartActionsHelper.suggestReplies(this, entry, mSettings);
return createEnqueuedNotificationAdjustment(entry, actions, replies);
}
@@ -239,8 +237,7 @@
if (!smartReplies.isEmpty()) {
signals.putCharSequenceArrayList(Adjustment.KEY_SMART_REPLIES, smartReplies);
}
- if (Settings.Secure.getInt(getContentResolver(),
- Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL, 1) == 1) {
+ if (mSettings.mNewInterruptionModel) {
if (mNotificationCategorizer.shouldSilence(entry)) {
final int importance = entry.getImportance() < IMPORTANCE_LOW
? entry.getImportance() : IMPORTANCE_LOW;
@@ -460,6 +457,11 @@
}
@VisibleForTesting
+ public void setSmartActionsHelper(SmartActionsHelper smartActionsHelper) {
+ mSmartActionsHelper = smartActionsHelper;
+ }
+
+ @VisibleForTesting
public ChannelImpressions getImpressions(String key) {
synchronized (mkeyToImpressions) {
return mkeyToImpressions.get(key);
@@ -475,10 +477,20 @@
private ChannelImpressions createChannelImpressionsWithThresholds() {
ChannelImpressions impressions = new ChannelImpressions();
- impressions.updateThresholds(mDismissToViewRatioLimit, mStreakLimit);
+ impressions.updateThresholds(mSettings.mDismissToViewRatioLimit, mSettings.mStreakLimit);
return impressions;
}
+ private void updateThresholds() {
+ // Update all existing channel impression objects with any new limits/thresholds.
+ synchronized (mkeyToImpressions) {
+ for (ChannelImpressions channelImpressions: mkeyToImpressions.values()) {
+ channelImpressions.updateThresholds(
+ mSettings.mDismissToViewRatioLimit, mSettings.mStreakLimit);
+ }
+ }
+ }
+
protected final class AgingCallback implements Callback {
@Override
public void sendAdjustment(String key, int newImportance) {
@@ -495,51 +507,4 @@
}
}
- /**
- * Observer for updates on blocking helper threshold values.
- */
- protected final class SettingsObserver extends ContentObserver {
- private final Uri STREAK_LIMIT_URI =
- Settings.Global.getUriFor(Settings.Global.BLOCKING_HELPER_STREAK_LIMIT);
- private final Uri DISMISS_TO_VIEW_RATIO_LIMIT_URI =
- Settings.Global.getUriFor(
- Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT);
-
- public SettingsObserver(Handler handler) {
- super(handler);
- ContentResolver resolver = getApplicationContext().getContentResolver();
- resolver.registerContentObserver(
- DISMISS_TO_VIEW_RATIO_LIMIT_URI, false, this, getUserId());
- resolver.registerContentObserver(STREAK_LIMIT_URI, false, this, getUserId());
-
- // Update all uris on creation.
- update(null);
- }
-
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- update(uri);
- }
-
- private void update(Uri uri) {
- ContentResolver resolver = getApplicationContext().getContentResolver();
- if (uri == null || DISMISS_TO_VIEW_RATIO_LIMIT_URI.equals(uri)) {
- mDismissToViewRatioLimit = Settings.Global.getFloat(
- resolver, Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
- ChannelImpressions.DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT);
- }
- if (uri == null || STREAK_LIMIT_URI.equals(uri)) {
- mStreakLimit = Settings.Global.getInt(
- resolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT,
- ChannelImpressions.DEFAULT_STREAK_LIMIT);
- }
-
- // Update all existing channel impression objects with any new limits/thresholds.
- synchronized (mkeyToImpressions) {
- for (ChannelImpressions channelImpressions: mkeyToImpressions.values()) {
- channelImpressions.updateThresholds(mDismissToViewRatioLimit, mStreakLimit);
- }
- }
- }
- }
}
diff --git a/packages/ExtServices/src/android/ext/services/notification/AssistantSettings.java b/packages/ExtServices/src/android/ext/services/notification/AssistantSettings.java
new file mode 100644
index 0000000..39a1676
--- /dev/null
+++ b/packages/ExtServices/src/android/ext/services/notification/AssistantSettings.java
@@ -0,0 +1,140 @@
+/**
+ * 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.ext.services.notification;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.KeyValueListParser;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Observes the settings for {@link Assistant}.
+ */
+final class AssistantSettings extends ContentObserver {
+ public static Factory FACTORY = AssistantSettings::createAndRegister;
+ private static final boolean DEFAULT_GENERATE_REPLIES = true;
+ private static final boolean DEFAULT_GENERATE_ACTIONS = true;
+ private static final int DEFAULT_NEW_INTERRUPTION_MODEL_INT = 1;
+
+ private static final Uri STREAK_LIMIT_URI =
+ Settings.Global.getUriFor(Settings.Global.BLOCKING_HELPER_STREAK_LIMIT);
+ private static final Uri DISMISS_TO_VIEW_RATIO_LIMIT_URI =
+ Settings.Global.getUriFor(
+ Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT);
+ private static final Uri SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS_URI =
+ Settings.Global.getUriFor(
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS);
+ private static final Uri NOTIFICATION_NEW_INTERRUPTION_MODEL_URI =
+ Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL);
+
+ private static final String KEY_GENERATE_REPLIES = "generate_replies";
+ private static final String KEY_GENERATE_ACTIONS = "generate_actions";
+
+ private final KeyValueListParser mParser = new KeyValueListParser(',');
+ private final ContentResolver mResolver;
+ private final int mUserId;
+
+ @VisibleForTesting
+ protected final Runnable mOnUpdateRunnable;
+
+ // Actuall configuration settings.
+ float mDismissToViewRatioLimit;
+ int mStreakLimit;
+ boolean mGenerateReplies = DEFAULT_GENERATE_REPLIES;
+ boolean mGenerateActions = DEFAULT_GENERATE_ACTIONS;
+ boolean mNewInterruptionModel;
+
+ private AssistantSettings(Handler handler, ContentResolver resolver, int userId,
+ Runnable onUpdateRunnable) {
+ super(handler);
+ mResolver = resolver;
+ mUserId = userId;
+ mOnUpdateRunnable = onUpdateRunnable;
+ }
+
+ private static AssistantSettings createAndRegister(
+ Handler handler, ContentResolver resolver, int userId, Runnable onUpdateRunnable) {
+ AssistantSettings assistantSettings =
+ new AssistantSettings(handler, resolver, userId, onUpdateRunnable);
+ assistantSettings.register();
+ return assistantSettings;
+ }
+
+ /**
+ * Creates an instance but doesn't register it as an observer.
+ */
+ @VisibleForTesting
+ protected static AssistantSettings createForTesting(
+ Handler handler, ContentResolver resolver, int userId, Runnable onUpdateRunnable) {
+ return new AssistantSettings(handler, resolver, userId, onUpdateRunnable);
+ }
+
+ private void register() {
+ mResolver.registerContentObserver(
+ DISMISS_TO_VIEW_RATIO_LIMIT_URI, false, this, mUserId);
+ mResolver.registerContentObserver(STREAK_LIMIT_URI, false, this, mUserId);
+ mResolver.registerContentObserver(
+ SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS_URI, false, this, mUserId);
+
+ // Update all uris on creation.
+ update(null);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ update(uri);
+ }
+
+ private void update(Uri uri) {
+ if (uri == null || DISMISS_TO_VIEW_RATIO_LIMIT_URI.equals(uri)) {
+ mDismissToViewRatioLimit = Settings.Global.getFloat(
+ mResolver, Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
+ ChannelImpressions.DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT);
+ }
+ if (uri == null || STREAK_LIMIT_URI.equals(uri)) {
+ mStreakLimit = Settings.Global.getInt(
+ mResolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT,
+ ChannelImpressions.DEFAULT_STREAK_LIMIT);
+ }
+ if (uri == null || SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS_URI.equals(uri)) {
+ mParser.setString(
+ Settings.Global.getString(mResolver,
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+ mGenerateReplies =
+ mParser.getBoolean(KEY_GENERATE_REPLIES, DEFAULT_GENERATE_REPLIES);
+ mGenerateActions =
+ mParser.getBoolean(KEY_GENERATE_ACTIONS, DEFAULT_GENERATE_ACTIONS);
+ }
+ if (uri == null || NOTIFICATION_NEW_INTERRUPTION_MODEL_URI.equals(uri)) {
+ int mNewInterruptionModelInt = Settings.Secure.getInt(
+ mResolver, Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL,
+ DEFAULT_NEW_INTERRUPTION_MODEL_INT);
+ mNewInterruptionModel = mNewInterruptionModelInt == 1;
+ }
+
+ mOnUpdateRunnable.run();
+ }
+
+ public interface Factory {
+ AssistantSettings createAndRegister(Handler handler, ContentResolver resolver, int userId,
+ Runnable onUpdateRunnable);
+ }
+}
diff --git a/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java
index 892267b..6f2b6c9 100644
--- a/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java
+++ b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java
@@ -69,8 +69,11 @@
* from notification text / message, we can replace most of the code here by consuming that API.
*/
@NonNull
- ArrayList<Notification.Action> suggestActions(
- @Nullable Context context, @NonNull NotificationEntry entry) {
+ ArrayList<Notification.Action> suggestActions(@Nullable Context context,
+ @NonNull NotificationEntry entry, @NonNull AssistantSettings settings) {
+ if (!settings.mGenerateActions) {
+ return EMPTY_ACTION_LIST;
+ }
if (!isEligibleForActionAdjustment(entry)) {
return EMPTY_ACTION_LIST;
}
@@ -86,8 +89,11 @@
getMostSalientActionText(entry.getNotification()), MAX_SMART_ACTIONS);
}
- ArrayList<CharSequence> suggestReplies(
- @Nullable Context context, @NonNull NotificationEntry entry) {
+ ArrayList<CharSequence> suggestReplies(@Nullable Context context,
+ @NonNull NotificationEntry entry, @NonNull AssistantSettings settings) {
+ if (!settings.mGenerateReplies) {
+ return EMPTY_REPLY_LIST;
+ }
if (!isEligibleForReplyAdjustment(entry)) {
return EMPTY_REPLY_LIST;
}
diff --git a/packages/ExtServices/tests/Android.mk b/packages/ExtServices/tests/Android.mk
index 0a95b85..a57fa94 100644
--- a/packages/ExtServices/tests/Android.mk
+++ b/packages/ExtServices/tests/Android.mk
@@ -12,7 +12,8 @@
mockito-target-minus-junit4 \
espresso-core \
truth-prebuilt \
- testables
+ testables \
+ testng
# Include all test java files.
LOCAL_SRC_FILES := $(call all-java-files-under, src)
diff --git a/packages/ExtServices/tests/src/android/ext/services/autofill/AutofillFieldClassificationServiceImplTest.java b/packages/ExtServices/tests/src/android/ext/services/autofill/AutofillFieldClassificationServiceImplTest.java
index 48c076e..6fda4c7 100644
--- a/packages/ExtServices/tests/src/android/ext/services/autofill/AutofillFieldClassificationServiceImplTest.java
+++ b/packages/ExtServices/tests/src/android/ext/services/autofill/AutofillFieldClassificationServiceImplTest.java
@@ -16,14 +16,21 @@
package android.ext.services.autofill;
+import static android.service.autofill.AutofillFieldClassificationService.REQUIRED_ALGORITHM_EXACT_MATCH;
+import static android.view.autofill.AutofillValue.forText;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.os.Bundle;
+import android.view.autofill.AutofillValue;
+
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.view.autofill.AutofillValue;
+import java.util.HashMap;
+import java.util.List;
/**
* Contains the base tests that does not rely on the specific algorithm implementation.
@@ -34,26 +41,78 @@
new AutofillFieldClassificationServiceImpl();
@Test
- public void testOnGetScores_nullActualValues() {
- assertThat(mService.onGetScores(null, null, null, Arrays.asList("whatever"))).isNull();
+ public void testOnCalculateScores_nullActualValues() {
+ assertThat(mService.onCalculateScores(null, null, null, null, null, null, null)).isNull();
}
@Test
- public void testOnGetScores_emptyActualValues() {
- assertThat(mService.onGetScores(null, null, Collections.emptyList(),
- Arrays.asList("whatever"))).isNull();
+ public void testOnCalculateScores_emptyActualValues() {
+ assertThat(mService.onCalculateScores(Collections.emptyList(), Arrays.asList("whatever"),
+ null, null, null, null, null)).isNull();
}
@Test
- public void testOnGetScores_nullUserDataValues() {
- assertThat(mService.onGetScores(null, null,
- Arrays.asList(AutofillValue.forText("whatever")), null)).isNull();
+ public void testOnCalculateScores_nullUserDataValues() {
+ assertThat(mService.onCalculateScores(Arrays.asList(AutofillValue.forText("whatever")),
+ null, null, null, null, null, null)).isNull();
}
@Test
- public void testOnGetScores_emptyUserDataValues() {
- assertThat(mService.onGetScores(null, null,
- Arrays.asList(AutofillValue.forText("whatever")), Collections.emptyList()))
- .isNull();
+ public void testOnCalculateScores_emptyUserDataValues() {
+ assertThat(mService.onCalculateScores(Arrays.asList(AutofillValue.forText("whatever")),
+ Collections.emptyList(), null, null, null, null, null))
+ .isNull();
+ }
+
+ @Test
+ public void testCalculateScores() {
+ final List<AutofillValue> actualValues = Arrays.asList(forText("A"), forText("b"),
+ forText("dude"));
+ final List<String> userDataValues = Arrays.asList("a", "b", "B", "ab", "c", "dude",
+ "sweet_dude", "dude_sweet");
+ final List<String> categoryIds = Arrays.asList("cat", "cat", "cat", "cat", "cat", "last4",
+ "last4", "last4");
+ final HashMap<String, String> algorithms = new HashMap<>(1);
+ algorithms.put("last4", REQUIRED_ALGORITHM_EXACT_MATCH);
+
+ final Bundle last4Bundle = new Bundle();
+ last4Bundle.putInt("suffix", 4);
+
+ final HashMap<String, Bundle> args = new HashMap<>(1);
+ args.put("last4", last4Bundle);
+
+ final float[][] expectedScores = new float[][] {
+ new float[] { 1F, 0F, 0F, 0.5F, 0F, 0F, 0F, 0F },
+ new float[] { 0F, 1F, 1F, 0.5F, 0F, 0F, 0F, 0F },
+ new float[] { 0F, 0F, 0F, 0F , 0F, 1F, 1F, 0F }
+ };
+ final float[][] actualScores = mService.onCalculateScores(actualValues, userDataValues,
+ categoryIds, null, null, algorithms, args);
+
+ // Unfortunately, Truth does not have an easy way to compare float matrices and show useful
+ // messages in case of error, so we need to check.
+ assertWithMessage("actual=%s, expected=%s", toString(actualScores),
+ toString(expectedScores)).that(actualScores.length).isEqualTo(3);
+ for (int i = 0; i < 3; i++) {
+ assertWithMessage("actual=%s, expected=%s", toString(actualScores),
+ toString(expectedScores)).that(actualScores[i].length).isEqualTo(8);
+ }
+
+ for (int i = 0; i < actualScores.length; i++) {
+ final float[] line = actualScores[i];
+ for (int j = 0; j < line.length; j++) {
+ float cell = line[j];
+ assertWithMessage("wrong score at [%s, %s]", i, j).that(cell).isWithin(0.01F)
+ .of(expectedScores[i][j]);
+ }
+ }
+ }
+
+ public static String toString(float[][] matrix) {
+ final StringBuilder string = new StringBuilder("[ ");
+ for (int i = 0; i < matrix.length; i++) {
+ string.append(Arrays.toString(matrix[i])).append(" ");
+ }
+ return string.append(" ]").toString();
}
}
diff --git a/packages/ExtServices/tests/src/android/ext/services/autofill/EditDistanceScorerTest.java b/packages/ExtServices/tests/src/android/ext/services/autofill/EditDistanceScorerTest.java
index afe2236..9b9d4be 100644
--- a/packages/ExtServices/tests/src/android/ext/services/autofill/EditDistanceScorerTest.java
+++ b/packages/ExtServices/tests/src/android/ext/services/autofill/EditDistanceScorerTest.java
@@ -15,107 +15,67 @@
*/
package android.ext.services.autofill;
-import static android.ext.services.autofill.EditDistanceScorer.getScore;
-import static android.ext.services.autofill.EditDistanceScorer.getScores;
-import static android.view.autofill.AutofillValue.forText;
+import static android.ext.services.autofill.EditDistanceScorer.calculateScore;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
import android.view.autofill.AutofillValue;
import org.junit.Test;
-import java.util.Arrays;
-import java.util.List;
-
public class EditDistanceScorerTest {
@Test
- public void testGetScore_nullValue() {
- assertFloat(getScore(null, "D'OH!"), 0);
+ public void testCalculateScore_nullValue() {
+ assertFloat(calculateScore(null, "D'OH!"), 0);
}
@Test
- public void testGetScore_nonTextValue() {
- assertFloat(getScore(AutofillValue.forToggle(true), "D'OH!"), 0);
+ public void testCalculateScore_nonTextValue() {
+ assertFloat(calculateScore(AutofillValue.forToggle(true), "D'OH!"), 0);
}
@Test
- public void testGetScore_nullUserData() {
- assertFloat(getScore(AutofillValue.forText("D'OH!"), null), 0);
+ public void testCalculateScore_nullUserData() {
+ assertFloat(calculateScore(AutofillValue.forText("D'OH!"), null), 0);
}
@Test
- public void testGetScore_fullMatch() {
- assertFloat(getScore(AutofillValue.forText("D'OH!"), "D'OH!"), 1);
- assertFloat(getScore(AutofillValue.forText(""), ""), 1);
+ public void testCalculateScore_fullMatch() {
+ assertFloat(calculateScore(AutofillValue.forText("D'OH!"), "D'OH!"), 1);
+ assertFloat(calculateScore(AutofillValue.forText(""), ""), 1);
}
@Test
- public void testGetScore_fullMatchMixedCase() {
- assertFloat(getScore(AutofillValue.forText("D'OH!"), "D'oH!"), 1);
+ public void testCalculateScore_fullMatchMixedCase() {
+ assertFloat(calculateScore(AutofillValue.forText("D'OH!"), "D'oH!"), 1);
}
@Test
- public void testGetScore_mismatchDifferentSizes() {
- assertFloat(getScore(AutofillValue.forText("X"), "Xy"), 0.50F);
- assertFloat(getScore(AutofillValue.forText("Xy"), "X"), 0.50F);
- assertFloat(getScore(AutofillValue.forText("One"), "MoreThanOne"), 0.27F);
- assertFloat(getScore(AutofillValue.forText("MoreThanOne"), "One"), 0.27F);
- assertFloat(getScore(AutofillValue.forText("1600 Amphitheatre Parkway"),
+ public void testCalculateScore_mismatchDifferentSizes() {
+ assertFloat(calculateScore(AutofillValue.forText("X"), "Xy"), 0.50F);
+ assertFloat(calculateScore(AutofillValue.forText("Xy"), "X"), 0.50F);
+ assertFloat(calculateScore(AutofillValue.forText("One"), "MoreThanOne"), 0.27F);
+ assertFloat(calculateScore(AutofillValue.forText("MoreThanOne"), "One"), 0.27F);
+ assertFloat(calculateScore(AutofillValue.forText("1600 Amphitheatre Parkway"),
"1600 Amphitheatre Pkwy"), 0.88F);
- assertFloat(getScore(AutofillValue.forText("1600 Amphitheatre Pkwy"),
+ assertFloat(calculateScore(AutofillValue.forText("1600 Amphitheatre Pkwy"),
"1600 Amphitheatre Parkway"), 0.88F);
}
@Test
- public void testGetScore_partialMatch() {
- assertFloat(getScore(AutofillValue.forText("Dude"), "Dxxx"), 0.25F);
- assertFloat(getScore(AutofillValue.forText("Dude"), "DUxx"), 0.50F);
- assertFloat(getScore(AutofillValue.forText("Dude"), "DUDx"), 0.75F);
- assertFloat(getScore(AutofillValue.forText("Dxxx"), "Dude"), 0.25F);
- assertFloat(getScore(AutofillValue.forText("DUxx"), "Dude"), 0.50F);
- assertFloat(getScore(AutofillValue.forText("DUDx"), "Dude"), 0.75F);
- }
-
- @Test
- public void testGetScores() {
- final List<AutofillValue> actualValues = Arrays.asList(forText("A"), forText("b"));
- final List<String> userDataValues = Arrays.asList("a", "B", "ab", "c");
- final float[][] expectedScores = new float[][] {
- new float[] { 1F, 0F, 0.5F, 0F },
- new float[] { 0F, 1F, 0.5F, 0F }
- };
- final float[][] actualScores = getScores(actualValues, userDataValues);
-
- // Unfortunately, Truth does not have an easy way to compare float matrices and show useful
- // messages in case of error, so we need to check.
- assertWithMessage("actual=%s, expected=%s", toString(actualScores),
- toString(expectedScores)).that(actualScores.length).isEqualTo(2);
- assertWithMessage("actual=%s, expected=%s", toString(actualScores),
- toString(expectedScores)).that(actualScores[0].length).isEqualTo(4);
- assertWithMessage("actual=%s, expected=%s", toString(actualScores),
- toString(expectedScores)).that(actualScores[1].length).isEqualTo(4);
- for (int i = 0; i < actualScores.length; i++) {
- final float[] line = actualScores[i];
- for (int j = 0; j < line.length; j++) {
- float cell = line[j];
- assertWithMessage("wrong score at [%s, %s]", i, j).that(cell).isWithin(0.01F)
- .of(expectedScores[i][j]);
- }
- }
+ public void testCalculateScore_partialMatch() {
+ assertFloat(calculateScore(AutofillValue.forText("Dude"), "Dxxx"), 0.25F);
+ assertFloat(calculateScore(AutofillValue.forText("Dude"), "DUxx"), 0.50F);
+ assertFloat(calculateScore(AutofillValue.forText("Dude"), "DUDx"), 0.75F);
+ assertFloat(calculateScore(AutofillValue.forText("Dxxx"), "Dude"), 0.25F);
+ assertFloat(calculateScore(AutofillValue.forText("DUxx"), "Dude"), 0.50F);
+ assertFloat(calculateScore(AutofillValue.forText("DUDx"), "Dude"), 0.75F);
}
public static void assertFloat(float actualValue, float expectedValue) {
assertThat(actualValue).isWithin(0.01F).of(expectedValue);
}
- public static String toString(float[][] matrix) {
- final StringBuilder string = new StringBuilder("[ ");
- for (int i = 0; i < matrix.length; i++) {
- string.append(Arrays.toString(matrix[i])).append(" ");
- }
- return string.append(" ]").toString();
- }
+
}
diff --git a/packages/ExtServices/tests/src/android/ext/services/autofill/ExactMatchTest.java b/packages/ExtServices/tests/src/android/ext/services/autofill/ExactMatchTest.java
new file mode 100644
index 0000000..bf5e160
--- /dev/null
+++ b/packages/ExtServices/tests/src/android/ext/services/autofill/ExactMatchTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.ext.services.autofill;
+
+import static android.ext.services.autofill.ExactMatch.calculateScore;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.os.Bundle;
+import android.view.autofill.AutofillValue;
+
+import org.junit.Test;
+
+public class ExactMatchTest {
+
+ private Bundle last4Bundle() {
+ final Bundle bundle = new Bundle();
+ bundle.putInt("suffix", 4);
+ return bundle;
+ }
+
+ @Test
+ public void testCalculateScore_nullValue() {
+ assertFloat(calculateScore(null, "TEST", null), 0);
+ }
+
+ @Test
+ public void testCalculateScore_nonTextValue() {
+ assertFloat(calculateScore(AutofillValue.forToggle(true), "TEST", null), 0);
+ }
+
+ @Test
+ public void testCalculateScore_nullUserData() {
+ assertFloat(calculateScore(AutofillValue.forText("TEST"), null, null), 0);
+ }
+
+ @Test
+ public void testCalculateScore_succeedMatchMixedCases_last4() {
+ final Bundle last4 = last4Bundle();
+ assertFloat(calculateScore(AutofillValue.forText("TEST"), "1234 test", last4), 1);
+ assertFloat(calculateScore(AutofillValue.forText("test"), "1234 TEST", last4), 1);
+ }
+
+ @Test
+ public void testCalculateScore_mismatchDifferentSizes_last4() {
+ final Bundle last4 = last4Bundle();
+ assertFloat(calculateScore(AutofillValue.forText("TEST"), "TEST1", last4), 0);
+ assertFloat(calculateScore(AutofillValue.forText(""), "TEST", last4), 0);
+ assertFloat(calculateScore(AutofillValue.forText("TEST"), "", last4), 0);
+ }
+
+ @Test
+ public void testCalculateScore_match() {
+ final Bundle last4 = last4Bundle();
+ assertFloat(calculateScore(AutofillValue.forText("1234 1234 1234 1234"),
+ "xxxx xxxx xxxx 1234", last4), 1);
+ assertFloat(calculateScore(AutofillValue.forText("TEST"), "TEST", null), 1);
+ assertFloat(calculateScore(AutofillValue.forText("TEST 1234"), "1234", last4), 1);
+ assertFloat(calculateScore(AutofillValue.forText("TEST"), "test", null), 1);
+ }
+
+ @Test
+ public void testCalculateScore_badBundle() {
+ final Bundle bundle = new Bundle();
+ bundle.putInt("suffix", -2);
+ assertThrows(IllegalArgumentException.class, () -> calculateScore(
+ AutofillValue.forText("TEST"), "TEST", bundle));
+
+ final Bundle largeBundle = new Bundle();
+ largeBundle.putInt("suffix", 10);
+ assertFloat(calculateScore(AutofillValue.forText("TEST"), "TEST", largeBundle), 1);
+
+ final Bundle stringBundle = new Bundle();
+ stringBundle.putString("suffix", "value");
+ assertThrows(IllegalArgumentException.class, () -> calculateScore(
+ AutofillValue.forText("TEST"), "TEST", stringBundle));
+
+ }
+
+ public static void assertFloat(float actualValue, float expectedValue) {
+ assertThat(actualValue).isWithin(0.01F).of(expectedValue);
+ }
+}
diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantSettingsTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantSettingsTest.java
new file mode 100644
index 0000000..fd23f2b
--- /dev/null
+++ b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantSettingsTest.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 android.ext.services.notification;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentResolver;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.testing.TestableContext;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class AssistantSettingsTest {
+ private static final int USER_ID = 5;
+
+ @Rule
+ public final TestableContext mContext =
+ new TestableContext(InstrumentationRegistry.getContext(), null);
+
+ @Mock Runnable mOnUpdateRunnable;
+
+ private ContentResolver mResolver;
+ private AssistantSettings mAssistantSettings;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mResolver = mContext.getContentResolver();
+ Handler handler = new Handler(Looper.getMainLooper());
+
+ // To bypass real calls to global settings values, set the Settings values here.
+ Settings.Global.putFloat(mResolver,
+ Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, 0.8f);
+ Settings.Global.putInt(mResolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, 2);
+ Settings.Global.putString(mResolver,
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS,
+ "generate_replies=true,generate_actions=true");
+ Settings.Secure.putInt(mResolver, Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL, 1);
+
+ mAssistantSettings = AssistantSettings.createForTesting(
+ handler, mResolver, USER_ID, mOnUpdateRunnable);
+ }
+
+ @Test
+ public void testGenerateRepliesDisabled() {
+ Settings.Global.putString(mResolver,
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS,
+ "generate_replies=false");
+
+ // Notify for the settings values we updated.
+ mAssistantSettings.onChange(false,
+ Settings.Global.getUriFor(
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+
+
+ assertFalse(mAssistantSettings.mGenerateReplies);
+ }
+
+ @Test
+ public void testGenerateRepliesEnabled() {
+ Settings.Global.putString(mResolver,
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, "generate_replies=true");
+
+ // Notify for the settings values we updated.
+ mAssistantSettings.onChange(false,
+ Settings.Global.getUriFor(
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+
+ assertTrue(mAssistantSettings.mGenerateReplies);
+ }
+
+ @Test
+ public void testGenerateActionsDisabled() {
+ Settings.Global.putString(mResolver,
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, "generate_actions=false");
+
+ // Notify for the settings values we updated.
+ mAssistantSettings.onChange(false,
+ Settings.Global.getUriFor(
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+
+ assertTrue(mAssistantSettings.mGenerateReplies);
+ }
+
+ @Test
+ public void testGenerateActionsEnabled() {
+ Settings.Global.putString(mResolver,
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, "generate_actions=true");
+
+ // Notify for the settings values we updated.
+ mAssistantSettings.onChange(false,
+ Settings.Global.getUriFor(
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+
+ assertTrue(mAssistantSettings.mGenerateReplies);
+ }
+
+ @Test
+ public void testStreakLimit() {
+ verify(mOnUpdateRunnable, never()).run();
+
+ // Update settings value.
+ int newStreakLimit = 4;
+ Settings.Global.putInt(mResolver,
+ Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, newStreakLimit);
+
+ // Notify for the settings value we updated.
+ mAssistantSettings.onChange(false, Settings.Global.getUriFor(
+ Settings.Global.BLOCKING_HELPER_STREAK_LIMIT));
+
+ assertEquals(newStreakLimit, mAssistantSettings.mStreakLimit);
+ verify(mOnUpdateRunnable).run();
+ }
+
+ @Test
+ public void testDismissToViewRatioLimit() {
+ verify(mOnUpdateRunnable, never()).run();
+
+ // Update settings value.
+ float newDismissToViewRatioLimit = 3f;
+ Settings.Global.putFloat(mResolver,
+ Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
+ newDismissToViewRatioLimit);
+
+ // Notify for the settings value we updated.
+ mAssistantSettings.onChange(false, Settings.Global.getUriFor(
+ Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT));
+
+ assertEquals(newDismissToViewRatioLimit, mAssistantSettings.mDismissToViewRatioLimit, 1e-6);
+ verify(mOnUpdateRunnable).run();
+ }
+}
diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java
index 2eb005a..0a95b83 100644
--- a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java
+++ b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java
@@ -33,13 +33,11 @@
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
-import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.os.Build;
import android.os.UserHandle;
-import android.provider.Settings;
import android.service.notification.Adjustment;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.Ranking;
@@ -86,8 +84,7 @@
@Mock INotificationManager mNoMan;
@Mock AtomicFile mFile;
- @Mock
- IPackageManager mPackageManager;
+ @Mock IPackageManager mPackageManager;
Assistant mAssistant;
Application mApplication;
@@ -108,20 +105,26 @@
new Intent("android.service.notification.NotificationAssistantService");
startIntent.setPackage("android.ext.services");
- // To bypass real calls to global settings values, set the Settings values here.
- Settings.Global.putFloat(mContext.getContentResolver(),
- Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, 0.8f);
- Settings.Global.putInt(mContext.getContentResolver(),
- Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, 2);
mApplication = (Application) InstrumentationRegistry.getInstrumentation().
getTargetContext().getApplicationContext();
// Force the test to use the correct application instead of trying to use a mock application
setApplication(mApplication);
- bindService(startIntent);
+
+ setupService();
mAssistant = getService();
+
+ // Override the AssistantSettings factory.
+ mAssistant.mSettingsFactory = AssistantSettings::createForTesting;
+
+ bindService(startIntent);
+
+ mAssistant.mSettings.mDismissToViewRatioLimit = 0.8f;
+ mAssistant.mSettings.mStreakLimit = 2;
+ mAssistant.mSettings.mNewInterruptionModel = true;
mAssistant.setNoMan(mNoMan);
mAssistant.setFile(mFile);
mAssistant.setPackageManager(mPackageManager);
+
ApplicationInfo info = mock(ApplicationInfo.class);
when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt()))
.thenReturn(info);
@@ -408,6 +411,8 @@
mAssistant.writeXml(serializer);
Assistant assistant = new Assistant();
+ // onCreate is not invoked, so settings won't be initialised, unless we do it here.
+ assistant.mSettings = mAssistant.mSettings;
assistant.readXml(new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())));
assertEquals(ci1, assistant.getImpressions(key1));
@@ -417,8 +422,6 @@
@Test
public void testSettingsProviderUpdate() {
- ContentResolver resolver = mApplication.getContentResolver();
-
// Set up channels
String key = mAssistant.getKey("pkg1", 1, "channel1");
ChannelImpressions ci = new ChannelImpressions();
@@ -435,19 +438,11 @@
assertEquals(false, ci.shouldTriggerBlock());
// Update settings values.
- float newDismissToViewRatioLimit = 0f;
- int newStreakLimit = 0;
- Settings.Global.putFloat(resolver,
- Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
- newDismissToViewRatioLimit);
- Settings.Global.putInt(resolver,
- Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, newStreakLimit);
+ mAssistant.mSettings.mDismissToViewRatioLimit = 0f;
+ mAssistant.mSettings.mStreakLimit = 0;
// Notify for the settings values we updated.
- mAssistant.mSettingsObserver.onChange(false, Settings.Global.getUriFor(
- Settings.Global.BLOCKING_HELPER_STREAK_LIMIT));
- mAssistant.mSettingsObserver.onChange(false, Settings.Global.getUriFor(
- Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT));
+ mAssistant.mSettings.mOnUpdateRunnable.run();
// With the new threshold, the blocking helper should be triggered.
assertEquals(true, ci.shouldTriggerBlock());
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index cc17b25..0126e7e5 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -18,6 +18,7 @@
"SettingsLibSettingsSpinner",
"SettingsLayoutPreference",
"ActionButtonsPreference",
+ "SettingsLibEntityHeaderWidgets",
],
// ANDROIDMK TRANSLATION ERROR: unsupported assignment to LOCAL_SHARED_JAVA_LIBRARIES
diff --git a/packages/SettingsLib/EntityHeaderWidgets/Android.bp b/packages/SettingsLib/EntityHeaderWidgets/Android.bp
new file mode 100644
index 0000000..3ca4ecd
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/Android.bp
@@ -0,0 +1,14 @@
+android_library {
+ name: "SettingsLibEntityHeaderWidgets",
+
+ srcs: ["src/**/*.java"],
+ resource_dirs: ["res"],
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "SettingsLibAppPreference"
+ ],
+
+ sdk_version: "system_current",
+ min_sdk_version: "21",
+}
diff --git a/packages/SettingsLib/EntityHeaderWidgets/AndroidManifest.xml b/packages/SettingsLib/EntityHeaderWidgets/AndroidManifest.xml
new file mode 100644
index 0000000..4b9f1ab
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?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.settingslib.widget">
+
+ <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_entities_header.xml b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_entities_header.xml
new file mode 100644
index 0000000..9f30eda
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_entities_header.xml
@@ -0,0 +1,64 @@
+<?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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/app_entities_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="24dp"
+ android:paddingEnd="8dp"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/header_title"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:gravity="center"
+ android:textAppearance="@style/AppEntitiesHeader.Text.HeaderTitle"/>
+
+ <LinearLayout
+ android:id="@+id/all_apps_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:gravity="center">
+
+ <include
+ android:id="@+id/app1_view"
+ layout="@layout/app_view"/>
+
+ <include
+ android:id="@+id/app2_view"
+ layout="@layout/app_view"/>
+
+ <include
+ android:id="@+id/app3_view"
+ layout="@layout/app_view"/>
+
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/header_details"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:gravity="center"/>
+
+</LinearLayout>
diff --git a/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_view.xml b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_view.xml
new file mode 100644
index 0000000..fcafa31
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_view.xml
@@ -0,0 +1,50 @@
+<?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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginEnd="16dp"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/app_icon"
+ android:layout_width="@dimen/secondary_app_icon_size"
+ android:layout_height="@dimen/secondary_app_icon_size"
+ android:layout_marginBottom="12dp"/>
+
+ <TextView
+ android:id="@+id/app_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="2dp"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="@style/AppEntitiesHeader.Text.Title"/>
+
+ <TextView
+ android:id="@+id/app_summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="@style/AppEntitiesHeader.Text.Summary"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/EntityHeaderWidgets/res/values/styles.xml b/packages/SettingsLib/EntityHeaderWidgets/res/values/styles.xml
new file mode 100644
index 0000000..0eefd4b
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/res/values/styles.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <style name="AppEntitiesHeader.Text"
+ parent="@android:style/TextAppearance.Material.Subhead">
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+
+ <style name="AppEntitiesHeader.Text.HeaderTitle">
+ <item name="android:textSize">14sp</item>
+ </style>
+
+ <style name="AppEntitiesHeader.Text.Title">
+ <item name="android:textSize">16sp</item>
+ </style>
+
+ <style name="AppEntitiesHeader.Text.Summary"
+ parent="@android:style/TextAppearance.Material.Body1">
+ <item name="android:textColor">?android:attr/textColorSecondary</item>
+ <item name="android:textSize">14sp</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/EntityHeaderWidgets/src/com/android/settingslib/widget/AppEntitiesHeaderController.java b/packages/SettingsLib/EntityHeaderWidgets/src/com/android/settingslib/widget/AppEntitiesHeaderController.java
new file mode 100644
index 0000000..8ccf89f
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/src/com/android/settingslib/widget/AppEntitiesHeaderController.java
@@ -0,0 +1,267 @@
+/*
+ * 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.settingslib.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * This is used to initialize view which was inflated
+ * from {@link R.xml.app_entities_header.xml}.
+ *
+ * <p>The view looks like below.
+ *
+ * <pre>
+ * --------------------------------------------------------------
+ * | Header title |
+ * --------------------------------------------------------------
+ * | App1 icon | App2 icon | App3 icon |
+ * | App1 title | App2 title | App3 title |
+ * | App1 summary | App2 summary | App3 summary |
+ * |-------------------------------------------------------------
+ * | Header details |
+ * --------------------------------------------------------------
+ * </pre>
+ *
+ * <p>How to use AppEntitiesHeaderController?
+ *
+ * <p>1. Add a {@link LayoutPreference} in layout XML file.
+ * <pre>
+ * <com.android.settingslib.widget.LayoutPreference
+ * android:key="app_entities_header"
+ * android:layout="@layout/app_entities_header"/>
+ * </pre>
+ *
+ * <p>2. Use AppEntitiesHeaderController to call below methods, then you can initialize
+ * view of <code>app_entities_header</code>.
+ *
+ * <pre>
+ *
+ * View headerView = ((LayoutPreference) screen.findPreference("app_entities_header"))
+ * .findViewById(R.id.app_entities_header);
+ *
+ * AppEntitiesHeaderController.newInstance(context, headerView)
+ * .setHeaderTitleRes(R.string.xxxxx)
+ * .setHeaderDetailsRes(R.string.xxxxx)
+ * .setHeaderDetailsClickListener(onClickListener)
+ * .setAppEntity(0, icon, "app title", "app summary")
+ * .setAppEntity(1, icon, "app title", "app summary")
+ * .setAppEntity(2, icon, "app title", "app summary")
+ * .apply();
+ * </pre>
+ */
+public class AppEntitiesHeaderController {
+
+ private static final String TAG = "AppEntitiesHeaderCtl";
+
+ @VisibleForTesting
+ static final int MAXIMUM_APPS = 3;
+
+ private final Context mContext;
+ private final TextView mHeaderTitleView;
+ private final Button mHeaderDetailsView;
+
+ private final AppEntity[] mAppEntities;
+ private final View[] mAppEntityViews;
+ private final ImageView[] mAppIconViews;
+ private final TextView[] mAppTitleViews;
+ private final TextView[] mAppSummaryViews;
+
+ private int mHeaderTitleRes;
+ private int mHeaderDetailsRes;
+ private View.OnClickListener mDetailsOnClickListener;
+
+ /**
+ * Creates a new instance of the controller.
+ *
+ * @param context the Context the view is running in
+ * @param appEntitiesHeaderView view was inflated from <code>app_entities_header</code>
+ */
+ public static AppEntitiesHeaderController newInstance(@NonNull Context context,
+ @NonNull View appEntitiesHeaderView) {
+ return new AppEntitiesHeaderController(context, appEntitiesHeaderView);
+ }
+
+ private AppEntitiesHeaderController(Context context, View appEntitiesHeaderView) {
+ mContext = context;
+ mHeaderTitleView = appEntitiesHeaderView.findViewById(R.id.header_title);
+ mHeaderDetailsView = appEntitiesHeaderView.findViewById(R.id.header_details);
+
+ mAppEntities = new AppEntity[MAXIMUM_APPS];
+ mAppIconViews = new ImageView[MAXIMUM_APPS];
+ mAppTitleViews = new TextView[MAXIMUM_APPS];
+ mAppSummaryViews = new TextView[MAXIMUM_APPS];
+
+ mAppEntityViews = new View[]{
+ appEntitiesHeaderView.findViewById(R.id.app1_view),
+ appEntitiesHeaderView.findViewById(R.id.app2_view),
+ appEntitiesHeaderView.findViewById(R.id.app3_view)
+ };
+
+ // Initialize view in advance, so we won't take too much time to do it when controller is
+ // binding view.
+ for (int index = 0; index < MAXIMUM_APPS; index++) {
+ final View appView = mAppEntityViews[index];
+ mAppIconViews[index] = (ImageView) appView.findViewById(R.id.app_icon);
+ mAppTitleViews[index] = (TextView) appView.findViewById(R.id.app_title);
+ mAppSummaryViews[index] = (TextView) appView.findViewById(R.id.app_summary);
+ }
+ }
+
+ /**
+ * Set the text resource for app entities header title.
+ */
+ public AppEntitiesHeaderController setHeaderTitleRes(@StringRes int titleRes) {
+ mHeaderTitleRes = titleRes;
+ return this;
+ }
+
+ /**
+ * Set the text resource for app entities header details.
+ */
+ public AppEntitiesHeaderController setHeaderDetailsRes(@StringRes int detailsRes) {
+ mHeaderDetailsRes = detailsRes;
+ return this;
+ }
+
+ /**
+ * Register a callback to be invoked when header details view is clicked.
+ */
+ public AppEntitiesHeaderController setHeaderDetailsClickListener(
+ @Nullable View.OnClickListener clickListener) {
+ mDetailsOnClickListener = clickListener;
+ return this;
+ }
+
+ /**
+ * Set an app entity at a specified position view.
+ *
+ * @param index the index at which the specified view is to be inserted
+ * @param icon the icon of app entity
+ * @param titleRes the title of app entity
+ * @param summaryRes the summary of app entity
+ * @return this {@code AppEntitiesHeaderController} object
+ */
+ public AppEntitiesHeaderController setAppEntity(int index, @NonNull Drawable icon,
+ @Nullable CharSequence titleRes, @Nullable CharSequence summaryRes) {
+ final AppEntity appEntity = new AppEntity(icon, titleRes, summaryRes);
+ mAppEntities[index] = appEntity;
+ return this;
+ }
+
+ /**
+ * Remove an app entity at a specified position view.
+ *
+ * @param index the index at which the specified view is to be removed
+ * @return this {@code AppEntitiesHeaderController} object
+ */
+ public AppEntitiesHeaderController removeAppEntity(int index) {
+ mAppEntities[index] = null;
+ return this;
+ }
+
+ /**
+ * Clear all app entities in app entities header.
+ *
+ * @return this {@code AppEntitiesHeaderController} object
+ */
+ public AppEntitiesHeaderController clearAllAppEntities() {
+ for (int index = 0; index < MAXIMUM_APPS; index++) {
+ removeAppEntity(index);
+ }
+ return this;
+ }
+
+ /**
+ * Done mutating app entities header, rebinds everything.
+ */
+ public void apply() {
+ bindHeaderTitleView();
+ bindHeaderDetailsView();
+
+ // Rebind all apps view
+ for (int index = 0; index < MAXIMUM_APPS; index++) {
+ bindAppEntityView(index);
+ }
+ }
+
+ private void bindHeaderTitleView() {
+ CharSequence titleText = "";
+ try {
+ titleText = mContext.getText(mHeaderTitleRes);
+ } catch (Resources.NotFoundException e) {
+ Log.e(TAG, "Resource of header title can't not be found!", e);
+ }
+ mHeaderTitleView.setText(titleText);
+ mHeaderTitleView.setVisibility(
+ TextUtils.isEmpty(titleText) ? View.GONE : View.VISIBLE);
+ }
+
+ private void bindHeaderDetailsView() {
+ CharSequence detailsText = "";
+ try {
+ detailsText = mContext.getText(mHeaderDetailsRes);
+ } catch (Resources.NotFoundException e) {
+ Log.e(TAG, "Resource of header details can't not be found!", e);
+ }
+ mHeaderDetailsView.setText(detailsText);
+ mHeaderDetailsView.setVisibility(
+ TextUtils.isEmpty(detailsText) ? View.GONE : View.VISIBLE);
+ mHeaderDetailsView.setOnClickListener(mDetailsOnClickListener);
+ }
+
+ private void bindAppEntityView(int index) {
+ final AppEntity appEntity = mAppEntities[index];
+ mAppEntityViews[index].setVisibility(appEntity != null ? View.VISIBLE : View.GONE);
+
+ if (appEntity != null) {
+ mAppIconViews[index].setImageDrawable(appEntity.icon);
+
+ mAppTitleViews[index].setVisibility(
+ TextUtils.isEmpty(appEntity.title) ? View.INVISIBLE : View.VISIBLE);
+ mAppTitleViews[index].setText(appEntity.title);
+
+ mAppSummaryViews[index].setVisibility(
+ TextUtils.isEmpty(appEntity.summary) ? View.INVISIBLE : View.VISIBLE);
+ mAppSummaryViews[index].setText(appEntity.summary);
+ }
+ }
+
+ private static class AppEntity {
+ public final Drawable icon;
+ public final CharSequence title;
+ public final CharSequence summary;
+
+ AppEntity(Drawable appIcon, CharSequence appTitle, CharSequence appSummary) {
+ icon = appIcon;
+ title = appTitle;
+ summary = appSummary;
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/PermissionsSummaryHelper.java b/packages/SettingsLib/src/com/android/settingslib/applications/PermissionsSummaryHelper.java
index f2b97b4..2387b01 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/PermissionsSummaryHelper.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/PermissionsSummaryHelper.java
@@ -15,7 +15,6 @@
*/
package com.android.settingslib.applications;
-import android.annotation.NonNull;
import android.content.Context;
import android.permission.RuntimePermissionPresentationInfo;
import android.permission.RuntimePermissionPresenter;
@@ -31,37 +30,33 @@
final PermissionsResultCallback callback) {
final RuntimePermissionPresenter presenter =
RuntimePermissionPresenter.getInstance(context);
- presenter.getAppPermissions(pkg, new RuntimePermissionPresenter.OnResultCallback() {
- @Override
- public void onGetAppPermissions(
- @NonNull List<RuntimePermissionPresentationInfo> permissions) {
- final int permissionCount = permissions.size();
+ presenter.getAppPermissions(pkg, permissions -> {
+ final int permissionCount = permissions.size();
- int grantedStandardCount = 0;
- int grantedAdditionalCount = 0;
- int requestedCount = 0;
- List<CharSequence> grantedStandardLabels = new ArrayList<>();
+ int grantedStandardCount = 0;
+ int grantedAdditionalCount = 0;
+ int requestedCount = 0;
+ List<CharSequence> grantedStandardLabels = new ArrayList<>();
- for (int i = 0; i < permissionCount; i++) {
- RuntimePermissionPresentationInfo permission = permissions.get(i);
- requestedCount++;
- if (permission.isGranted()) {
- if (permission.isStandard()) {
- grantedStandardLabels.add(permission.getLabel());
- grantedStandardCount++;
- } else {
- grantedAdditionalCount++;
- }
+ for (int i = 0; i < permissionCount; i++) {
+ RuntimePermissionPresentationInfo permission = permissions.get(i);
+ requestedCount++;
+ if (permission.isGranted()) {
+ if (permission.isStandard()) {
+ grantedStandardLabels.add(permission.getLabel());
+ grantedStandardCount++;
+ } else {
+ grantedAdditionalCount++;
}
}
-
- Collator collator = Collator.getInstance();
- collator.setStrength(Collator.PRIMARY);
- Collections.sort(grantedStandardLabels, collator);
-
- callback.onPermissionSummaryResult(grantedStandardCount, requestedCount,
- grantedAdditionalCount, grantedStandardLabels);
}
+
+ Collator collator = Collator.getInstance();
+ collator.setStrength(Collator.PRIMARY);
+ Collections.sort(grantedStandardLabels, collator);
+
+ callback.onPermissionSummaryResult(grantedStandardCount, requestedCount,
+ grantedAdditionalCount, grantedStandardLabels);
}, null);
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppEntitiesHeaderControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppEntitiesHeaderControllerTest.java
new file mode 100644
index 0000000..c3bc8da
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppEntitiesHeaderControllerTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.settingslib.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class AppEntitiesHeaderControllerTest {
+
+ private static final CharSequence TITLE = "APP_TITLE";
+ private static final CharSequence SUMMARY = "APP_SUMMARY";
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ private Context mContext;
+ private Drawable mIcon;
+ private View mAppEntitiesHeaderView;
+ private AppEntitiesHeaderController mController;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mAppEntitiesHeaderView = LayoutInflater.from(mContext).inflate(
+ R.layout.app_entities_header, null /* root */);
+ mIcon = mContext.getDrawable(R.drawable.ic_menu);
+ mController = AppEntitiesHeaderController.newInstance(mContext,
+ mAppEntitiesHeaderView);
+ }
+
+ @Test
+ public void assert_amountOfMaximumAppsAreThree() {
+ assertThat(AppEntitiesHeaderController.MAXIMUM_APPS).isEqualTo(3);
+ }
+
+ @Test
+ public void setHeaderTitleRes_setTextRes_shouldSetToTitleView() {
+ mController.setHeaderTitleRes(R.string.expand_button_title).apply();
+ final TextView view = mAppEntitiesHeaderView.findViewById(R.id.header_title);
+
+ assertThat(view.getText()).isEqualTo(mContext.getText(R.string.expand_button_title));
+ }
+
+ @Test
+ public void setHeaderDetailsRes_setTextRes_shouldSetToDetailsView() {
+ mController.setHeaderDetailsRes(R.string.expand_button_title).apply();
+ final TextView view = mAppEntitiesHeaderView.findViewById(R.id.header_details);
+
+ assertThat(view.getText()).isEqualTo(mContext.getText(R.string.expand_button_title));
+ }
+
+ @Test
+ public void setHeaderDetailsClickListener_setClickListener_detailsViewAttachClickListener() {
+ mController.setHeaderDetailsClickListener(v -> {
+ }).apply();
+ final TextView view = mAppEntitiesHeaderView.findViewById(R.id.header_details);
+
+ assertThat(view.hasOnClickListeners()).isTrue();
+ }
+
+ @Test
+ public void setAppEntity_indexLessThanZero_shouldThrowArrayIndexOutOfBoundsException() {
+ thrown.expect(ArrayIndexOutOfBoundsException.class);
+
+ mController.setAppEntity(-1, mIcon, TITLE, SUMMARY);
+ }
+
+ @Test
+ public void asetAppEntity_indexGreaterThanMaximum_shouldThrowArrayIndexOutOfBoundsException() {
+ thrown.expect(ArrayIndexOutOfBoundsException.class);
+
+ mController.setAppEntity(AppEntitiesHeaderController.MAXIMUM_APPS + 1, mIcon, TITLE,
+ SUMMARY);
+ }
+
+ @Test
+ public void setAppEntity_addAppToIndex0_shouldShowAppView1() {
+ mController.setAppEntity(0, mIcon, TITLE, SUMMARY).apply();
+ final View app1View = mAppEntitiesHeaderView.findViewById(R.id.app1_view);
+ final ImageView appIconView = app1View.findViewById(R.id.app_icon);
+ final TextView appTitle = app1View.findViewById(R.id.app_title);
+ final TextView appSummary = app1View.findViewById(R.id.app_summary);
+
+ assertThat(app1View.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(appIconView.getDrawable()).isNotNull();
+ assertThat(appTitle.getText()).isEqualTo(TITLE);
+ assertThat(appSummary.getText()).isEqualTo(SUMMARY);
+ }
+
+ @Test
+ public void setAppEntity_addAppToIndex1_shouldShowAppView2() {
+ mController.setAppEntity(1, mIcon, TITLE, SUMMARY).apply();
+ final View app2View = mAppEntitiesHeaderView.findViewById(R.id.app2_view);
+ final ImageView appIconView = app2View.findViewById(R.id.app_icon);
+ final TextView appTitle = app2View.findViewById(R.id.app_title);
+ final TextView appSummary = app2View.findViewById(R.id.app_summary);
+
+ assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(appIconView.getDrawable()).isNotNull();
+ assertThat(appTitle.getText()).isEqualTo(TITLE);
+ assertThat(appSummary.getText()).isEqualTo(SUMMARY);
+ }
+
+ @Test
+ public void setAppEntity_addAppToIndex2_shouldShowAppView3() {
+ mController.setAppEntity(2, mIcon, TITLE, SUMMARY).apply();
+ final View app3View = mAppEntitiesHeaderView.findViewById(R.id.app3_view);
+ final ImageView appIconView = app3View.findViewById(R.id.app_icon);
+ final TextView appTitle = app3View.findViewById(R.id.app_title);
+ final TextView appSummary = app3View.findViewById(R.id.app_summary);
+
+ assertThat(app3View.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(appIconView.getDrawable()).isNotNull();
+ assertThat(appTitle.getText()).isEqualTo(TITLE);
+ assertThat(appSummary.getText()).isEqualTo(SUMMARY);
+ }
+
+ @Test
+ public void removeAppEntity_removeIndex0_shouldNotShowAppView1() {
+ mController.setAppEntity(0, mIcon, TITLE, SUMMARY)
+ .setAppEntity(1, mIcon, TITLE, SUMMARY).apply();
+ final View app1View = mAppEntitiesHeaderView.findViewById(R.id.app1_view);
+ final View app2View = mAppEntitiesHeaderView.findViewById(R.id.app2_view);
+
+ assertThat(app1View.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE);
+
+ mController.removeAppEntity(0).apply();
+
+ assertThat(app1View.getVisibility()).isEqualTo(View.GONE);
+ assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void clearAllAppEntities_shouldNotShowAllAppViews() {
+ mController.setAppEntity(0, mIcon, TITLE, SUMMARY)
+ .setAppEntity(1, mIcon, TITLE, SUMMARY)
+ .setAppEntity(2, mIcon, TITLE, SUMMARY).apply();
+ final View app1View = mAppEntitiesHeaderView.findViewById(R.id.app1_view);
+ final View app2View = mAppEntitiesHeaderView.findViewById(R.id.app2_view);
+ final View app3View = mAppEntitiesHeaderView.findViewById(R.id.app3_view);
+
+ assertThat(app1View.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(app3View.getVisibility()).isEqualTo(View.VISIBLE);
+
+ mController.clearAllAppEntities().apply();
+ assertThat(app1View.getVisibility()).isEqualTo(View.GONE);
+ assertThat(app2View.getVisibility()).isEqualTo(View.GONE);
+ assertThat(app3View.getVisibility()).isEqualTo(View.GONE);
+ }
+}
diff --git a/packages/SystemUI/res/layout/bubble_expanded_view.xml b/packages/SystemUI/res/layout/bubble_expanded_view.xml
new file mode 100644
index 0000000..b2307e7
--- /dev/null
+++ b/packages/SystemUI/res/layout/bubble_expanded_view.xml
@@ -0,0 +1,30 @@
+<?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
+ -->
+<com.android.systemui.bubbles.BubbleExpandedViewContainer
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:id="@+id/bubble_expanded_view">
+
+ <!-- TODO: header -->
+
+ <View
+ android:id="@+id/pointer_view"
+ android:layout_width="@dimen/bubble_pointer_width"
+ android:layout_height="@dimen/bubble_pointer_height"
+ />
+</com.android.systemui.bubbles.BubbleExpandedViewContainer>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index b6c9b8c..3851190 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -361,6 +361,7 @@
<!-- The height of the qs customize header. Should be (28dp - qs_tile_margin_top_bottom). -->
<dimen name="qs_customize_header_min_height">40dp</dimen>
<dimen name="qs_tile_margin_top">18dp</dimen>
+ <dimen name="qs_tile_background_size">40dp</dimen>
<dimen name="qs_quick_tile_size">48dp</dimen>
<!-- Maximum width of quick quick settings panel. Defaults to MATCH_PARENT-->
<dimen name="qs_quick_layout_width">-1px</dimen>
@@ -993,4 +994,10 @@
<dimen name="bubble_icon_size">24dp</dimen>
<!-- Default height of the expanded view shown when the bubble is expanded -->
<dimen name="bubble_expanded_default_height">400dp</dimen>
+ <!-- Height of the triangle that points to the expanded bubble -->
+ <dimen name="bubble_pointer_height">4dp</dimen>
+ <!-- Width of the triangle that points to the expanded bubble -->
+ <dimen name="bubble_pointer_width">6dp</dimen>
+ <!-- Extra padding around the dismiss target for bubbles -->
+ <dimen name="bubble_dismiss_slop">16dp</dimen>
</resources>
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedViewContainer.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedViewContainer.java
new file mode 100644
index 0000000..e28d96b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedViewContainer.java
@@ -0,0 +1,102 @@
+/*
+ * 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.systemui.bubbles;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.drawable.ShapeDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.systemui.R;
+import com.android.systemui.recents.TriangleShape;
+
+/**
+ * Container for the expanded bubble view, handles rendering the caret and header of the view.
+ */
+public class BubbleExpandedViewContainer extends LinearLayout {
+
+ // The triangle pointing to the expanded view
+ private View mPointerView;
+ // The view that is being displayed for the expanded state
+ private View mExpandedView;
+
+ public BubbleExpandedViewContainer(Context context) {
+ this(context, null);
+ }
+
+ public BubbleExpandedViewContainer(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BubbleExpandedViewContainer(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public BubbleExpandedViewContainer(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ setOrientation(VERTICAL);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ Resources res = getResources();
+ mPointerView = findViewById(R.id.pointer_view);
+ int width = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
+ int height = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
+ ShapeDrawable triangleDrawable = new ShapeDrawable(
+ TriangleShape.create(width, height, true /* pointUp */));
+ triangleDrawable.setTint(Color.WHITE); // TODO: dark mode
+ mPointerView.setBackground(triangleDrawable);
+ }
+
+ /**
+ * Set the x position that the tip of the triangle should point to.
+ */
+ public void setPointerPosition(int x) {
+ // Adjust for the pointer size
+ x -= (mPointerView.getWidth() / 2);
+ mPointerView.setTranslationX(x);
+ }
+
+ /**
+ * Set the view to display for the expanded state. Passing null will clear the view.
+ */
+ public void setExpandedView(View view) {
+ if (mExpandedView != null) {
+ removeView(mExpandedView);
+ }
+ mExpandedView = view;
+ if (mExpandedView != null) {
+ addView(mExpandedView);
+ }
+ }
+
+ /**
+ * @return the view containing the expanded content, can be null.
+ */
+ @Nullable
+ public View getExpandedView() {
+ return mExpandedView;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index e395c4c..dfd18b2 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -25,6 +25,7 @@
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.RectF;
+import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -52,7 +53,7 @@
private Point mDisplaySize;
private FrameLayout mBubbleContainer;
- private FrameLayout mExpandedViewContainer;
+ private BubbleExpandedViewContainer mExpandedViewContainer;
private int mBubbleSize;
private int mBubblePadding;
@@ -111,7 +112,9 @@
int padding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
- mExpandedViewContainer = new FrameLayout(context);
+ mExpandedViewContainer = (BubbleExpandedViewContainer)
+ LayoutInflater.from(context).inflate(R.layout.bubble_expanded_view,
+ this /* parent */, false /* attachToRoot */);
mExpandedViewContainer.setElevation(elevation);
mExpandedViewContainer.setPadding(padding, padding, padding, padding);
mExpandedViewContainer.setClipChildren(false);
@@ -390,16 +393,19 @@
ExpandableNotificationRow row = mExpandedBubble.getRowView();
if (!row.equals(mExpandedViewContainer.getChildAt(0))) {
// Different expanded view than what we have
- mExpandedViewContainer.removeAllViews();
+ mExpandedViewContainer.setExpandedView(null);
}
- mExpandedViewContainer.addView(row);
+ int pointerPosition = mExpandedBubble.getPosition().x
+ + (mExpandedBubble.getWidth() / 2);
+ mExpandedViewContainer.setPointerPosition(pointerPosition);
+ mExpandedViewContainer.setExpandedView(row);
}
}
private void applyCurrentState() {
mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
if (!mIsExpanded) {
- mExpandedViewContainer.removeAllViews();
+ mExpandedViewContainer.setExpandedView(null);
} else {
mExpandedViewContainer.setTranslationY(mBubbleContainer.getHeight());
ExpandableNotificationRow row = mExpandedBubble.getRowView();
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipDismissViewController.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipDismissViewController.java
index 8262b54..8224365 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipDismissViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipDismissViewController.java
@@ -51,6 +51,8 @@
// Used for dismissing a bubble -- bubble should be in the target to be considered a dismiss
private View mTargetView;
+ private int mTargetSlop;
+ private Point mWindowSize;
private int[] mLoc = new int[2];
private boolean mIntersecting;
private Vibrator mVibe;
@@ -69,12 +71,14 @@
// Determine sizes for the view
final Rect stableInsets = new Rect();
WindowManagerWrapper.getInstance().getStableInsets(stableInsets);
- final Point windowSize = new Point();
- mWindowManager.getDefaultDisplay().getRealSize(windowSize);
+ mWindowSize = new Point();
+ mWindowManager.getDefaultDisplay().getRealSize(mWindowSize);
final int gradientHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.pip_dismiss_gradient_height);
final int bottomMargin = mContext.getResources().getDimensionPixelSize(
R.dimen.pip_dismiss_text_bottom_margin);
+ mTargetSlop = mContext.getResources().getDimensionPixelSize(
+ R.dimen.bubble_dismiss_slop);
// Create a new view for the dismiss target
LayoutInflater inflater = LayoutInflater.from(mContext);
@@ -96,7 +100,7 @@
// Add the target to the window
LayoutParams lp = new LayoutParams(
LayoutParams.MATCH_PARENT, gradientHeight,
- 0, windowSize.y - gradientHeight,
+ 0, mWindowSize.y - gradientHeight,
LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
LayoutParams.FLAG_LAYOUT_IN_SCREEN
| LayoutParams.FLAG_NOT_TOUCHABLE
@@ -112,7 +116,7 @@
/**
- * Updates the dismiss target based on location of the view.
+ * Updates the dismiss target based on location of the view, only used for bubbles not for PIP.
*
* @return whether the view is within the dismiss target.
*/
@@ -127,11 +131,13 @@
mTargetView.getLocationOnScreen(mLoc);
Rect targetRect = new Rect(mLoc[0], mLoc[1], mLoc[0] + mTargetView.getWidth(),
mLoc[1] + mTargetView.getHeight());
+ expandRect(targetRect, mTargetSlop);
boolean intersecting = targetRect.intersect(viewRect);
- if (intersecting && !mIntersecting) {
+ if (intersecting != mIntersecting) {
// TODO: is this the right effect?
- mVibe.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
- mIntersecting = true;
+ mVibe.vibrate(VibrationEffect.get(intersecting
+ ? VibrationEffect.EFFECT_CLICK
+ : VibrationEffect.EFFECT_TICK));
}
mIntersecting = intersecting;
return intersecting;
@@ -139,7 +145,6 @@
return false;
}
-
/**
* Shows the dismiss target.
*/
@@ -172,4 +177,11 @@
.start();
}
}
+
+ private void expandRect(Rect outRect, int expandAmount) {
+ outRect.left = Math.max(0, outRect.left - expandAmount);
+ outRect.top = Math.max(0, outRect.top - expandAmount);
+ outRect.right = Math.min(mWindowSize.x, outRect.right + expandAmount);
+ outRect.bottom = Math.min(mWindowSize.y, outRect.bottom + expandAmount);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java
index 3a96595d..32fd2dc 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java
@@ -19,14 +19,19 @@
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
+import android.graphics.Path;
+import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.PathShape;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.service.quicksettings.Tile;
import android.text.TextUtils;
import android.util.Log;
+import android.util.PathParser;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
@@ -46,6 +51,7 @@
public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
private static final String TAG = "QSTileBaseView";
+ private static final int ICON_MASK_ID = com.android.internal.R.string.config_icon_mask;
private final H mHandler = new H();
private final int[] mLocInScreen = new int[2];
private final FrameLayout mIconFrame;
@@ -62,6 +68,7 @@
private final int mColorInactive;
private final int mColorDisabled;
private int mCircleColor;
+ private int mBgSize;
public QSTileBaseView(Context context, QSIconView icon) {
this(context, icon, false);
@@ -71,15 +78,23 @@
super(context);
// Default to Quick Tile padding, and QSTileView will specify its own padding.
int padding = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_padding);
-
mIconFrame = new FrameLayout(context);
mIconFrame.setForegroundGravity(Gravity.CENTER);
int size = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size);
addView(mIconFrame, new LayoutParams(size, size));
mBg = new ImageView(getContext());
+ Path path = new Path(PathParser.createPathFromPathData(
+ context.getResources().getString(ICON_MASK_ID)));
+ float pathSize = AdaptiveIconDrawable.MASK_SIZE;
+ PathShape p = new PathShape(path, pathSize, pathSize);
+ ShapeDrawable d = new ShapeDrawable(p);
+ int bgSize = context.getResources().getDimensionPixelSize(R.dimen.qs_tile_background_size);
+ d.setIntrinsicHeight(bgSize);
+ d.setIntrinsicWidth(bgSize);
mBg.setScaleType(ScaleType.FIT_CENTER);
- mBg.setImageResource(R.drawable.ic_qs_circle);
- mIconFrame.addView(mBg);
+ mBg.setImageDrawable(d);
+ mIconFrame.addView(mBg, ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT);
mIcon = icon;
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@@ -107,7 +122,7 @@
setFocusable(true);
}
- public View getBgCicle() {
+ public View getBgCircle() {
return mBg;
}
@@ -303,6 +318,7 @@
private class H extends Handler {
private static final int STATE_CHANGED = 1;
+
public H() {
super(Looper.getMainLooper());
}
@@ -314,4 +330,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 20c4816..b1fa6a5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -21,7 +21,6 @@
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
-import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
@@ -60,10 +59,11 @@
private ViewGroup mTransientContainer;
private boolean mInShelf;
private boolean mTransformingInShelf;
- @Nullable private ExpandableViewState mViewState;
+ private final ExpandableViewState mViewState;
public ExpandableView(Context context, AttributeSet attrs) {
super(context, attrs);
+ mViewState = createExpandableViewState();
}
@Override
@@ -536,11 +536,6 @@
/** Sets {@link ExpandableViewState} to default state. */
public ExpandableViewState resetViewState() {
- // TODO(http://b/119762423): Move the null check to getViewState().
- if (mViewState == null) {
- mViewState = createExpandableViewState();
- }
-
// initialize with the default values of the view
mViewState.height = getIntrinsicHeight();
mViewState.gone = getVisibility() == View.GONE;
@@ -573,9 +568,7 @@
/** Applies internal {@link ExpandableViewState} to this view. */
public void applyViewState() {
- if (mViewState == null) {
- Log.wtf(TAG, "No child state was found when applying this state to the hostView");
- } else if (!mViewState.gone) {
+ if (!mViewState.gone) {
mViewState.applyToView(this);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java
index 9eb5737a..0cec637 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java
@@ -296,13 +296,15 @@
} else if (exceededSwipeHorizontalTouchSlop) {
if (mDragHPositive ? (posH < touchDownH) : (posH > touchDownH)) {
// Swiping left (rtl) gesture
- int index = isEdgeSwipeAlongNavBar(touchDownH, !mDragHPositive)
+ int index = mGestureActions[ACTION_SWIPE_LEFT_FROM_EDGE_INDEX] != null
+ && isEdgeSwipeAlongNavBar(touchDownH, !mDragHPositive)
? ACTION_SWIPE_LEFT_FROM_EDGE_INDEX : ACTION_SWIPE_LEFT_INDEX;
tryToStartGesture(mGestureActions[index], true /* alignedWithNavBar */,
event);
} else {
// Swiping right (ltr) gesture
- int index = isEdgeSwipeAlongNavBar(touchDownH, mDragHPositive)
+ int index = mGestureActions[ACTION_SWIPE_RIGHT_FROM_EDGE_INDEX] != null
+ && isEdgeSwipeAlongNavBar(touchDownH, mDragHPositive)
? ACTION_SWIPE_RIGHT_FROM_EDGE_INDEX : ACTION_SWIPE_RIGHT_INDEX;
tryToStartGesture(mGestureActions[index], true /* alignedWithNavBar */,
event);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java
index abb8c79..2805908 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java
@@ -601,6 +601,25 @@
assertGestureDragsHitTargetAllDirections(buttonView, true /* isRTL */, NAV_BAR_LEFT);
}
+ @Test
+ public void testNoEdgeActionsTriggerNormalActions() {
+ doReturn(NAVBAR_WIDTH).when(mNavigationBarView).getWidth();
+ doReturn(NAVBAR_HEIGHT).when(mNavigationBarView).getHeight();
+
+ NavigationGestureAction swipeUp = mockAction(true);
+ NavigationGestureAction swipeDown = mockAction(true);
+ NavigationGestureAction swipeLeft = mockAction(true);
+ NavigationGestureAction swipeRight = mockAction(true);
+ mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight,
+ null /* swipeLeftFromEdgeAction */,
+ null /* swipeLeftFromEdgeAction */);
+
+ // Swipe Left from Edge
+ assertGestureTriggersAction(swipeLeft, NAVBAR_WIDTH, 1, 5, 1);
+ // Swipe Right from Edge
+ assertGestureTriggersAction(swipeRight, 0, 1, NAVBAR_WIDTH, 5);
+ }
+
private void assertGestureDragsHitTargetAllDirections(View buttonView, boolean isRTL,
int navPos) {
mController.setBarState(isRTL, navPos);
@@ -665,7 +684,6 @@
reset(buttonView);
}
-
private MotionEvent event(int action, float x, float y) {
final MotionEvent event = mock(MotionEvent.class);
doReturn(x).when(event).getX();
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index e8887e7..612c929 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -484,15 +484,15 @@
}
// Called by Shell command.
- void getScore(@Nullable String algorithmName, @NonNull String value1,
+ void calculateScore(@Nullable String algorithmName, @NonNull String value1,
@NonNull String value2, @NonNull RemoteCallback callback) {
enforceCallingPermissionForManagement();
final FieldClassificationStrategy strategy =
new FieldClassificationStrategy(getContext(), UserHandle.USER_CURRENT);
- strategy.getScores(callback, algorithmName, null,
- Arrays.asList(AutofillValue.forText(value1)), new String[] { value2 });
+ strategy.calculateScores(callback, Arrays.asList(AutofillValue.forText(value1)),
+ new String[] { value2 }, null, algorithmName, null, null, null);
}
// Called by Shell command.
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 5a0d12c..fa62ef8 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -76,7 +76,6 @@
import com.android.server.autofill.RemoteAugmentedAutofillService.RemoteAugmentedAutofillServiceCallbacks;
import com.android.server.autofill.ui.AutoFillUI;
import com.android.server.infra.AbstractPerUserSystemService;
-import com.android.server.infra.AbstractRemoteService;
import com.android.server.infra.FrameworkResourcesServiceNameResolver;
import com.android.server.infra.SecureSettingsServiceNameResolver;
@@ -1031,8 +1030,7 @@
return null;
}
final ComponentName componentName = RemoteAugmentedAutofillService.getComponentName(
- getContext(), serviceName, mUserId,
- mAugmentedAutofillResolver.isTemporaryLocked());
+ serviceName, mUserId, mAugmentedAutofillResolver.isTemporaryLocked());
if (componentName == null) return null;
if (sVerbose) {
Slog.v(TAG, "getRemoteAugmentedAutofillServiceLocked(): " + componentName);
@@ -1041,8 +1039,7 @@
mRemoteAugmentedAutofillService = new RemoteAugmentedAutofillService(getContext(),
componentName, mUserId, new RemoteAugmentedAutofillServiceCallbacks() {
@Override
- public void onServiceDied(
- AbstractRemoteService<? extends AbstractRemoteService<?>> service) {
+ public void onServiceDied(@NonNull RemoteAugmentedAutofillService service) {
// TODO(b/111330312): properly implement
Slog.w(TAG, "remote augmented autofill service died");
}
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java
index 35c5102..c562fb1 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java
@@ -233,7 +233,7 @@
final String value2 = getNextArgRequired();
final CountDownLatch latch = new CountDownLatch(1);
- mService.getScore(algorithm, value1, value2, new RemoteCallback((result) -> {
+ mService.calculateScore(algorithm, value1, value2, new RemoteCallback((result) -> {
final Scores scores = result.getParcelable(EXTRA_SCORES);
if (scores == null) {
pw.println("no score");
diff --git a/services/autofill/java/com/android/server/autofill/FieldClassificationStrategy.java b/services/autofill/java/com/android/server/autofill/FieldClassificationStrategy.java
index 293f908e..9db6254 100644
--- a/services/autofill/java/com/android/server/autofill/FieldClassificationStrategy.java
+++ b/services/autofill/java/com/android/server/autofill/FieldClassificationStrategy.java
@@ -15,11 +15,12 @@
*/
package com.android.server.autofill;
-import static com.android.server.autofill.Helper.sDebug;
-import static com.android.server.autofill.Helper.sVerbose;
import static android.service.autofill.AutofillFieldClassificationService.SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS;
import static android.service.autofill.AutofillFieldClassificationService.SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM;
+import static com.android.server.autofill.Helper.sDebug;
+import static com.android.server.autofill.Helper.sVerbose;
+
import android.Manifest;
import android.annotation.MainThread;
import android.annotation.NonNull;
@@ -40,6 +41,7 @@
import android.os.UserHandle;
import android.service.autofill.AutofillFieldClassificationService;
import android.service.autofill.IAutofillFieldClassificationService;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
import android.view.autofill.AutofillValue;
@@ -257,13 +259,13 @@
return parser.get(res, resourceId);
}
- //TODO(b/70291841): rename this method (and all others in the chain) to something like
- // calculateScores() ?
- void getScores(RemoteCallback callback, @Nullable String algorithmName,
- @Nullable Bundle algorithmArgs, @NonNull List<AutofillValue> actualValues,
- @NonNull String[] userDataValues) {
- connectAndRun((service) -> service.getScores(callback, algorithmName,
- algorithmArgs, actualValues, userDataValues));
+ void calculateScores(RemoteCallback callback, @NonNull List<AutofillValue> actualValues,
+ @NonNull String[] userDataValues, @NonNull String[] categoryIds,
+ @Nullable String defaultAlgorithm, @Nullable Bundle defaultArgs,
+ @Nullable ArrayMap<String, String> algorithms,
+ @Nullable ArrayMap<String, Bundle> args) {
+ connectAndRun((service) -> service.calculateScores(callback, actualValues,
+ userDataValues, categoryIds, defaultAlgorithm, defaultArgs, algorithms, args));
}
void dump(String prefix, PrintWriter pw) {
diff --git a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java
index 222888c..fc7265d 100644
--- a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java
+++ b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java
@@ -26,7 +26,6 @@
import android.content.pm.ServiceInfo;
import android.os.Bundle;
import android.os.IBinder;
-import android.os.IInterface;
import android.os.RemoteException;
import android.os.SystemClock;
import android.service.autofill.augmented.AugmentedAutofillService;
@@ -43,7 +42,8 @@
import com.android.server.infra.AbstractSinglePendingRequestRemoteService;
final class RemoteAugmentedAutofillService
- extends AbstractSinglePendingRequestRemoteService<RemoteAugmentedAutofillService> {
+ extends AbstractSinglePendingRequestRemoteService<RemoteAugmentedAutofillService,
+ IAugmentedAutofillService> {
private static final String TAG = RemoteAugmentedAutofillService.class.getSimpleName();
@@ -51,20 +51,16 @@
private static final long TIMEOUT_IDLE_BIND_MILLIS = 2 * DateUtils.MINUTE_IN_MILLIS;
private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS;
- private final RemoteAugmentedAutofillServiceCallbacks mCallbacks;
- private IAugmentedAutofillService mService;
-
RemoteAugmentedAutofillService(Context context, ComponentName serviceName,
int userId, RemoteAugmentedAutofillServiceCallbacks callbacks,
boolean bindInstantServiceAllowed, boolean verbose) {
super(context, AugmentedAutofillService.SERVICE_INTERFACE, serviceName, userId, callbacks,
bindInstantServiceAllowed, verbose);
- mCallbacks = callbacks;
}
@Nullable
- public static ComponentName getComponentName(@NonNull Context context,
- @NonNull String componentName, @UserIdInt int userId, boolean isTemporary) {
+ public static ComponentName getComponentName(@NonNull String componentName,
+ @UserIdInt int userId, boolean isTemporary) {
int flags = PackageManager.GET_META_DATA;
if (!isTemporary) {
flags |= PackageManager.MATCH_SYSTEM_ONLY;
@@ -88,9 +84,8 @@
}
@Override // from AbstractRemoteService
- protected IInterface getServiceInterface(IBinder service) {
- mService = IAugmentedAutofillService.Stub.asInterface(service);
- return mService;
+ protected IAugmentedAutofillService getServiceInterface(IBinder service) {
+ return IAugmentedAutofillService.Stub.asInterface(service);
}
@Override // from AbstractRemoteService
@@ -109,7 +104,6 @@
public void onRequestAutofillLocked(int sessionId, @NonNull IAutoFillManagerClient client,
int taskId, @NonNull ComponentName activityComponent, @NonNull AutofillId focusedId,
@Nullable AutofillValue focusedValue) {
- cancelScheduledUnbind();
scheduleRequest(new PendingAutofillRequest(this, sessionId, client, taskId,
activityComponent, focusedId, focusedValue));
}
@@ -118,12 +112,12 @@
* Called by {@link Session} when it's time to destroy all augmented autofill requests.
*/
public void onDestroyAutofillWindowsRequest(int sessionId) {
- cancelScheduledUnbind();
- scheduleRequest(new PendingDestroyAutofillWindowsRequest(this, sessionId));
+ scheduleAsyncRequest((s) -> s.onDestroyFillWindowRequest(sessionId));
}
+ // TODO(b/111330312): inline into PendingAutofillRequest if it doesn't have any other subclass
private abstract static class MyPendingRequest
- extends PendingRequest<RemoteAugmentedAutofillService> {
+ extends PendingRequest<RemoteAugmentedAutofillService, IAugmentedAutofillService> {
protected final int mSessionId;
private MyPendingRequest(@NonNull RemoteAugmentedAutofillService service, int sessionId) {
@@ -196,38 +190,8 @@
}
- private static final class PendingDestroyAutofillWindowsRequest extends MyPendingRequest {
-
- protected PendingDestroyAutofillWindowsRequest(
- @NonNull RemoteAugmentedAutofillService service, @NonNull int sessionId) {
- super(service, sessionId);
- }
-
- @Override
- public void run() {
- final RemoteAugmentedAutofillService remoteService = getService();
- if (remoteService == null) return;
-
- try {
- remoteService.mService.onDestroyFillWindowRequest(mSessionId);
- } catch (RemoteException e) {
- Slog.w(TAG, "exception handling onDestroyAutofillWindowsRequest() for "
- + mSessionId + ": " + e);
- } finally {
- // Service is not calling back, so we finish right away.
- finish();
- }
- }
-
- @Override
- protected void onTimeout(RemoteAugmentedAutofillService remoteService) {
- // Should not happen because we called finish() on run(), although currently it might
- // be called if the service is destroyed while showing it.
- Slog.e(TAG, "timed out: " + this);
- }
- }
-
- public interface RemoteAugmentedAutofillServiceCallbacks extends VultureCallback {
+ public interface RemoteAugmentedAutofillServiceCallbacks
+ extends VultureCallback<RemoteAugmentedAutofillService> {
// NOTE: so far we don't need to notify the callback implementation (an inner class on
// AutofillManagerServiceImpl) of the request results (success, timeouts, etc..), so this
// callback interface is empty.
diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
index 4b7d290..417ea9c 100644
--- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java
+++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
@@ -28,7 +28,6 @@
import android.content.IntentSender;
import android.os.IBinder;
import android.os.ICancellationSignal;
-import android.os.IInterface;
import android.os.RemoteException;
import android.service.autofill.AutofillService;
import android.service.autofill.FillRequest;
@@ -42,15 +41,15 @@
import com.android.server.infra.AbstractSinglePendingRequestRemoteService;
-final class RemoteFillService extends AbstractSinglePendingRequestRemoteService<RemoteFillService> {
+final class RemoteFillService
+ extends AbstractSinglePendingRequestRemoteService<RemoteFillService, IAutoFillService> {
private static final long TIMEOUT_IDLE_BIND_MILLIS = 5 * DateUtils.SECOND_IN_MILLIS;
private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 5 * DateUtils.SECOND_IN_MILLIS;
private final FillServiceCallbacks mCallbacks;
- private IAutoFillService mAutoFillService;
- public interface FillServiceCallbacks extends VultureCallback {
+ public interface FillServiceCallbacks extends VultureCallback<RemoteFillService> {
void onFillRequestSuccess(int requestId, @Nullable FillResponse response,
@NonNull String servicePackageName, int requestFlags);
void onFillRequestFailure(int requestId, @Nullable CharSequence message);
@@ -71,21 +70,20 @@
@Override // from AbstractRemoteService
protected void handleOnConnectedStateChanged(boolean state) {
- if (mAutoFillService == null) {
+ if (mService == null) {
Slog.w(mTag, "onConnectedStateChanged(): null service");
return;
}
try {
- mAutoFillService.onConnectedStateChanged(state);
+ mService.onConnectedStateChanged(state);
} catch (Exception e) {
Slog.w(mTag, "Exception calling onConnectedStateChanged(): " + e);
}
}
@Override // from AbstractRemoteService
- protected IInterface getServiceInterface(IBinder service) {
- mAutoFillService = IAutoFillService.Stub.asInterface(service);
- return mAutoFillService;
+ protected IAutoFillService getServiceInterface(IBinder service) {
+ return IAutoFillService.Stub.asInterface(service);
}
@Override // from AbstractRemoteService
@@ -127,17 +125,15 @@
}
public void onFillRequest(@NonNull FillRequest request) {
- cancelScheduledUnbind();
scheduleRequest(new PendingFillRequest(request, this));
}
public void onSaveRequest(@NonNull SaveRequest request) {
- cancelScheduledUnbind();
scheduleRequest(new PendingSaveRequest(request, this));
}
private boolean handleResponseCallbackCommon(
- @NonNull PendingRequest<RemoteFillService> pendingRequest) {
+ @NonNull PendingRequest<RemoteFillService, IAutoFillService> pendingRequest) {
if (isDestroyed()) return false;
if (mPendingRequest == pendingRequest) {
@@ -204,7 +200,8 @@
});
}
- private static final class PendingFillRequest extends PendingRequest<RemoteFillService> {
+ private static final class PendingFillRequest
+ extends PendingRequest<RemoteFillService, IAutoFillService> {
private final FillRequest mRequest;
private final IFillCallback mCallback;
private ICancellationSignal mCancellation;
@@ -282,7 +279,7 @@
if (remoteService != null) {
if (sVerbose) Slog.v(mTag, "calling onFillRequest() for id=" + mRequest.getId());
try {
- remoteService.mAutoFillService.onFillRequest(mRequest, mCallback);
+ remoteService.mService.onFillRequest(mRequest, mCallback);
} catch (RemoteException e) {
Slog.e(mTag, "Error calling on fill request", e);
@@ -310,7 +307,8 @@
}
}
- private static final class PendingSaveRequest extends PendingRequest<RemoteFillService> {
+ private static final class PendingSaveRequest
+ extends PendingRequest<RemoteFillService, IAutoFillService> {
private final SaveRequest mRequest;
private final ISaveCallback mCallback;
@@ -355,7 +353,7 @@
if (remoteService != null) {
if (sVerbose) Slog.v(mTag, "calling onSaveRequest()");
try {
- remoteService.mAutoFillService.onSaveRequest(mRequest, mCallback);
+ remoteService.mService.onSaveRequest(mRequest, mCallback);
} catch (RemoteException e) {
Slog.e(mTag, "Error calling on save request", e);
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index aef16b1..0e9b407 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -98,7 +98,6 @@
import com.android.server.autofill.AutofillManagerService.SmartSuggestionMode;
import com.android.server.autofill.ui.AutoFillUI;
import com.android.server.autofill.ui.PendingUi;
-import com.android.server.infra.AbstractRemoteService;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -909,7 +908,7 @@
// VultureCallback
@Override
- public void onServiceDied(AbstractRemoteService<? extends AbstractRemoteService<?>> service) {
+ public void onServiceDied(@NonNull RemoteFillService service) {
Slog.w(TAG, "removing session because service died");
forceRemoveSelfLocked();
}
@@ -1402,6 +1401,12 @@
final String[] userValues = userData.getValues();
final String[] categoryIds = userData.getCategoryIds();
+ final String defaultAlgorithm = userData.getFieldClassificationAlgorithm();
+ final Bundle defaultArgs = userData.getDefaultFieldClassificationArgs();
+
+ final ArrayMap<String, String> algorithms = userData.getFieldClassificationAlgorithms();
+ final ArrayMap<String, Bundle> args = userData.getFieldClassificationArgs();
+
// Sanity check
if (userValues == null || categoryIds == null || userValues.length != categoryIds.length) {
final int valuesLength = userValues == null ? -1 : userValues.length;
@@ -1417,8 +1422,6 @@
final ArrayList<FieldClassification> detectedFieldClassifications = new ArrayList<>(
maxFieldsSize);
- final String algorithm = userData.getFieldClassificationAlgorithm();
- final Bundle algorithmArgs = userData.getAlgorithmArgs();
final int viewsSize = viewStates.size();
// First, we get all scores.
@@ -1505,7 +1508,8 @@
mComponentName, mCompatMode);
});
- fcStrategy.getScores(callback, algorithm, algorithmArgs, currentValues, userValues);
+ fcStrategy.calculateScores(callback, currentValues, userValues, categoryIds,
+ defaultAlgorithm, defaultArgs, algorithms, args);
}
/**
diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java
index 3acdc8e3..a917ced 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerService.java
@@ -16,7 +16,9 @@
package com.android.server.backup;
+import android.Manifest;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IBackupObserver;
@@ -30,6 +32,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.os.Binder;
import android.os.Environment;
import android.os.HandlerThread;
import android.os.IBinder;
@@ -79,6 +82,7 @@
return sInstance;
}
+ private final Context mContext;
private UserBackupManagerService mUserBackupManagerService;
/** Instantiate a new instance of {@link BackupManagerService}. */
@@ -91,11 +95,26 @@
transportWhitelist = Collections.emptySet();
}
+ mContext = context;
mUserBackupManagerService =
UserBackupManagerService.createAndInitializeService(
context, trampoline, backupThread, transportWhitelist);
}
+ /**
+ * If {@code userId} is different from the calling user id, then the caller must hold the
+ * android.permission.INTERACT_ACROSS_USERS_FULL permission.
+ *
+ * @param userId User id on which the backup operation is being requested.
+ * @param message A message to include in the exception if it is thrown.
+ */
+ private void enforceCallingPermissionOnUserId(int userId, String message) {
+ if (Binder.getCallingUserHandle().getIdentifier() != userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, message);
+ }
+ }
+
// TODO(b/118520567): Remove when tests are modified to use per-user instance.
@VisibleForTesting
void setUserBackupManagerService(UserBackupManagerService userBackupManagerService) {
@@ -314,7 +333,8 @@
// ---------------------------------------------
/** Enable/disable the backup service. This is user-configurable via backup settings. */
- public void setBackupEnabled(boolean enable) {
+ public void setBackupEnabled(@UserIdInt int userId, boolean enable) {
+ enforceCallingPermissionOnUserId(userId, "setBackupEnabled");
mUserBackupManagerService.setBackupEnabled(enable);
}
@@ -331,7 +351,8 @@
/**
* Return {@code true} if the backup mechanism is currently enabled, else returns {@code false}.
*/
- public boolean isBackupEnabled() {
+ public boolean isBackupEnabled(@UserIdInt int userId) {
+ enforceCallingPermissionOnUserId(userId, "isBackupEnabled");
return mUserBackupManagerService.isBackupEnabled();
}
@@ -355,7 +376,8 @@
* Run a backup pass immediately for any key-value backup applications that have declared that
* they have pending updates.
*/
- public void backupNow() {
+ public void backupNow(@UserIdInt int userId) {
+ enforceCallingPermissionOnUserId(userId, "backupNow");
mUserBackupManagerService.backupNow();
}
@@ -364,12 +386,18 @@
* IBackupManagerMonitor} for receiving events during the operation.
*/
public int requestBackup(
- String[] packages, IBackupObserver observer, IBackupManagerMonitor monitor, int flags) {
+ @UserIdInt int userId,
+ String[] packages,
+ IBackupObserver observer,
+ IBackupManagerMonitor monitor,
+ int flags) {
+ enforceCallingPermissionOnUserId(userId, "requestBackup");
return mUserBackupManagerService.requestBackup(packages, observer, monitor, flags);
}
/** Cancel all running backup operations. */
- public void cancelBackups() {
+ public void cancelBackups(@UserIdInt int userId) {
+ enforceCallingPermissionOnUserId(userId, "cancelBackups");
mUserBackupManagerService.cancelBackups();
}
@@ -533,8 +561,10 @@
stage.renameTo(enableFile);
// will be synced immediately by the try-with-resources call to close()
} catch (IOException | RuntimeException e) {
- Slog.e(TAG, "Unable to record backup enable state; reverting to disabled: "
- + e.getMessage());
+ Slog.e(
+ TAG,
+ "Unable to record backup enable state; reverting to disabled: "
+ + e.getMessage());
enableFile.delete();
stage.delete();
}
diff --git a/services/backup/java/com/android/server/backup/Trampoline.java b/services/backup/java/com/android/server/backup/Trampoline.java
index 108f50d..ed6ff9b 100644
--- a/services/backup/java/com/android/server/backup/Trampoline.java
+++ b/services/backup/java/com/android/server/backup/Trampoline.java
@@ -19,6 +19,7 @@
import static com.android.server.backup.BackupManagerService.TAG;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.admin.DevicePolicyManager;
import android.app.backup.BackupManager;
import android.app.backup.IBackupManager;
@@ -123,6 +124,10 @@
== MULTI_USER_ENABLED;
}
+ protected int binderGetCallingUserId() {
+ return Binder.getCallingUserHandle().getIdentifier();
+ }
+
protected int binderGetCallingUid() {
return Binder.getCallingUid();
}
@@ -319,14 +324,20 @@
}
@Override
- public void setBackupEnabled(boolean isEnabled) throws RemoteException {
+ public void setBackupEnabledForUser(@UserIdInt int userId, boolean isEnabled)
+ throws RemoteException {
BackupManagerService svc = mService;
if (svc != null) {
- svc.setBackupEnabled(isEnabled);
+ svc.setBackupEnabled(userId, isEnabled);
}
}
@Override
+ public void setBackupEnabled(boolean isEnabled) throws RemoteException {
+ setBackupEnabledForUser(binderGetCallingUserId(), isEnabled);
+ }
+
+ @Override
public void setAutoRestore(boolean doAutoRestore) throws RemoteException {
BackupManagerService svc = mService;
if (svc != null) {
@@ -343,9 +354,14 @@
}
@Override
- public boolean isBackupEnabled() throws RemoteException {
+ public boolean isBackupEnabledForUser(@UserIdInt int userId) throws RemoteException {
BackupManagerService svc = mService;
- return (svc != null) ? svc.isBackupEnabled() : false;
+ return (svc != null) ? svc.isBackupEnabled(userId) : false;
+ }
+
+ @Override
+ public boolean isBackupEnabled() throws RemoteException {
+ return isBackupEnabledForUser(binderGetCallingUserId());
}
@Override
@@ -361,14 +377,19 @@
}
@Override
- public void backupNow() throws RemoteException {
+ public void backupNowForUser(@UserIdInt int userId) throws RemoteException {
BackupManagerService svc = mService;
if (svc != null) {
- svc.backupNow();
+ svc.backupNow(userId);
}
}
@Override
+ public void backupNow() throws RemoteException {
+ backupNowForUser(binderGetCallingUserId());
+ }
+
+ @Override
public void adbBackup(ParcelFileDescriptor fd, boolean includeApks, boolean includeObbs,
boolean includeShared, boolean doWidgets, boolean allApps,
boolean allIncludesSystem, boolean doCompress, boolean doKeyValue, String[] packageNames)
@@ -543,21 +564,33 @@
}
@Override
- public int requestBackup(String[] packages, IBackupObserver observer,
- IBackupManagerMonitor monitor, int flags) throws RemoteException {
+ public int requestBackupForUser(@UserIdInt int userId, String[] packages, IBackupObserver
+ observer, IBackupManagerMonitor monitor, int flags) throws RemoteException {
BackupManagerService svc = mService;
if (svc == null) {
return BackupManager.ERROR_BACKUP_NOT_ALLOWED;
}
- return svc.requestBackup(packages, observer, monitor, flags);
+ return svc.requestBackup(userId, packages, observer, monitor, flags);
+ }
+
+ @Override
+ public int requestBackup(String[] packages, IBackupObserver observer,
+ IBackupManagerMonitor monitor, int flags) throws RemoteException {
+ return requestBackupForUser(binderGetCallingUserId(), packages,
+ observer, monitor, flags);
+ }
+
+ @Override
+ public void cancelBackupsForUser(@UserIdInt int userId) throws RemoteException {
+ BackupManagerService svc = mService;
+ if (svc != null) {
+ svc.cancelBackups(userId);
+ }
}
@Override
public void cancelBackups() throws RemoteException {
- BackupManagerService svc = mService;
- if (svc != null) {
- svc.cancelBackups();
- }
+ cancelBackupsForUser(binderGetCallingUserId());
}
@Override
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureSession.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureSession.java
index 2302b7d..a4012d5 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureSession.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureSession.java
@@ -29,7 +29,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import com.android.server.contentcapture.RemoteContentCaptureService.ContentCaptureServiceCallbacks;
-import com.android.server.infra.AbstractRemoteService;
import java.io.PrintWriter;
import java.util.List;
@@ -124,8 +123,8 @@
}
}
- @Override // from RemoteScreenObservationServiceCallbacks
- public void onServiceDied(AbstractRemoteService<?> service) {
+ @Override // from RemoteContentCaptureServiceCallbacks
+ public void onServiceDied(@NonNull RemoteContentCaptureService service) {
// TODO(b/111276913): implement (remove session from PerUserSession?)
if (mService.isDebug()) {
Slog.d(TAG, "onServiceDied() for " + mId);
@@ -135,17 +134,6 @@
}
}
- @Override // from RemoteScreenObservationServiceCallbacks
- public void onFailureOrTimeout(boolean timedOut) {
- // TODO(b/111276913): log metrics on whether timed out or not
- if (mService.isDebug()) {
- Slog.d(TAG, "onFailureOrTimeout(" + mId + "): timed out=" + timedOut);
- }
- synchronized (mLock) {
- removeSelfLocked(/* notifyRemoteService= */ false);
- }
- }
-
@GuardedBy("mLock")
public void dumpLocked(@NonNull String prefix, @NonNull PrintWriter pw) {
pw.print(prefix); pw.print("id: "); pw.print(mId); pw.println();
diff --git a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
index 6a111f2..33b6c8d 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
@@ -20,14 +20,11 @@
import android.content.ComponentName;
import android.content.Context;
import android.os.IBinder;
-import android.os.IInterface;
-import android.os.RemoteException;
import android.service.contentcapture.ContentCaptureEventsRequest;
import android.service.contentcapture.IContentCaptureService;
import android.service.contentcapture.InteractionContext;
import android.service.contentcapture.SnapshotData;
import android.text.format.DateUtils;
-import android.util.Slog;
import android.view.contentcapture.ContentCaptureEvent;
import com.android.server.infra.AbstractMultiplePendingRequestsRemoteService;
@@ -35,30 +32,24 @@
import java.util.List;
final class RemoteContentCaptureService
- extends AbstractMultiplePendingRequestsRemoteService<RemoteContentCaptureService> {
-
- private static final String TAG = RemoteContentCaptureService.class.getSimpleName();
+ extends AbstractMultiplePendingRequestsRemoteService<RemoteContentCaptureService,
+ IContentCaptureService> {
// TODO(b/117779333): changed it so it's permanentely bound
private static final long TIMEOUT_IDLE_BIND_MILLIS = 2 * DateUtils.MINUTE_IN_MILLIS;
private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS;
- private final ContentCaptureServiceCallbacks mCallbacks;
- private IContentCaptureService mService;
-
RemoteContentCaptureService(Context context, String serviceInterface,
ComponentName componentName, int userId,
ContentCaptureServiceCallbacks callbacks, boolean bindInstantServiceAllowed,
boolean verbose) {
super(context, serviceInterface, componentName, userId, callbacks,
bindInstantServiceAllowed, verbose, /* initialCapacity= */ 2);
- mCallbacks = callbacks;
}
@Override // from RemoteService
- protected IInterface getServiceInterface(@NonNull IBinder service) {
- mService = IContentCaptureService.Stub.asInterface(service);
- return mService;
+ protected IContentCaptureService getServiceInterface(@NonNull IBinder service) {
+ return IContentCaptureService.Stub.asInterface(service);
}
// TODO(b/111276913): modify super class to allow permanent binding when value is 0 or negative
@@ -81,8 +72,7 @@
*/
public void onSessionLifecycleRequest(@Nullable InteractionContext context,
@NonNull String sessionId) {
- cancelScheduledUnbind();
- scheduleRequest(new PendingSessionLifecycleRequest(this, context, sessionId));
+ scheduleAsyncRequest((s) -> s.onSessionLifecycle(context, sessionId));
}
/**
@@ -90,8 +80,8 @@
*/
public void onContentCaptureEventsRequest(@NonNull String sessionId,
@NonNull List<ContentCaptureEvent> events) {
- cancelScheduledUnbind();
- scheduleRequest(new PendingOnContentCaptureEventsRequest(this, sessionId, events));
+ scheduleAsyncRequest((s) -> s.onContentCaptureEventsRequest(sessionId,
+ new ContentCaptureEventsRequest(events)));
}
/**
@@ -99,103 +89,13 @@
*/
public void onActivitySnapshotRequest(@NonNull String sessionId,
@NonNull SnapshotData snapshotData) {
- cancelScheduledUnbind();
- scheduleRequest(new PendingOnActivitySnapshotRequest(this, sessionId, snapshotData));
+ scheduleAsyncRequest((s) -> s.onActivitySnapshot(sessionId, snapshotData));
}
- private abstract static class MyPendingRequest
- extends PendingRequest<RemoteContentCaptureService> {
- protected final String mSessionId;
-
- private MyPendingRequest(@NonNull RemoteContentCaptureService service,
- @NonNull String sessionId) {
- super(service);
- mSessionId = sessionId;
- }
-
- @Override // from PendingRequest
- protected final void onTimeout(RemoteContentCaptureService remoteService) {
- Slog.w(TAG, "timed out handling " + getClass().getSimpleName() + " for "
- + mSessionId);
- remoteService.mCallbacks.onFailureOrTimeout(/* timedOut= */ true);
- }
-
- @Override // from PendingRequest
- public final void run() {
- final RemoteContentCaptureService remoteService = getService();
- if (remoteService != null) {
- try {
- // We don't expect the service to call us back, so we finish right away.
- myRun(remoteService);
- // TODO(b/111330312): not true anymore!!
- finish();
- } catch (RemoteException e) {
- Slog.w(TAG, "exception handling " + getClass().getSimpleName() + " for "
- + mSessionId + ": " + e);
- remoteService.mCallbacks.onFailureOrTimeout(/* timedOut= */ false);
- }
- }
- }
-
- protected abstract void myRun(@NonNull RemoteContentCaptureService service)
- throws RemoteException;
-
- }
-
- private static final class PendingSessionLifecycleRequest extends MyPendingRequest {
-
- private final InteractionContext mContext;
-
- protected PendingSessionLifecycleRequest(@NonNull RemoteContentCaptureService service,
- @Nullable InteractionContext context, @NonNull String sessionId) {
- super(service, sessionId);
- mContext = context;
- }
-
- @Override // from MyPendingRequest
- public void myRun(@NonNull RemoteContentCaptureService remoteService)
- throws RemoteException {
- remoteService.mService.onSessionLifecycle(mContext, mSessionId);
- }
- }
-
- private static final class PendingOnContentCaptureEventsRequest extends MyPendingRequest {
-
- private final List<ContentCaptureEvent> mEvents;
-
- protected PendingOnContentCaptureEventsRequest(@NonNull RemoteContentCaptureService service,
- @NonNull String sessionId, @NonNull List<ContentCaptureEvent> events) {
- super(service, sessionId);
- mEvents = events;
- }
-
- @Override // from MyPendingRequest
- public void myRun(@NonNull RemoteContentCaptureService remoteService)
- throws RemoteException {
- remoteService.mService.onContentCaptureEventsRequest(mSessionId,
- new ContentCaptureEventsRequest(mEvents));
- }
- }
-
- private static final class PendingOnActivitySnapshotRequest extends MyPendingRequest {
-
- private final SnapshotData mSnapshotData;
-
- protected PendingOnActivitySnapshotRequest(@NonNull RemoteContentCaptureService service,
- @NonNull String sessionId, @NonNull SnapshotData snapshotData) {
- super(service, sessionId);
- mSnapshotData = snapshotData;
- }
-
- @Override // from MyPendingRequest
- protected void myRun(@NonNull RemoteContentCaptureService remoteService)
- throws RemoteException {
- remoteService.mService.onActivitySnapshot(mSessionId, mSnapshotData);
- }
- }
-
- public interface ContentCaptureServiceCallbacks extends VultureCallback {
- // To keep it simple, we use the same callback for all failures / timeouts.
- void onFailureOrTimeout(boolean timedOut);
+ public interface ContentCaptureServiceCallbacks
+ extends VultureCallback<RemoteContentCaptureService> {
+ // NOTE: so far we don't need to notify the callback implementation (an inner class on
+ // AutofillManagerServiceImpl) of the request results (success, timeouts, etc..), so this
+ // callback interface is empty.
}
}
diff --git a/services/core/java/com/android/server/BinderCallsStatsService.java b/services/core/java/com/android/server/BinderCallsStatsService.java
index 11a2fc9..8b7f321 100644
--- a/services/core/java/com/android/server/BinderCallsStatsService.java
+++ b/services/core/java/com/android/server/BinderCallsStatsService.java
@@ -19,7 +19,6 @@
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
-import android.app.AppGlobals;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -28,9 +27,7 @@
import android.net.Uri;
import android.os.Binder;
import android.os.Process;
-import android.os.RemoteException;
import android.os.SystemProperties;
-import android.os.ThreadLocalWorkSource;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.ArrayMap;
@@ -38,19 +35,16 @@
import android.util.KeyValueListParser;
import android.util.Slog;
-import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.AppIdToPackageMap;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.BinderCallsStats;
import com.android.internal.os.BinderInternal;
-import com.android.internal.os.BinderInternal.CallSession;
import com.android.internal.os.CachedDeviceState;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
public class BinderCallsStatsService extends Binder {
@@ -60,10 +54,10 @@
= "persist.sys.binder_calls_detailed_tracking";
/** Resolves the work source of an incoming binder transaction. */
- static class WorkSourceProvider {
+ static class AuthorizedWorkSourceProvider implements BinderInternal.WorkSourceProvider {
private ArraySet<Integer> mAppIdWhitelist;
- WorkSourceProvider() {
+ AuthorizedWorkSourceProvider() {
mAppIdWhitelist = new ArraySet<>();
}
@@ -82,11 +76,11 @@
mAppIdWhitelist = createAppidWhitelist(context);
}
- public void dump(PrintWriter pw, Map<Integer, String> appIdToPackageName) {
+ public void dump(PrintWriter pw, AppIdToPackageMap packageMap) {
pw.println("AppIds of apps that can set the work source:");
final ArraySet<Integer> whitelist = mAppIdWhitelist;
for (Integer appId : whitelist) {
- pw.println("\t- " + appIdToPackageName.getOrDefault(appId, String.valueOf(appId)));
+ pw.println("\t- " + packageMap.mapAppId(appId));
}
}
@@ -103,7 +97,7 @@
final ArraySet<Integer> whitelist = new ArraySet<>();
// We trust our own process.
- whitelist.add(Process.myUid());
+ whitelist.add(UserHandle.getAppId(Process.myUid()));
// We only need to initialize it once. UPDATE_DEVICE_STATS is a system permission.
final PackageManager pm = context.getPackageManager();
final String[] permissions = { android.Manifest.permission.UPDATE_DEVICE_STATS };
@@ -125,41 +119,6 @@
}
}
- /** Observer for all system server incoming binder transactions. */
- @VisibleForTesting
- static class BinderCallsObserver implements BinderInternal.Observer {
- private final BinderInternal.Observer mBinderCallsStats;
- private final WorkSourceProvider mWorkSourceProvider;
-
- BinderCallsObserver(BinderInternal.Observer callsStats,
- WorkSourceProvider workSourceProvider) {
- mBinderCallsStats = callsStats;
- mWorkSourceProvider = workSourceProvider;
- }
-
- @Override
- public CallSession callStarted(Binder binder, int code) {
- // We depend on the code in Binder#execTransact to reset the state of
- // ThreadLocalWorkSource
- setThreadLocalWorkSourceUid(mWorkSourceProvider.resolveWorkSourceUid());
- return mBinderCallsStats.callStarted(binder, code);
- }
-
- @Override
- public void callEnded(CallSession s, int parcelRequestSize, int parcelReplySize) {
- mBinderCallsStats.callEnded(s, parcelRequestSize, parcelReplySize);
- }
-
- @Override
- public void callThrewException(CallSession s, Exception exception) {
- mBinderCallsStats.callThrewException(s, exception);
- }
-
- protected void setThreadLocalWorkSourceUid(int uid) {
- ThreadLocalWorkSource.setUid(uid);
- }
- }
-
/** Listens for flag changes. */
private static class SettingsObserver extends ContentObserver {
private static final String SETTINGS_ENABLED_KEY = "enabled";
@@ -173,16 +132,16 @@
private final Context mContext;
private final KeyValueListParser mParser = new KeyValueListParser(',');
private final BinderCallsStats mBinderCallsStats;
- private final BinderCallsObserver mBinderCallsObserver;
+ private final AuthorizedWorkSourceProvider mWorkSourceProvider;
SettingsObserver(Context context, BinderCallsStats binderCallsStats,
- BinderCallsObserver observer) {
+ AuthorizedWorkSourceProvider workSourceProvider) {
super(BackgroundThread.getHandler());
mContext = context;
context.getContentResolver().registerContentObserver(mUri, false, this,
UserHandle.USER_SYSTEM);
mBinderCallsStats = binderCallsStats;
- mBinderCallsObserver = observer;
+ mWorkSourceProvider = workSourceProvider;
// Always kick once to ensure that we match current state
onChange();
}
@@ -220,12 +179,14 @@
mParser.getBoolean(SETTINGS_ENABLED_KEY, BinderCallsStats.ENABLED_DEFAULT);
if (mEnabled != enabled) {
if (enabled) {
- Binder.setObserver(mBinderCallsObserver);
+ Binder.setObserver(mBinderCallsStats);
Binder.setProxyTransactListener(
new Binder.PropagateWorkSourceTransactListener());
+ Binder.setWorkSourceProvider(mWorkSourceProvider);
} else {
Binder.setObserver(null);
Binder.setProxyTransactListener(null);
+ Binder.setWorkSourceProvider(Binder::getCallingUid);
}
mEnabled = enabled;
mBinderCallsStats.reset();
@@ -268,7 +229,7 @@
public static class LifeCycle extends SystemService {
private BinderCallsStatsService mService;
private BinderCallsStats mBinderCallsStats;
- private WorkSourceProvider mWorkSourceProvider;
+ private AuthorizedWorkSourceProvider mWorkSourceProvider;
public LifeCycle(Context context) {
super(context);
@@ -277,11 +238,9 @@
@Override
public void onStart() {
mBinderCallsStats = new BinderCallsStats(new BinderCallsStats.Injector());
- mWorkSourceProvider = new WorkSourceProvider();
- BinderCallsObserver binderCallsObserver =
- new BinderCallsObserver(mBinderCallsStats, mWorkSourceProvider);
+ mWorkSourceProvider = new AuthorizedWorkSourceProvider();
mService = new BinderCallsStatsService(
- mBinderCallsStats, binderCallsObserver, mWorkSourceProvider);
+ mBinderCallsStats, mWorkSourceProvider);
publishLocalService(Internal.class, new Internal(mBinderCallsStats));
publishBinderService("binder_calls_stats", mService);
boolean detailedTrackingEnabled = SystemProperties.getBoolean(
@@ -311,18 +270,16 @@
private SettingsObserver mSettingsObserver;
private final BinderCallsStats mBinderCallsStats;
- private final BinderCallsObserver mBinderCallsObserver;
- private final WorkSourceProvider mWorkSourceProvider;
+ private final AuthorizedWorkSourceProvider mWorkSourceProvider;
- BinderCallsStatsService(BinderCallsStats binderCallsStats, BinderCallsObserver observer,
- WorkSourceProvider workSourceProvider) {
+ BinderCallsStatsService(BinderCallsStats binderCallsStats,
+ AuthorizedWorkSourceProvider workSourceProvider) {
mBinderCallsStats = binderCallsStats;
- mBinderCallsObserver = observer;
mWorkSourceProvider = workSourceProvider;
}
public void systemReady(Context context) {
- mSettingsObserver = new SettingsObserver(context, mBinderCallsStats, mBinderCallsObserver);
+ mSettingsObserver = new SettingsObserver(context, mBinderCallsStats, mWorkSourceProvider);
}
public void reset() {
@@ -342,7 +299,7 @@
pw.println("binder_calls_stats reset.");
return;
} else if ("--enable".equals(arg)) {
- Binder.setObserver(mBinderCallsObserver);
+ Binder.setObserver(mBinderCallsStats);
return;
} else if ("--disable".equals(arg)) {
Binder.setObserver(null);
@@ -361,7 +318,7 @@
pw.println("Detailed tracking disabled");
return;
} else if ("--dump-worksource-provider".equals(arg)) {
- mWorkSourceProvider.dump(pw, getAppIdToPackagesMap());
+ mWorkSourceProvider.dump(pw, AppIdToPackageMap.getSnapshot());
return;
} else if ("-h".equals(arg)) {
pw.println("binder_calls_stats commands:");
@@ -377,28 +334,6 @@
}
}
}
- mBinderCallsStats.dump(pw, getAppIdToPackagesMap(), verbose);
- }
-
- private Map<Integer, String> getAppIdToPackagesMap() {
- List<PackageInfo> packages;
- try {
- packages = AppGlobals.getPackageManager()
- .getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES,
- UserHandle.USER_SYSTEM).getList();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- Map<Integer,String> map = new HashMap<>();
- for (PackageInfo pkg : packages) {
- String name = pkg.packageName;
- int uid = pkg.applicationInfo.uid;
- // Use sharedUserId string as package name if there are collisions
- if (pkg.sharedUserId != null && map.containsKey(uid)) {
- name = "shared:" + pkg.sharedUserId;
- }
- map.put(uid, name);
- }
- return map;
+ mBinderCallsStats.dump(pw, AppIdToPackageMap.getSnapshot(), verbose);
}
}
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 14503f9..eda9fe1 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -902,6 +902,7 @@
// Listen to package add and removal events for all users.
intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
intentFilter.addDataScheme("package");
mContext.registerReceiverAsUser(
@@ -4203,12 +4204,46 @@
mPermissionMonitor.onPackageAdded(packageName, uid);
}
- private void onPackageRemoved(String packageName, int uid) {
+ private void onPackageReplaced(String packageName, int uid) {
+ if (TextUtils.isEmpty(packageName) || uid < 0) {
+ Slog.wtf(TAG, "Invalid package in onPackageReplaced: " + packageName + " | " + uid);
+ return;
+ }
+ final int userId = UserHandle.getUserId(uid);
+ synchronized (mVpns) {
+ final Vpn vpn = mVpns.get(userId);
+ if (vpn == null) {
+ return;
+ }
+ // Legacy always-on VPN won't be affected since the package name is not set.
+ if (TextUtils.equals(vpn.getAlwaysOnPackage(), packageName)) {
+ Slog.d(TAG, "Restarting always-on VPN package " + packageName + " for user "
+ + userId);
+ vpn.startAlwaysOnVpn();
+ }
+ }
+ }
+
+ private void onPackageRemoved(String packageName, int uid, boolean isReplacing) {
if (TextUtils.isEmpty(packageName) || uid < 0) {
Slog.wtf(TAG, "Invalid package in onPackageRemoved: " + packageName + " | " + uid);
return;
}
mPermissionMonitor.onPackageRemoved(uid);
+
+ final int userId = UserHandle.getUserId(uid);
+ synchronized (mVpns) {
+ final Vpn vpn = mVpns.get(userId);
+ if (vpn == null) {
+ return;
+ }
+ // Legacy always-on VPN won't be affected since the package name is not set.
+ if (TextUtils.equals(vpn.getAlwaysOnPackage(), packageName) && !isReplacing) {
+ Slog.d(TAG, "Removing always-on VPN package " + packageName + " for user "
+ + userId);
+ vpn.setAlwaysOnPackage(null, false);
+ }
+ }
}
private void onUserUnlocked(int userId) {
@@ -4245,8 +4280,12 @@
onUserUnlocked(userId);
} else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
onPackageAdded(packageName, uid);
+ } else if (Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
+ onPackageReplaced(packageName, uid);
} else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
- onPackageRemoved(packageName, uid);
+ final boolean isReplacing = intent.getBooleanExtra(
+ Intent.EXTRA_REPLACING, false);
+ onPackageRemoved(packageName, uid, isReplacing);
}
}
};
diff --git a/services/core/java/com/android/server/LooperStatsService.java b/services/core/java/com/android/server/LooperStatsService.java
index fa3baba..cee98c1 100644
--- a/services/core/java/com/android/server/LooperStatsService.java
+++ b/services/core/java/com/android/server/LooperStatsService.java
@@ -31,6 +31,7 @@
import android.util.KeyValueListParser;
import android.util.Slog;
+import com.android.internal.os.AppIdToPackageMap;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.CachedDeviceState;
import com.android.internal.os.LooperStats;
@@ -92,6 +93,7 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+ AppIdToPackageMap packageMap = AppIdToPackageMap.getSnapshot();
pw.print("Start time: ");
pw.println(DateFormat.format("yyyy-MM-dd HH:mm:ss", mStats.getStartTimeMillis()));
pw.print("On battery time (ms): ");
@@ -121,7 +123,7 @@
pw.println(header);
for (LooperStats.ExportedEntry entry : entries) {
pw.printf("%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n",
- entry.workSourceUid,
+ packageMap.mapUid(entry.workSourceUid),
entry.threadName,
entry.handlerClassName,
entry.messageName,
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 40da881..c9cab98 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -17532,8 +17532,9 @@
// how many slots we have for background processes; we may want
// to put multiple processes in a slot of there are enough of
// them.
- int numSlots = (ProcessList.CACHED_APP_MAX_ADJ
- - ProcessList.CACHED_APP_MIN_ADJ + 1) / 2;
+ final int numSlots = (ProcessList.CACHED_APP_MAX_ADJ
+ - ProcessList.CACHED_APP_MIN_ADJ + 1) / 2
+ / ProcessList.CACHED_APP_IMPORTANCE_LEVELS;
int numEmptyProcs = N - mNumNonCachedProcs - mNumCachedHiddenProcs;
if (numEmptyProcs > cachedProcessLimit) {
// If there are more empty processes than our limit on cached
@@ -17544,17 +17545,19 @@
// instead of a gazillion empty processes.
numEmptyProcs = cachedProcessLimit;
}
- int emptyFactor = numEmptyProcs/numSlots;
+ int emptyFactor = (numEmptyProcs + numSlots - 1) / numSlots;
if (emptyFactor < 1) emptyFactor = 1;
- int cachedFactor = (mNumCachedHiddenProcs > 0 ? mNumCachedHiddenProcs : 1)/numSlots;
+ int cachedFactor = (mNumCachedHiddenProcs > 0 ? (mNumCachedHiddenProcs + numSlots - 1) : 1)
+ / numSlots;
if (cachedFactor < 1) cachedFactor = 1;
- int stepCached = 0;
- int stepEmpty = 0;
+ int stepCached = -1;
+ int stepEmpty = -1;
int numCached = 0;
int numCachedExtraGroup = 0;
int numEmpty = 0;
int numTrimming = 0;
int lastCachedGroup = 0;
+ int lastCachedGroupImportance = 0;
int lastCachedGroupUid = 0;
mNumNonCachedProcs = 0;
@@ -17563,9 +17566,10 @@
// First update the OOM adjustment for each of the
// application processes based on their current state.
int curCachedAdj = ProcessList.CACHED_APP_MIN_ADJ;
- int nextCachedAdj = curCachedAdj+1;
- int curEmptyAdj = ProcessList.CACHED_APP_MIN_ADJ;
- int nextEmptyAdj = curEmptyAdj+2;
+ int nextCachedAdj = curCachedAdj + (ProcessList.CACHED_APP_IMPORTANCE_LEVELS * 2);
+ int curCachedImpAdj = 0;
+ int curEmptyAdj = ProcessList.CACHED_APP_MIN_ADJ + ProcessList.CACHED_APP_IMPORTANCE_LEVELS;
+ int nextEmptyAdj = curEmptyAdj + (ProcessList.CACHED_APP_IMPORTANCE_LEVELS * 2);
boolean retryCycles = false;
@@ -17590,27 +17594,61 @@
case PROCESS_STATE_CACHED_ACTIVITY:
case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
case ActivityManager.PROCESS_STATE_CACHED_RECENT:
- // This process is a cached process holding activities...
- // assign it the next cached value for that type, and then
- // step that cached level.
- app.setCurRawAdj(curCachedAdj);
- app.curAdj = app.modifyRawOomAdj(curCachedAdj);
- if (DEBUG_LRU && false) Slog.d(TAG_LRU, "Assigning activity LRU #" + i
- + " adj: " + app.curAdj + " (curCachedAdj=" + curCachedAdj
- + ")");
- if (curCachedAdj != nextCachedAdj) {
+ // Figure out the next cached level, taking into account groups.
+ boolean inGroup = false;
+ if (app.connectionGroup != 0) {
+ if (lastCachedGroupUid == app.uid
+ && lastCachedGroup == app.connectionGroup) {
+ // This is in the same group as the last process, just tweak
+ // adjustment by importance.
+ if (app.connectionImportance > lastCachedGroupImportance) {
+ lastCachedGroupImportance = app.connectionImportance;
+ if (curCachedAdj < nextCachedAdj
+ && curCachedAdj < ProcessList.CACHED_APP_MAX_ADJ) {
+ curCachedImpAdj++;
+ }
+ }
+ inGroup = true;
+ } else {
+ lastCachedGroupUid = app.uid;
+ lastCachedGroup = app.connectionGroup;
+ lastCachedGroupImportance = app.connectionImportance;
+ }
+ }
+ if (!inGroup && curCachedAdj != nextCachedAdj) {
stepCached++;
+ curCachedImpAdj = 0;
if (stepCached >= cachedFactor) {
stepCached = 0;
curCachedAdj = nextCachedAdj;
- nextCachedAdj += 2;
+ nextCachedAdj += ProcessList.CACHED_APP_IMPORTANCE_LEVELS * 2;
if (nextCachedAdj > ProcessList.CACHED_APP_MAX_ADJ) {
nextCachedAdj = ProcessList.CACHED_APP_MAX_ADJ;
}
}
}
+ // This process is a cached process holding activities...
+ // assign it the next cached value for that type, and then
+ // step that cached level.
+ app.setCurRawAdj(curCachedAdj + curCachedImpAdj);
+ app.curAdj = app.modifyRawOomAdj(curCachedAdj + curCachedImpAdj);
+ if (DEBUG_LRU && false) Slog.d(TAG_LRU, "Assigning activity LRU #" + i
+ + " adj: " + app.curAdj + " (curCachedAdj=" + curCachedAdj
+ + " curCachedImpAdj=" + curCachedImpAdj + ")");
break;
default:
+ // Figure out the next cached level.
+ if (curEmptyAdj != nextEmptyAdj) {
+ stepEmpty++;
+ if (stepEmpty >= emptyFactor) {
+ stepEmpty = 0;
+ curEmptyAdj = nextEmptyAdj;
+ nextEmptyAdj += ProcessList.CACHED_APP_IMPORTANCE_LEVELS * 2;
+ if (nextEmptyAdj > ProcessList.CACHED_APP_MAX_ADJ) {
+ nextEmptyAdj = ProcessList.CACHED_APP_MAX_ADJ;
+ }
+ }
+ }
// For everything else, assign next empty cached process
// level and bump that up. Note that this means that
// long-running services that have dropped down to the
@@ -17621,22 +17659,9 @@
if (DEBUG_LRU && false) Slog.d(TAG_LRU, "Assigning empty LRU #" + i
+ " adj: " + app.curAdj + " (curEmptyAdj=" + curEmptyAdj
+ ")");
- if (curEmptyAdj != nextEmptyAdj) {
- stepEmpty++;
- if (stepEmpty >= emptyFactor) {
- stepEmpty = 0;
- curEmptyAdj = nextEmptyAdj;
- nextEmptyAdj += 2;
- if (nextEmptyAdj > ProcessList.CACHED_APP_MAX_ADJ) {
- nextEmptyAdj = ProcessList.CACHED_APP_MAX_ADJ;
- }
- }
- }
break;
}
}
-
-
}
}
@@ -17668,6 +17693,8 @@
}
}
+ lastCachedGroup = lastCachedGroupUid = 0;
+
for (int i=N-1; i>=0; i--) {
ProcessRecord app = mProcessList.mLruProcesses.get(i);
if (!app.killedByAm && app.thread != null) {
@@ -18935,7 +18962,7 @@
ProcessMemoryState processMemoryState =
new ProcessMemoryState(uid,
r.processName,
- r.maxAdj,
+ r.curAdj,
memoryStat.pgfault,
memoryStat.pgmajfault,
memoryStat.rssInBytes,
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index 8c39d75..3a0899d 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -1337,7 +1337,7 @@
// Broadcast is being executed, its package can't be stopped.
try {
AppGlobals.getPackageManager().setPackageStoppedState(
- r.curComponent.getPackageName(), false, UserHandle.getUserId(r.callingUid));
+ r.curComponent.getPackageName(), false, r.userId);
} catch (RemoteException e) {
} catch (IllegalArgumentException e) {
Slog.w(TAG, "Failed trying to unstop package "
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 8cf9f1e..117984e 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -137,9 +137,13 @@
// This is a process only hosting activities that are not visible,
// so it can be killed without any disruption.
- static final int CACHED_APP_MAX_ADJ = 906;
+ static final int CACHED_APP_MAX_ADJ = 999;
static final int CACHED_APP_MIN_ADJ = 900;
+ // Number of levels we have available for different service connection group importance
+ // levels.
+ static final int CACHED_APP_IMPORTANCE_LEVELS = 5;
+
// The B list of SERVICE_ADJ -- these are the old and decrepit
// services that aren't as shiny and interesting as the ones in the A list.
static final int SERVICE_B_ADJ = 800;
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 3c14393..d75601b 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -944,10 +944,11 @@
public boolean hasTetherableConfiguration() {
final TetheringConfiguration cfg = mConfig;
final boolean hasDownstreamConfiguration =
- (cfg.tetherableUsbRegexs.length != 0) ||
- (cfg.tetherableWifiRegexs.length != 0) ||
- (cfg.tetherableBluetoothRegexs.length != 0);
- final boolean hasUpstreamConfiguration = !cfg.preferredUpstreamIfaceTypes.isEmpty();
+ (cfg.tetherableUsbRegexs.length != 0)
+ || (cfg.tetherableWifiRegexs.length != 0)
+ || (cfg.tetherableBluetoothRegexs.length != 0);
+ final boolean hasUpstreamConfiguration = !cfg.preferredUpstreamIfaceTypes.isEmpty()
+ || cfg.chooseUpstreamAutomatically;
return hasDownstreamConfiguration && hasUpstreamConfiguration;
}
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index b7ed2f9..602aedb 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -206,45 +206,6 @@
// Handle of the user initiating VPN.
private final int mUserHandle;
- // Listen to package removal and change events (update/uninstall) for this user
- private final BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- final Uri data = intent.getData();
- final String packageName = data == null ? null : data.getSchemeSpecificPart();
- if (packageName == null) {
- return;
- }
-
- synchronized (Vpn.this) {
- // Avoid race where always-on package has been unset
- if (!packageName.equals(getAlwaysOnPackage())) {
- return;
- }
-
- final String action = intent.getAction();
- Log.i(TAG, "Received broadcast " + action + " for always-on VPN package "
- + packageName + " in user " + mUserHandle);
-
- switch(action) {
- case Intent.ACTION_PACKAGE_REPLACED:
- // Start vpn after app upgrade
- startAlwaysOnVpn();
- break;
- case Intent.ACTION_PACKAGE_REMOVED:
- final boolean isPackageRemoved = !intent.getBooleanExtra(
- Intent.EXTRA_REPLACING, false);
- if (isPackageRemoved) {
- setAlwaysOnPackage(null, false);
- }
- break;
- }
- }
- }
- };
-
- private boolean mIsPackageIntentReceiverRegistered = false;
-
public Vpn(Looper looper, Context context, INetworkManagementService netService,
@UserIdInt int userHandle) {
this(looper, context, netService, userHandle, new SystemServices(context));
@@ -500,7 +461,6 @@
// Prepare this app. The notification will update as a side-effect of updateState().
prepareInternal(packageName);
}
- maybeRegisterPackageChangeReceiverLocked(packageName);
setVpnForcedLocked(mLockdown);
return true;
}
@@ -509,31 +469,6 @@
return packageName == null || VpnConfig.LEGACY_VPN.equals(packageName);
}
- private void unregisterPackageChangeReceiverLocked() {
- if (mIsPackageIntentReceiverRegistered) {
- mContext.unregisterReceiver(mPackageIntentReceiver);
- mIsPackageIntentReceiverRegistered = false;
- }
- }
-
- private void maybeRegisterPackageChangeReceiverLocked(String packageName) {
- // Unregister IntentFilter listening for previous always-on package change
- unregisterPackageChangeReceiverLocked();
-
- if (!isNullOrLegacyVpn(packageName)) {
- mIsPackageIntentReceiverRegistered = true;
-
- IntentFilter intentFilter = new IntentFilter();
- // Protected intent can only be sent by system. No permission required in register.
- intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
- intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
- intentFilter.addDataScheme("package");
- intentFilter.addDataSchemeSpecificPart(packageName, PatternMatcher.PATTERN_LITERAL);
- mContext.registerReceiverAsUser(
- mPackageIntentReceiver, UserHandle.of(mUserHandle), intentFilter, null, null);
- }
- }
-
/**
* @return the package name of the VPN controller responsible for always-on VPN,
* or {@code null} if none is set or always-on VPN is controlled through
@@ -1302,7 +1237,6 @@
setLockdown(false);
mAlwaysOn = false;
- unregisterPackageChangeReceiverLocked();
// Quit any active connections
agentDisconnect();
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index de0f298..25ca278 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -1082,13 +1082,14 @@
assertRunOnServiceThread();
if (!canStartArcUpdateAction(message.getSource(), true)) {
- if (getAvrDeviceInfo() == null) {
+ HdmiDeviceInfo avrDeviceInfo = getAvrDeviceInfo();
+ if (avrDeviceInfo == null) {
// AVR may not have been discovered yet. Delay the message processing.
mDelayedMessageBuffer.add(message);
return true;
}
mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
- if (!isConnectedToArcPort(message.getSource())) {
+ if (!isConnectedToArcPort(avrDeviceInfo.getPhysicalAddress())) {
displayOsd(OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT);
}
return true;
diff --git a/services/core/java/com/android/server/infra/AbstractMultiplePendingRequestsRemoteService.java b/services/core/java/com/android/server/infra/AbstractMultiplePendingRequestsRemoteService.java
index 513a6a3..aaea45e 100644
--- a/services/core/java/com/android/server/infra/AbstractMultiplePendingRequestsRemoteService.java
+++ b/services/core/java/com/android/server/infra/AbstractMultiplePendingRequestsRemoteService.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
+import android.os.IInterface;
import android.util.Slog;
import java.io.PrintWriter;
@@ -29,21 +30,21 @@
* bound.
*
* @param <S> the concrete remote service class
- *
+ * @param <I> the interface of the binder service
* @hide
*/
-public abstract class AbstractMultiplePendingRequestsRemoteService<
- S extends AbstractMultiplePendingRequestsRemoteService<S>>
- extends AbstractRemoteService<S> {
+public abstract class AbstractMultiplePendingRequestsRemoteService<S
+ extends AbstractMultiplePendingRequestsRemoteService<S, I>, I extends IInterface>
+ extends AbstractRemoteService<S, I> {
private final int mInitialCapacity;
- protected ArrayList<PendingRequest<S>> mPendingRequests;
+ protected ArrayList<PendingRequest<S, I>> mPendingRequests;
public AbstractMultiplePendingRequestsRemoteService(@NonNull Context context,
@NonNull String serviceInterface, @NonNull ComponentName componentName, int userId,
- @NonNull VultureCallback callback, boolean bindInstantServiceAllowed, boolean verbose,
- int initialCapacity) {
+ @NonNull VultureCallback<S> callback, boolean bindInstantServiceAllowed,
+ boolean verbose, int initialCapacity) {
super(context, serviceInterface, componentName, userId, callback, bindInstantServiceAllowed,
verbose);
mInitialCapacity = initialCapacity;
@@ -84,7 +85,7 @@
}
@Override // from AbstractRemoteService
- void handlePendingRequestWhileUnBound(@NonNull PendingRequest<S> pendingRequest) {
+ void handlePendingRequestWhileUnBound(@NonNull PendingRequest<S, I> pendingRequest) {
if (mPendingRequests == null) {
mPendingRequests = new ArrayList<>(mInitialCapacity);
}
diff --git a/services/core/java/com/android/server/infra/AbstractRemoteService.java b/services/core/java/com/android/server/infra/AbstractRemoteService.java
index 67b3ecf..41dcf89 100644
--- a/services/core/java/com/android/server/infra/AbstractRemoteService.java
+++ b/services/core/java/com/android/server/infra/AbstractRemoteService.java
@@ -54,13 +54,13 @@
* (no pun intended) example of how to use it.
*
* @param <S> the concrete remote service class
+ * @param <I> the interface of the binder service
*
* @hide
*/
//TODO(b/117779333): improve javadoc above instead of using Autofill as an example
-public abstract class AbstractRemoteService<S extends AbstractRemoteService<S>>
- implements DeathRecipient {
-
+public abstract class AbstractRemoteService<S extends AbstractRemoteService<S, I>,
+ I extends IInterface> implements DeathRecipient {
private static final int MSG_UNBIND = 1;
protected static final int LAST_PRIVATE_MSG = MSG_UNBIND;
@@ -74,11 +74,11 @@
private final Context mContext;
private final Intent mIntent;
- private final VultureCallback mVultureCallback;
+ private final VultureCallback<S> mVultureCallback;
private final int mUserId;
private final ServiceConnection mServiceConnection = new RemoteServiceConnection();
private final boolean mBindInstantServiceAllowed;
- private IInterface mServiceInterface;
+ protected I mService;
private boolean mBinding;
private boolean mDestroyed;
@@ -87,19 +87,21 @@
/**
* Callback called when the service dies.
+ *
+ * @param <T> service class
*/
- public interface VultureCallback {
+ public interface VultureCallback<T> {
/**
* Called when the service dies.
*
* @param service service that died!
*/
- void onServiceDied(AbstractRemoteService<? extends AbstractRemoteService<?>> service);
+ void onServiceDied(T service);
}
// NOTE: must be package-protected so this class is not extend outside
AbstractRemoteService(@NonNull Context context, @NonNull String serviceInterface,
- @NonNull ComponentName componentName, int userId, @NonNull VultureCallback callback,
+ @NonNull ComponentName componentName, int userId, @NonNull VultureCallback<S> callback,
boolean bindInstantServiceAllowed, boolean verbose) {
mContext = context;
mVultureCallback = callback;
@@ -150,7 +152,7 @@
* Gets the base Binder interface from the service.
*/
@NonNull
- protected abstract IInterface getServiceInterface(@NonNull IBinder service);
+ protected abstract I getServiceInterface(@NonNull IBinder service);
/**
* Defines How long after the last interaction with the service we would unbind.
@@ -183,12 +185,14 @@
private void handleBinderDied() {
if (checkIfDestroyed()) return;
- if (mServiceInterface != null) {
- mServiceInterface.asBinder().unlinkToDeath(this, 0);
+ if (mService != null) {
+ mService.asBinder().unlinkToDeath(this, 0);
}
- mServiceInterface = null;
+ mService = null;
mServiceDied = true;
- mVultureCallback.onServiceDied(this);
+ @SuppressWarnings("unchecked") // TODO(b/117779333): fix this warning
+ final S castService = (S) this;
+ mVultureCallback.onServiceDied(castService);
}
// Note: we are dumping without a lock held so this is a bit racy but
@@ -216,12 +220,35 @@
pw.println();
}
- protected void scheduleRequest(@NonNull PendingRequest<S> pendingRequest) {
+ /**
+ * Schedules a "sync" request.
+ *
+ * <p>This request must be responded by the service somehow (typically using a callback),
+ * othewise it will trigger a {@link PendingRequest#onTimeout(AbstractRemoteService)} if the
+ * service doesn't respond.
+ */
+ protected void scheduleRequest(@NonNull PendingRequest<S, I> pendingRequest) {
+ cancelScheduledUnbind();
mHandler.sendMessage(obtainMessage(
AbstractRemoteService::handlePendingRequest, this, pendingRequest));
}
- protected void cancelScheduledUnbind() {
+ /**
+ * Schedules an async request.
+ *
+ * <p>This request is not expecting a callback from the service, hence it's represented by
+ * a simple {@link Runnable}.
+ */
+ protected void scheduleAsyncRequest(@NonNull AsyncRequest<I> request) {
+ cancelScheduledUnbind();
+ // TODO(b/117779333): fix generics below
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ final MyAsyncPendingRequest<S, I> asyncRequest = new MyAsyncPendingRequest(this, request);
+ mHandler.sendMessage(
+ obtainMessage(AbstractRemoteService::handlePendingRequest, this, asyncRequest));
+ }
+
+ private void cancelScheduledUnbind() {
mHandler.removeMessages(MSG_UNBIND);
}
@@ -244,7 +271,7 @@
* Handles a request, either processing it right now when bound, or saving it to be handled when
* bound.
*/
- protected final void handlePendingRequest(@NonNull PendingRequest<S> pendingRequest) {
+ protected final void handlePendingRequest(@NonNull PendingRequest<S, I> pendingRequest) {
if (checkIfDestroyed() || mCompleted) return;
if (!handleIsBound()) {
@@ -263,10 +290,10 @@
/**
* Defines what to do with a request that arrives while not bound to the service.
*/
- abstract void handlePendingRequestWhileUnBound(@NonNull PendingRequest<S> pendingRequest);
+ abstract void handlePendingRequestWhileUnBound(@NonNull PendingRequest<S, I> pendingRequest);
private boolean handleIsBound() {
- return mServiceInterface != null;
+ return mService != null;
}
private void handleEnsureBound() {
@@ -300,9 +327,9 @@
mBinding = false;
if (handleIsBound()) {
handleOnConnectedStateChangedInternal(false);
- if (mServiceInterface != null) {
- mServiceInterface.asBinder().unlinkToDeath(this, 0);
- mServiceInterface = null;
+ if (mService != null) {
+ mService.asBinder().unlinkToDeath(this, 0);
+ mService = null;
}
}
mContext.unbindService(mServiceConnection);
@@ -318,7 +345,7 @@
return;
}
mBinding = false;
- mServiceInterface = getServiceInterface(service);
+ mService = getServiceInterface(service);
try {
service.linkToDeath(AbstractRemoteService.this, 0);
} catch (RemoteException re) {
@@ -332,7 +359,7 @@
@Override
public void onServiceDisconnected(ComponentName name) {
mBinding = true;
- mServiceInterface = null;
+ mService = null;
}
}
@@ -349,10 +376,15 @@
/**
* Base class for the requests serviced by the remote service.
*
+ * <p><b>NOTE: </b> this class is typically used when the service needs to use a callback to
+ * communicate back with the system server. For cases where that's not needed, you should use
+ * {@link AbstractRemoteService#scheduleAsyncRequest(AsyncRequest)} instead.
+ *
* @param <S> the remote service class
+ * @param <I> the interface of the binder service
*/
- public abstract static class PendingRequest<S extends AbstractRemoteService<S>>
- implements Runnable {
+ public abstract static class PendingRequest<S extends AbstractRemoteService<S, I>,
+ I extends IInterface> implements Runnable {
protected final String mTag = getClass().getSimpleName();
protected final Object mLock = new Object();
@@ -366,7 +398,7 @@
@GuardedBy("mLock")
private boolean mCompleted;
- protected PendingRequest(S service) {
+ protected PendingRequest(@NonNull S service) {
mWeakService = new WeakReference<>(service);
mServiceHandler = service.mHandler;
mTimeoutTrigger = () -> {
@@ -452,4 +484,50 @@
return false;
}
}
+
+ /**
+ * Represents a request that does not expect a callback from the remote service.
+ *
+ * @param <I> the interface of the binder service
+ */
+ public interface AsyncRequest<I extends IInterface> {
+
+ /**
+ * Run Forrest, run!
+ */
+ void run(@NonNull I binder) throws RemoteException;
+ }
+
+ private static final class MyAsyncPendingRequest<S extends AbstractRemoteService<S, I>,
+ I extends IInterface> extends PendingRequest<S, I> {
+ private static final String TAG = MyAsyncPendingRequest.class.getSimpleName();
+
+ private final AsyncRequest<I> mRequest;
+
+ protected MyAsyncPendingRequest(@NonNull S service, @NonNull AsyncRequest<I> request) {
+ super(service);
+
+ mRequest = request;
+ }
+
+ @Override
+ public void run() {
+ final S remoteService = getService();
+ if (remoteService == null) return;
+ try {
+ mRequest.run(remoteService.mService);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "exception handling async request (" + this + "): " + e);
+ } finally {
+ finish();
+ }
+ }
+
+ @Override
+ protected void onTimeout(S remoteService) {
+ // TODO(b/117779333): should not happen because we called finish() on run(), although
+ // currently it might be called if the service is destroyed while showing it.
+ Slog.w(TAG, "AsyncPending requested timed out");
+ }
+ }
}
diff --git a/services/core/java/com/android/server/infra/AbstractSinglePendingRequestRemoteService.java b/services/core/java/com/android/server/infra/AbstractSinglePendingRequestRemoteService.java
index 37a1f54..d32f13b 100644
--- a/services/core/java/com/android/server/infra/AbstractSinglePendingRequestRemoteService.java
+++ b/services/core/java/com/android/server/infra/AbstractSinglePendingRequestRemoteService.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
+import android.os.IInterface;
import android.util.Slog;
import java.io.PrintWriter;
@@ -29,17 +30,19 @@
* <p>If another request is received while not bound, the previous one will be canceled.
*
* @param <S> the concrete remote service class
+ * @param <I> the interface of the binder service
*
* @hide
*/
-public abstract class AbstractSinglePendingRequestRemoteService<
- S extends AbstractSinglePendingRequestRemoteService<S>> extends AbstractRemoteService<S> {
+public abstract class AbstractSinglePendingRequestRemoteService<S
+ extends AbstractSinglePendingRequestRemoteService<S, I>, I extends IInterface>
+ extends AbstractRemoteService<S, I> {
- protected PendingRequest<S> mPendingRequest;
+ protected PendingRequest<S, I> mPendingRequest;
public AbstractSinglePendingRequestRemoteService(@NonNull Context context,
@NonNull String serviceInterface, @NonNull ComponentName componentName, int userId,
- @NonNull VultureCallback callback, boolean bindInstantServiceAllowed,
+ @NonNull VultureCallback<S> callback, boolean bindInstantServiceAllowed,
boolean verbose) {
super(context, serviceInterface, componentName, userId, callback, bindInstantServiceAllowed,
verbose);
@@ -48,7 +51,7 @@
@Override // from AbstractRemoteService
void handlePendingRequests() {
if (mPendingRequest != null) {
- final PendingRequest<S> pendingRequest = mPendingRequest;
+ final PendingRequest<S, I> pendingRequest = mPendingRequest;
mPendingRequest = null;
handlePendingRequest(pendingRequest);
}
@@ -70,7 +73,7 @@
}
@Override // from AbstractRemoteService
- void handlePendingRequestWhileUnBound(@NonNull PendingRequest<S> pendingRequest) {
+ void handlePendingRequestWhileUnBound(@NonNull PendingRequest<S, I> pendingRequest) {
if (mPendingRequest != null) {
if (mVerbose) {
Slog.v(mTag, "handlePendingRequestWhileUnBound(): cancelling " + mPendingRequest
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index d96b6cb..e7c3c7b 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -1951,6 +1951,11 @@
}
// Native callback.
+ private int getPointerDisplayId() {
+ return mWindowManagerCallbacks.getPointerDisplayId();
+ }
+
+ // Native callback.
private String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) {
if (!mSystemReady) {
return null;
@@ -2017,6 +2022,8 @@
KeyEvent event, int policyFlags);
public int getPointerLayer();
+
+ public int getPointerDisplayId();
}
/**
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
index 6e08949..26e8270 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
@@ -16,13 +16,17 @@
package com.android.server.locksettings.recoverablekeystore.certificate;
-import static javax.xml.xpath.XPathConstants.NODESET;
-
import android.annotation.IntDef;
import android.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -40,7 +44,6 @@
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertStore;
-import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
@@ -58,15 +61,6 @@
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.xpath.XPath;
-import javax.xml.xpath.XPathExpressionException;
-import javax.xml.xpath.XPathFactory;
-
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import org.xml.sax.SAXException;
/** Utility functions related to parsing and validating public-key certificates. */
public final class CertUtils {
@@ -167,50 +161,63 @@
static List<String> getXmlNodeContents(@MustExist int mustExist, Element rootNode,
String... nodeTags)
throws CertParsingException {
- String expression = String.join("/", nodeTags);
-
- XPath xPath = XPathFactory.newInstance().newXPath();
- NodeList nodeList;
- try {
- nodeList = (NodeList) xPath.compile(expression).evaluate(rootNode, NODESET);
- } catch (XPathExpressionException e) {
- throw new CertParsingException(e);
+ if (nodeTags.length == 0) {
+ throw new CertParsingException("The tag list must not be empty");
}
- switch (mustExist) {
- case MUST_EXIST_UNENFORCED:
- break;
-
- case MUST_EXIST_EXACTLY_ONE:
- if (nodeList.getLength() != 1) {
- throw new CertParsingException(
- "The XML file must contain exactly one node with the path "
- + expression);
- }
- break;
-
- case MUST_EXIST_AT_LEAST_ONE:
- if (nodeList.getLength() == 0) {
- throw new CertParsingException(
- "The XML file must contain at least one node with the path "
- + expression);
- }
- break;
-
- default:
- throw new UnsupportedOperationException(
- "This value of MustExist is not supported: " + mustExist);
+ // Go down through all the intermediate node tags (except the last tag for the leaf nodes).
+ // Note that this implementation requires that at most one path exists for the given
+ // intermediate node tags.
+ Element parent = rootNode;
+ for (int i = 0; i < nodeTags.length - 1; i++) {
+ String tag = nodeTags[i];
+ List<Element> children = getXmlDirectChildren(parent, tag);
+ if ((children.size() == 0 && mustExist != MUST_EXIST_UNENFORCED)
+ || children.size() > 1) {
+ throw new CertParsingException(
+ "The XML file must contain exactly one path with the tag " + tag);
+ }
+ if (children.size() == 0) {
+ return new ArrayList<>();
+ }
+ parent = children.get(0);
}
+ // Then collect the contents of the leaf nodes.
+ List<Element> leafs = getXmlDirectChildren(parent, nodeTags[nodeTags.length - 1]);
+ if (mustExist == MUST_EXIST_EXACTLY_ONE && leafs.size() != 1) {
+ throw new CertParsingException(
+ "The XML file must contain exactly one node with the path "
+ + String.join("/", nodeTags));
+ }
+ if (mustExist == MUST_EXIST_AT_LEAST_ONE && leafs.size() == 0) {
+ throw new CertParsingException(
+ "The XML file must contain at least one node with the path "
+ + String.join("/", nodeTags));
+ }
List<String> result = new ArrayList<>();
- for (int i = 0; i < nodeList.getLength(); i++) {
- Node node = nodeList.item(i);
+ for (Element leaf : leafs) {
// Remove whitespaces and newlines.
- result.add(node.getTextContent().replaceAll("\\s", ""));
+ result.add(leaf.getTextContent().replaceAll("\\s", ""));
}
return result;
}
+ /** Get the direct child nodes with a given tag. */
+ private static List<Element> getXmlDirectChildren(Element parent, String tag) {
+ // Cannot use Element.getElementsByTagName because it will return all descendant elements
+ // with the tag name, i.e. not only the direct child nodes.
+ List<Element> children = new ArrayList<>();
+ NodeList childNodes = parent.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node node = childNodes.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals(tag)) {
+ children.add((Element) node);
+ }
+ }
+ return children;
+ }
+
/**
* Decodes a base64-encoded string.
*
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index f19ecf3..2d2f9e3 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -207,6 +207,7 @@
import com.android.internal.util.XmlUtils;
import com.android.server.DeviceIdleController;
import com.android.server.EventLogTags;
+import com.android.server.IoThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.lights.Light;
@@ -265,7 +266,7 @@
// message codes
static final int MESSAGE_DURATION_REACHED = 2;
- static final int MESSAGE_SAVE_POLICY_FILE = 3;
+ // 3: removed to a different handler
static final int MESSAGE_SEND_RANKING_UPDATE = 4;
static final int MESSAGE_LISTENER_HINTS_CHANGED = 5;
static final int MESSAGE_LISTENER_NOTIFICATION_FILTER_CHANGED = 6;
@@ -573,7 +574,7 @@
mListeners.migrateToXml();
mAssistants.migrateToXml();
mConditionProviders.migrateToXml();
- savePolicyFile();
+ handleSavePolicyFile();
}
mAssistants.ensureAssistant();
@@ -603,34 +604,29 @@
}
}
- /**
- * Saves notification policy
- */
- public void savePolicyFile() {
- mHandler.removeMessages(MESSAGE_SAVE_POLICY_FILE);
- mHandler.sendEmptyMessage(MESSAGE_SAVE_POLICY_FILE);
- }
+ @VisibleForTesting
+ protected void handleSavePolicyFile() {
+ IoThread.getHandler().post(() -> {
+ if (DBG) Slog.d(TAG, "handleSavePolicyFile");
+ synchronized (mPolicyFile) {
+ final FileOutputStream stream;
+ try {
+ stream = mPolicyFile.startWrite();
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to save policy file", e);
+ return;
+ }
- private void handleSavePolicyFile() {
- if (DBG) Slog.d(TAG, "handleSavePolicyFile");
- synchronized (mPolicyFile) {
- final FileOutputStream stream;
- try {
- stream = mPolicyFile.startWrite();
- } catch (IOException e) {
- Slog.w(TAG, "Failed to save policy file", e);
- return;
+ try {
+ writePolicyXml(stream, false /*forBackup*/);
+ mPolicyFile.finishWrite(stream);
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to save policy file, restoring backup", e);
+ mPolicyFile.failWrite(stream);
+ }
}
-
- try {
- writePolicyXml(stream, false /*forBackup*/);
- mPolicyFile.finishWrite(stream);
- } catch (IOException e) {
- Slog.w(TAG, "Failed to save policy file, restoring backup", e);
- mPolicyFile.failWrite(stream);
- }
- }
- BackupManager.dataChanged(getContext().getPackageName());
+ BackupManager.dataChanged(getContext().getPackageName());
+ });
}
private void writePolicyXml(OutputStream stream, boolean forBackup) throws IOException {
@@ -1138,8 +1134,9 @@
}
}
+
mHandler.scheduleOnPackageChanged(removingPackage, changeUserId, pkgList, uidList);
- savePolicyFile();
+ handleSavePolicyFile();
}
}
};
@@ -1206,7 +1203,7 @@
mListeners.onUserRemoved(userId);
mConditionProviders.onUserRemoved(userId);
mAssistants.onUserRemoved(userId);
- savePolicyFile();
+ handleSavePolicyFile();
} else if (action.equals(Intent.ACTION_USER_UNLOCKED)) {
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
mUserProfiles.updateCache(context);
@@ -1462,7 +1459,7 @@
mZenModeHelper.addCallback(new ZenModeHelper.Callback() {
@Override
public void onConfigChanged() {
- savePolicyFile();
+ handleSavePolicyFile();
}
@Override
@@ -1764,7 +1761,7 @@
modifiedChannel, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED);
}
- savePolicyFile();
+ handleSavePolicyFile();
}
private void maybeNotifyChannelOwner(String pkg, int uid, NotificationChannel preUpdate,
@@ -2232,7 +2229,7 @@
Slog.w(TAG, "Can't notify app about app block change", e);
}
- savePolicyFile();
+ handleSavePolicyFile();
}
/**
@@ -2289,7 +2286,7 @@
public void setShowBadge(String pkg, int uid, boolean showBadge) {
checkCallerIsSystem();
mPreferencesHelper.setShowBadge(pkg, uid, showBadge);
- savePolicyFile();
+ handleSavePolicyFile();
}
@Override
@@ -2305,7 +2302,7 @@
if (info != null) {
mPreferencesHelper.setNotificationDelegate(
callingPkg, callingUid, delegate, info.uid);
- savePolicyFile();
+ handleSavePolicyFile();
}
} catch (RemoteException e) {
// :(
@@ -2316,7 +2313,7 @@
public void revokeNotificationDelegate(String callingPkg) {
checkCallerIsSameApp(callingPkg);
mPreferencesHelper.revokeNotificationDelegate(callingPkg, Binder.getCallingUid());
- savePolicyFile();
+ handleSavePolicyFile();
}
@Override
@@ -2351,7 +2348,7 @@
NotificationChannelGroup group) throws RemoteException {
enforceSystemOrSystemUI("Caller not system or systemui");
createNotificationChannelGroup(pkg, uid, group, false, false);
- savePolicyFile();
+ handleSavePolicyFile();
}
@Override
@@ -2364,7 +2361,7 @@
final NotificationChannelGroup group = groups.get(i);
createNotificationChannelGroup(pkg, Binder.getCallingUid(), group, true, false);
}
- savePolicyFile();
+ handleSavePolicyFile();
}
private void createNotificationChannelsImpl(String pkg, int uid,
@@ -2382,7 +2379,7 @@
mPreferencesHelper.getNotificationChannel(pkg, uid, channel.getId(), false),
NOTIFICATION_CHANNEL_OR_GROUP_ADDED);
}
- savePolicyFile();
+ handleSavePolicyFile();
}
@Override
@@ -2427,7 +2424,7 @@
UserHandle.getUserHandleForUid(callingUid),
mPreferencesHelper.getNotificationChannel(pkg, callingUid, channelId, true),
NOTIFICATION_CHANNEL_OR_GROUP_DELETED);
- savePolicyFile();
+ handleSavePolicyFile();
}
@Override
@@ -2469,7 +2466,7 @@
mListeners.notifyNotificationChannelGroupChanged(
pkg, UserHandle.getUserHandleForUid(callingUid), groupToDelete,
NOTIFICATION_CHANNEL_OR_GROUP_DELETED);
- savePolicyFile();
+ handleSavePolicyFile();
}
}
@@ -2602,7 +2599,7 @@
true, UserHandle.getCallingUserId(), packages, uids);
}
- savePolicyFile();
+ handleSavePolicyFile();
}
@@ -3390,7 +3387,7 @@
final ByteArrayInputStream bais = new ByteArrayInputStream(payload);
try {
readPolicyXml(bais, true /*forRestore*/);
- savePolicyFile();
+ handleSavePolicyFile();
} catch (NumberFormatException | XmlPullParserException | IOException e) {
Slog.w(TAG, "applyRestore: error reading payload", e);
}
@@ -3431,7 +3428,7 @@
.setPackage(pkg)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
UserHandle.of(userId), null);
- savePolicyFile();
+ handleSavePolicyFile();
}
} finally {
Binder.restoreCallingIdentity(identity);
@@ -3575,7 +3572,7 @@
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
UserHandle.of(userId), null);
- savePolicyFile();
+ handleSavePolicyFile();
}
} finally {
Binder.restoreCallingIdentity(identity);
@@ -3601,7 +3598,7 @@
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
UserHandle.of(userId), null);
- savePolicyFile();
+ handleSavePolicyFile();
}
} finally {
Binder.restoreCallingIdentity(identity);
@@ -3684,7 +3681,7 @@
verifyPrivilegedListener(token, user, false);
createNotificationChannelGroup(
pkg, getUidForPackageAndUser(pkg, user), group, false, true);
- savePolicyFile();
+ handleSavePolicyFile();
}
@Override
@@ -3733,7 +3730,7 @@
}
if (allow != mLockScreenAllowSecureNotifications) {
mLockScreenAllowSecureNotifications = allow;
- savePolicyFile();
+ handleSavePolicyFile();
}
}
@@ -4492,7 +4489,7 @@
Slog.d(TAG, "Ignored enqueue for snoozed notification " + r.getKey());
}
mSnoozeHelper.update(userId, r);
- savePolicyFile();
+ handleSavePolicyFile();
return false;
}
@@ -4623,7 +4620,7 @@
mSnoozeHelper.snooze(r, mDuration);
}
r.recordSnoozed();
- savePolicyFile();
+ handleSavePolicyFile();
}
}
@@ -4701,7 +4698,7 @@
if (mReason != REASON_SNOOZED) {
final boolean wasSnoozed = mSnoozeHelper.cancel(mUserId, mPkg, mTag, mId);
if (wasSnoozed) {
- savePolicyFile();
+ handleSavePolicyFile();
}
}
}
@@ -5750,9 +5747,6 @@
case MESSAGE_FINISH_TOKEN_TIMEOUT:
handleKillTokenTimeout((ToastRecord) msg.obj);
break;
- case MESSAGE_SAVE_POLICY_FILE:
- handleSavePolicyFile();
- break;
case MESSAGE_SEND_RANKING_UPDATE:
handleSendRankingUpdate();
break;
@@ -6270,7 +6264,7 @@
Slog.d(TAG, String.format("unsnooze event(%s, %s)", key, listenerName));
}
mSnoozeHelper.repost(key);
- savePolicyFile();
+ handleSavePolicyFile();
}
@GuardedBy("mNotificationLock")
diff --git a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
index 65ccecd..7ae2271 100644
--- a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
+++ b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
@@ -19,6 +19,7 @@
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import android.annotation.UserIdInt;
+import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityOptions;
import android.app.AppOpsManager;
@@ -77,18 +78,20 @@
IApplicationThread caller,
String callingPackage,
ComponentName component,
- UserHandle user) throws RemoteException {
+ @UserIdInt int userId,
+ boolean launchMainActivity) throws RemoteException {
Preconditions.checkNotNull(callingPackage);
Preconditions.checkNotNull(component);
- Preconditions.checkNotNull(user);
verifyCallingPackage(callingPackage);
+ final int callerUserId = mInjector.getCallingUserId();
+ final int callingUid = mInjector.getCallingUid();
+
List<UserHandle> allowedTargetUsers = getTargetUserProfilesUnchecked(
- callingPackage, mInjector.getCallingUserId());
- if (!allowedTargetUsers.contains(user)) {
- throw new SecurityException(
- callingPackage + " cannot access unrelated user " + user.getIdentifier());
+ callingPackage, callerUserId);
+ if (!allowedTargetUsers.contains(UserHandle.of(userId))) {
+ throw new SecurityException(callingPackage + " cannot access unrelated user " + userId);
}
// Verify that caller package is starting activity in its own package.
@@ -98,25 +101,43 @@
+ component.getPackageName());
}
- final int callingUid = mInjector.getCallingUid();
-
- // Verify that target activity does handle the intent with ACTION_MAIN and
- // CATEGORY_LAUNCHER as calling startActivityAsUser ignore them if component is present.
- final Intent launchIntent = new Intent(Intent.ACTION_MAIN);
- launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
- launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
- // Only package name is set here, as opposed to component name, because intent action and
- // category are ignored if component name is present while we are resolving intent.
- launchIntent.setPackage(component.getPackageName());
- verifyActivityCanHandleIntentAndExported(launchIntent, component, callingUid, user);
+ // Verify that target activity does handle the intent correctly.
+ final Intent launchIntent = new Intent();
+ if (launchMainActivity) {
+ launchIntent.setAction(Intent.ACTION_MAIN);
+ launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+ // Only package name is set here, as opposed to component name, because intent action
+ // and category are ignored if component name is present while we are resolving intent.
+ launchIntent.setPackage(component.getPackageName());
+ } else {
+ // If the main activity is not being launched and the users are different, the caller
+ // must have the required permission and the users must be in the same profile group
+ // in order to launch any of its own activities.
+ if (callerUserId != userId) {
+ final int permissionFlag = ActivityManager.checkComponentPermission(
+ android.Manifest.permission.INTERACT_ACROSS_PROFILES, callingUid,
+ -1, true);
+ if (permissionFlag != PackageManager.PERMISSION_GRANTED
+ || !mInjector.getUserManager().isSameProfileGroup(callerUserId, userId)) {
+ throw new SecurityException("Attempt to launch activity without required "
+ + android.Manifest.permission.INTERACT_ACROSS_PROFILES + " permission"
+ + " or target user is not in the same profile group.");
+ }
+ }
+ launchIntent.setComponent(component);
+ }
+ verifyActivityCanHandleIntentAndExported(launchIntent, component, callingUid, userId);
launchIntent.setPackage(null);
launchIntent.setComponent(component);
mInjector.getActivityTaskManagerInternal().startActivityAsUser(
caller, callingPackage, launchIntent,
- ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(),
- user.getIdentifier());
+ launchMainActivity
+ ? ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle()
+ : null,
+ userId);
}
private List<UserHandle> getTargetUserProfilesUnchecked(
@@ -163,7 +184,7 @@
* activity is exported.
*/
private void verifyActivityCanHandleIntentAndExported(
- Intent launchIntent, ComponentName component, int callingUid, UserHandle user) {
+ Intent launchIntent, ComponentName component, int callingUid, @UserIdInt int userId) {
final long ident = mInjector.clearCallingIdentity();
try {
final List<ResolveInfo> apps =
@@ -171,7 +192,7 @@
launchIntent,
MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE,
callingUid,
- user.getIdentifier());
+ userId);
final int size = apps.size();
for (int i = 0; i < size; ++i) {
final ActivityInfo activityInfo = apps.get(i).activityInfo;
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 888675c..edab94c 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -86,11 +86,6 @@
import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.content.pm.PackageParser.isApkFile;
-import static android.content.pm.SharedLibraryNames.ANDROID_HIDL_BASE;
-import static android.content.pm.SharedLibraryNames.ANDROID_HIDL_MANAGER;
-import static android.content.pm.SharedLibraryNames.ANDROID_TEST_BASE;
-import static android.content.pm.SharedLibraryNames.ANDROID_TEST_MOCK;
-import static android.content.pm.SharedLibraryNames.ANDROID_TEST_RUNNER;
import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
import static android.os.storage.StorageManager.FLAG_STORAGE_CE;
import static android.os.storage.StorageManager.FLAG_STORAGE_DE;
@@ -242,6 +237,7 @@
import android.os.storage.StorageManagerInternal;
import android.os.storage.VolumeInfo;
import android.os.storage.VolumeRecord;
+import android.provider.MediaStore;
import android.provider.Settings.Global;
import android.provider.Settings.Secure;
import android.security.KeyStore;
@@ -1302,6 +1298,7 @@
final @Nullable String mStorageManagerPackage;
final @Nullable String mSystemTextClassifierPackage;
final @Nullable String mWellbeingPackage;
+ final @Nullable String mDocumenterPackage;
final @NonNull String mServicesSystemSharedLibraryPackageName;
final @NonNull String mSharedSystemSharedLibraryPackageName;
@@ -2094,28 +2091,6 @@
}
}
- @GuardedBy("mPackages")
- private void setupBuiltinSharedLibraryDependenciesLocked() {
- // Builtin libraries don't have versions.
- long version = SharedLibraryInfo.VERSION_UNDEFINED;
-
- SharedLibraryInfo libraryInfo = getSharedLibraryInfoLPr(ANDROID_HIDL_MANAGER, version);
- if (libraryInfo != null) {
- libraryInfo.addDependency(getSharedLibraryInfoLPr(ANDROID_HIDL_BASE, version));
- }
-
- libraryInfo = getSharedLibraryInfoLPr(ANDROID_TEST_RUNNER, version);
- if (libraryInfo != null) {
- libraryInfo.addDependency(getSharedLibraryInfoLPr(ANDROID_TEST_MOCK, version));
- libraryInfo.addDependency(getSharedLibraryInfoLPr(ANDROID_TEST_BASE, version));
- }
-
- libraryInfo = getSharedLibraryInfoLPr(ANDROID_TEST_MOCK, version);
- if (libraryInfo != null) {
- libraryInfo.addDependency(getSharedLibraryInfoLPr(ANDROID_TEST_BASE, version));
- }
- }
-
public PackageManagerService(Context context, Installer installer,
boolean factoryTest, boolean onlyCore) {
LockGuard.installLock(mPackages, LockGuard.INDEX_PACKAGES);
@@ -2223,17 +2198,32 @@
Watchdog.getInstance().addThread(mHandler, WATCHDOG_TIMEOUT);
mInstantAppRegistry = new InstantAppRegistry(this);
- ArrayMap<String, String> libConfig = systemConfig.getSharedLibraries();
+ ArrayMap<String, SystemConfig.SharedLibraryEntry> libConfig
+ = systemConfig.getSharedLibraries();
final int builtInLibCount = libConfig.size();
for (int i = 0; i < builtInLibCount; i++) {
String name = libConfig.keyAt(i);
- String path = libConfig.valueAt(i);
- addSharedLibraryLPw(path, null, null, name, SharedLibraryInfo.VERSION_UNDEFINED,
- SharedLibraryInfo.TYPE_BUILTIN, PLATFORM_PACKAGE_NAME, 0);
+ SystemConfig.SharedLibraryEntry entry = libConfig.valueAt(i);
+ addSharedLibraryLPw(entry.filename, null, null, name,
+ SharedLibraryInfo.VERSION_UNDEFINED, SharedLibraryInfo.TYPE_BUILTIN,
+ PLATFORM_PACKAGE_NAME, 0);
}
- // Builtin libraries cannot encode their dependency where they are
- // defined, so fix that now.
- setupBuiltinSharedLibraryDependenciesLocked();
+
+ // Now that we have added all the libraries, iterate again to add dependency
+ // information IFF their dependencies are added.
+ long undefinedVersion = SharedLibraryInfo.VERSION_UNDEFINED;
+ for (int i = 0; i < builtInLibCount; i++) {
+ String name = libConfig.keyAt(i);
+ SystemConfig.SharedLibraryEntry entry = libConfig.valueAt(i);
+ final int dependencyCount = entry.dependencies.length;
+ for (int j = 0; j < dependencyCount; j++) {
+ final SharedLibraryInfo dependency =
+ getSharedLibraryInfoLPr(entry.dependencies[j], undefinedVersion);
+ if (dependency != null) {
+ getSharedLibraryInfoLPr(name, undefinedVersion).addDependency(dependency);
+ }
+ }
+ }
SELinuxMMAC.readInstallPolicy();
@@ -2792,6 +2782,7 @@
mSystemTextClassifierPackage = getSystemTextClassifierPackageName();
mWellbeingPackage = getWellbeingPackageName();
+ mDocumenterPackage = getDocumenterPackageName();
// Now that we know all of the shared libraries, update all clients to have
// the correct library paths.
@@ -17936,7 +17927,7 @@
final int removedUserId = (user != null) ? user.getIdentifier()
: UserHandle.USER_ALL;
- clearPackageStateForUserLIF(ps, removedUserId, outInfo);
+ clearPackageStateForUserLIF(ps, removedUserId, outInfo, flags);
markPackageUninstalledForUserLPw(ps, user);
scheduleWritePackageRestrictionsLocked(user);
return;
@@ -17966,7 +17957,7 @@
// we need to do is clear this user's data and save that
// it is uninstalled.
if (DEBUG_REMOVE) Slog.d(TAG, "Still installed by other users");
- clearPackageStateForUserLIF(ps, user.getIdentifier(), outInfo);
+ clearPackageStateForUserLIF(ps, user.getIdentifier(), outInfo, flags);
scheduleWritePackageRestrictionsLocked(user);
return;
} else {
@@ -17982,7 +17973,7 @@
// we need to do is clear this user's data and save that
// it is uninstalled.
if (DEBUG_REMOVE) Slog.d(TAG, "Deleting system app");
- clearPackageStateForUserLIF(ps, user.getIdentifier(), outInfo);
+ clearPackageStateForUserLIF(ps, user.getIdentifier(), outInfo, flags);
scheduleWritePackageRestrictionsLocked(user);
return;
}
@@ -18099,7 +18090,7 @@
}
private void clearPackageStateForUserLIF(PackageSetting ps, int userId,
- PackageRemovedInfo outInfo) {
+ PackageRemovedInfo outInfo, int flags) {
final PackageParser.Package pkg;
synchronized (mPackages) {
pkg = mPackages.get(ps.name);
@@ -18124,6 +18115,14 @@
}
resetUserChangesToRuntimePermissionsAndFlagsLPw(ps, nextUserId);
}
+ // Also delete contributed media, when requested
+ if ((flags & PackageManager.DELETE_CONTRIBUTED_MEDIA) != 0) {
+ try {
+ MediaStore.deleteContributedMedia(mContext, ps.name, UserHandle.of(nextUserId));
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to delete contributed media for " + ps.name, e);
+ }
+ }
}
if (outInfo != null) {
@@ -19570,6 +19569,22 @@
return mContext.getString(R.string.config_defaultTextClassifierPackage);
}
+ private @Nullable String getDocumenterPackageName() {
+ final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+
+ final List<ResolveInfo> matches = queryIntentActivitiesInternal(intent, null,
+ MATCH_SYSTEM_ONLY | MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE
+ | MATCH_DISABLED_COMPONENTS,
+ UserHandle.myUserId());
+ if (matches.size() == 1) {
+ return matches.get(0).getComponentInfo().packageName;
+ } else {
+ Slog.e(TAG, "There should probably be exactly one documenter; found "
+ + matches.size() + ": matches=" + matches);
+ return null;
+ }
+ }
+
@Override
public String getWellbeingPackageName() {
return mContext.getString(R.string.config_defaultWellbeingPackage);
@@ -22733,6 +22748,8 @@
return mRequiredPermissionControllerPackage;
case PackageManagerInternal.PACKAGE_WELLBEING:
return mWellbeingPackage;
+ case PackageManagerInternal.PACKAGE_DOCUMENTER:
+ return mDocumenterPackage;
}
return null;
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index c18ca25..d1d5818 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -31,6 +31,7 @@
import android.app.IStopUserCallback;
import android.app.KeyguardManager;
import android.app.PendingIntent;
+import android.app.admin.DevicePolicyEventLogger;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -73,6 +74,7 @@
import android.os.storage.StorageManager;
import android.security.GateKeeper;
import android.service.gatekeeper.IGateKeeperService;
+import android.stats.devicepolicy.DevicePolicyEnums;
import android.util.AtomicFile;
import android.util.IntArray;
import android.util.Log;
@@ -100,6 +102,8 @@
import com.android.server.storage.DeviceStorageMonitorInternal;
import com.android.server.wm.ActivityTaskManagerInternal;
+import libcore.io.IoUtils;
+
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
@@ -121,8 +125,6 @@
import java.util.List;
import java.util.Objects;
-import libcore.io.IoUtils;
-
/**
* Service for {@link UserManager}.
*
@@ -173,6 +175,8 @@
private static final String TAG_ENTRY = "entry";
private static final String TAG_VALUE = "value";
private static final String TAG_SEED_ACCOUNT_OPTIONS = "seedAccountOptions";
+ private static final String TAG_LAST_REQUEST_QUIET_MODE_ENABLED_CALL =
+ "lastRequestQuietModeEnabledCall";
private static final String ATTR_KEY = "key";
private static final String ATTR_VALUE_TYPE = "type";
private static final String ATTR_MULTIPLE = "m";
@@ -268,6 +272,16 @@
/** Elapsed realtime since boot when the user was unlocked. */
long unlockRealtime;
+ private long mLastRequestQuietModeEnabledMillis;
+
+ void setLastRequestQuietModeEnabledMillis(long millis) {
+ mLastRequestQuietModeEnabledMillis = millis;
+ }
+
+ long getLastRequestQuietModeEnabledMillis() {
+ return mLastRequestQuietModeEnabledMillis;
+ }
+
void clearSeedAccountData() {
seedAccountName = null;
seedAccountType = null;
@@ -389,8 +403,8 @@
final IntentSender target = intent.getParcelableExtra(Intent.EXTRA_INTENT);
final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL);
// Call setQuietModeEnabled on bg thread to avoid ANR
- BackgroundThread.getHandler()
- .post(() -> setQuietModeEnabled(userHandle, false, target));
+ BackgroundThread.getHandler().post(() ->
+ setQuietModeEnabled(userHandle, false, target, /* callingPackage */ null));
}
};
@@ -834,21 +848,24 @@
ensureCanModifyQuietMode(callingPackage, Binder.getCallingUid(), target != null);
final long identity = Binder.clearCallingIdentity();
try {
+ boolean result = false;
if (enableQuietMode) {
- setQuietModeEnabled(userHandle, true /* enableQuietMode */, target);
- return true;
+ setQuietModeEnabled(
+ userHandle, true /* enableQuietMode */, target, callingPackage);
+ result = true;
} else {
boolean needToShowConfirmCredential =
mLockPatternUtils.isSecure(userHandle)
&& !StorageManager.isUserKeyUnlocked(userHandle);
if (needToShowConfirmCredential) {
showConfirmCredentialToDisableQuietMode(userHandle, target);
- return false;
} else {
- setQuietModeEnabled(userHandle, false /* enableQuietMode */, target);
- return true;
+ setQuietModeEnabled(
+ userHandle, false /* enableQuietMode */, target, callingPackage);
+ result = true;
}
}
+ return result;
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -894,8 +911,8 @@
+ "default launcher nor has MANAGE_USERS/MODIFY_QUIET_MODE permission");
}
- private void setQuietModeEnabled(
- int userHandle, boolean enableQuietMode, IntentSender target) {
+ private void setQuietModeEnabled(int userHandle, boolean enableQuietMode,
+ IntentSender target, @Nullable String callingPackage) {
final UserInfo profile, parent;
final UserData profileUserData;
synchronized (mUsersLock) {
@@ -927,6 +944,7 @@
ActivityManager.getService().startUserInBackgroundWithListener(
userHandle, callback);
}
+ logQuietModeEnabled(userHandle, enableQuietMode, callingPackage);
} catch (RemoteException e) {
// Should not happen, same process.
e.rethrowAsRuntimeException();
@@ -935,6 +953,28 @@
enableQuietMode);
}
+ private void logQuietModeEnabled(int userHandle, boolean enableQuietMode,
+ @Nullable String callingPackage) {
+ UserData userData;
+ synchronized (mUsersLock) {
+ userData = getUserDataLU(userHandle);
+ }
+ if (userData == null) {
+ return;
+ }
+ final long now = System.currentTimeMillis();
+ final long period = (userData.getLastRequestQuietModeEnabledMillis() != 0L
+ ? now - userData.getLastRequestQuietModeEnabledMillis()
+ : now - userData.info.creationTime);
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.REQUEST_QUIET_MODE_ENABLED)
+ .setStrings(callingPackage)
+ .setBoolean(enableQuietMode)
+ .setTimePeriod(period)
+ .write();
+ userData.setLastRequestQuietModeEnabledMillis(now);
+ }
+
@Override
public boolean isQuietModeEnabled(int userHandle) {
synchronized (mPackagesLock) {
@@ -2314,6 +2354,12 @@
serializer.endTag(null, TAG_SEED_ACCOUNT_OPTIONS);
}
+ if (userData.getLastRequestQuietModeEnabledMillis() != 0L) {
+ serializer.startTag(/* namespace */ null, TAG_LAST_REQUEST_QUIET_MODE_ENABLED_CALL);
+ serializer.text(String.valueOf(userData.getLastRequestQuietModeEnabledMillis()));
+ serializer.endTag(/* namespace */ null, TAG_LAST_REQUEST_QUIET_MODE_ENABLED_CALL);
+ }
+
serializer.endTag(null, TAG_USER);
serializer.endDocument();
@@ -2408,6 +2454,7 @@
String iconPath = null;
long creationTime = 0L;
long lastLoggedInTime = 0L;
+ long lastRequestQuietModeEnabledTimestamp = 0L;
String lastLoggedInFingerprint = null;
int profileGroupId = UserInfo.NO_PROFILE_GROUP_ID;
int profileBadge = 0;
@@ -2494,6 +2541,11 @@
} else if (TAG_SEED_ACCOUNT_OPTIONS.equals(tag)) {
seedAccountOptions = PersistableBundle.restoreFromXml(parser);
persistSeedData = true;
+ } else if (TAG_LAST_REQUEST_QUIET_MODE_ENABLED_CALL.equals(tag)) {
+ type = parser.next();
+ if (type == XmlPullParser.TEXT) {
+ lastRequestQuietModeEnabledTimestamp = Long.parseLong(parser.getText());
+ }
}
}
}
@@ -2518,6 +2570,7 @@
userData.seedAccountType = seedAccountType;
userData.persistSeedData = persistSeedData;
userData.seedAccountOptions = seedAccountOptions;
+ userData.setLastRequestQuietModeEnabledMillis(lastRequestQuietModeEnabledTimestamp);
synchronized (mRestrictionsLock) {
if (baseRestrictions != null) {
diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java
index 3a74ab5..36b7269 100644
--- a/services/core/java/com/android/server/pm/dex/DexManager.java
+++ b/services/core/java/com/android/server/pm/dex/DexManager.java
@@ -16,6 +16,11 @@
package com.android.server.pm.dex;
+import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
+import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
+import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
+
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -26,9 +31,9 @@
import android.os.Build;
import android.os.FileUtils;
import android.os.RemoteException;
-import android.os.storage.StorageManager;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.os.storage.StorageManager;
import android.provider.Settings.Global;
import android.util.Log;
import android.util.Slog;
@@ -48,18 +53,14 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
-import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
-import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
-import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
-
/**
* This class keeps track of how dex files are used.
* Every time it gets a notification about a dex file being loaded it tracks
@@ -89,6 +90,12 @@
// encode and save the dex usage data.
private final PackageDexUsage mPackageDexUsage;
+ // PackageDynamicCodeLoading handles recording of dynamic code loading -
+ // which is similar to PackageDexUsage but records a different aspect of the data.
+ // (It additionally includes DEX files loaded with unsupported class loaders, and doesn't
+ // record class loaders or ISAs.)
+ private final PackageDynamicCodeLoading mPackageDynamicCodeLoading;
+
private final IPackageManager mPackageManager;
private final PackageDexOptimizer mPackageDexOptimizer;
private final Object mInstallLock;
@@ -126,14 +133,15 @@
public DexManager(Context context, IPackageManager pms, PackageDexOptimizer pdo,
Installer installer, Object installLock, Listener listener) {
- mContext = context;
- mPackageCodeLocationsCache = new HashMap<>();
- mPackageDexUsage = new PackageDexUsage();
- mPackageManager = pms;
- mPackageDexOptimizer = pdo;
- mInstaller = installer;
- mInstallLock = installLock;
- mListener = listener;
+ mContext = context;
+ mPackageCodeLocationsCache = new HashMap<>();
+ mPackageDexUsage = new PackageDexUsage();
+ mPackageDynamicCodeLoading = new PackageDynamicCodeLoading();
+ mPackageManager = pms;
+ mPackageDexOptimizer = pdo;
+ mInstaller = installer;
+ mInstallLock = installLock;
+ mListener = listener;
}
public void systemReady() {
@@ -207,7 +215,6 @@
Slog.i(TAG, loadingAppInfo.packageName +
" uses unsupported class loader in " + classLoaderNames);
}
- return;
}
int dexPathIndex = 0;
@@ -236,15 +243,24 @@
continue;
}
- // Record dex file usage. If the current usage is a new pattern (e.g. new secondary,
- // or UsedByOtherApps), record will return true and we trigger an async write
- // to disk to make sure we don't loose the data in case of a reboot.
+ if (mPackageDynamicCodeLoading.record(searchResult.mOwningPackageName, dexPath,
+ PackageDynamicCodeLoading.FILE_TYPE_DEX, loaderUserId,
+ loadingAppInfo.packageName)) {
+ mPackageDynamicCodeLoading.maybeWriteAsync();
+ }
- String classLoaderContext = classLoaderContexts[dexPathIndex];
- if (mPackageDexUsage.record(searchResult.mOwningPackageName,
- dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit,
- loadingAppInfo.packageName, classLoaderContext)) {
- mPackageDexUsage.maybeWriteAsync();
+ if (classLoaderContexts != null) {
+
+ // Record dex file usage. If the current usage is a new pattern (e.g. new
+ // secondary, or UsedByOtherApps), record will return true and we trigger an
+ // async write to disk to make sure we don't loose the data in case of a reboot.
+
+ String classLoaderContext = classLoaderContexts[dexPathIndex];
+ if (mPackageDexUsage.record(searchResult.mOwningPackageName,
+ dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit,
+ loadingAppInfo.packageName, classLoaderContext)) {
+ mPackageDexUsage.maybeWriteAsync();
+ }
}
} else {
// If we can't find the owner of the dex we simply do not track it. The impact is
@@ -268,8 +284,8 @@
loadInternal(existingPackages);
} catch (Exception e) {
mPackageDexUsage.clear();
- Slog.w(TAG, "Exception while loading package dex usage. " +
- "Starting with a fresh state.", e);
+ mPackageDynamicCodeLoading.clear();
+ Slog.w(TAG, "Exception while loading. Starting with a fresh state.", e);
}
}
@@ -311,15 +327,24 @@
* all usage information for the package will be removed.
*/
public void notifyPackageDataDestroyed(String packageName, int userId) {
- boolean updated = userId == UserHandle.USER_ALL
- ? mPackageDexUsage.removePackage(packageName)
- : mPackageDexUsage.removeUserPackage(packageName, userId);
// In case there was an update, write the package use info to disk async.
- // Note that we do the writing here and not in PackageDexUsage in order to be
+ // Note that we do the writing here and not in the lower level classes in order to be
// consistent with other methods in DexManager (e.g. reconcileSecondaryDexFiles performs
// multiple updates in PackageDexUsage before writing it).
- if (updated) {
- mPackageDexUsage.maybeWriteAsync();
+ if (userId == UserHandle.USER_ALL) {
+ if (mPackageDexUsage.removePackage(packageName)) {
+ mPackageDexUsage.maybeWriteAsync();
+ }
+ if (mPackageDynamicCodeLoading.removePackage(packageName)) {
+ mPackageDynamicCodeLoading.maybeWriteAsync();
+ }
+ } else {
+ if (mPackageDexUsage.removeUserPackage(packageName, userId)) {
+ mPackageDexUsage.maybeWriteAsync();
+ }
+ if (mPackageDynamicCodeLoading.removeUserPackage(packageName, userId)) {
+ mPackageDynamicCodeLoading.maybeWriteAsync();
+ }
}
}
@@ -388,8 +413,23 @@
}
}
- mPackageDexUsage.read();
- mPackageDexUsage.syncData(packageToUsersMap, packageToCodePaths);
+ try {
+ mPackageDexUsage.read();
+ mPackageDexUsage.syncData(packageToUsersMap, packageToCodePaths);
+ } catch (Exception e) {
+ mPackageDexUsage.clear();
+ Slog.w(TAG, "Exception while loading package dex usage. "
+ + "Starting with a fresh state.", e);
+ }
+
+ try {
+ mPackageDynamicCodeLoading.read();
+ mPackageDynamicCodeLoading.syncData(packageToUsersMap);
+ } catch (Exception e) {
+ mPackageDynamicCodeLoading.clear();
+ Slog.w(TAG, "Exception while loading package dynamic code usage. "
+ + "Starting with a fresh state.", e);
+ }
}
/**
@@ -415,10 +455,16 @@
* TODO(calin): maybe we should not (prune) so we can have an accurate view when we try
* to access the package use.
*/
+ @VisibleForTesting
/*package*/ boolean hasInfoOnPackage(String packageName) {
return mPackageDexUsage.getPackageUseInfo(packageName) != null;
}
+ @VisibleForTesting
+ /*package*/ PackageDynamicCode getPackageDynamicCodeInfo(String packageName) {
+ return mPackageDynamicCodeLoading.getPackageDynamicCodeInfo(packageName);
+ }
+
/**
* Perform dexopt on with the given {@code options} on the secondary dex files.
* @return true if all secondary dex files were processed successfully (compiled or skipped
@@ -652,7 +698,7 @@
// to load dex files through it.
try {
String dexPathReal = PackageManagerServiceUtils.realpath(new File(dexPath));
- if (dexPathReal != dexPath) {
+ if (!dexPath.equals(dexPathReal)) {
Slog.d(TAG, "Dex loaded with symlink. dexPath=" +
dexPath + " dexPathReal=" + dexPathReal);
}
@@ -675,6 +721,7 @@
*/
public void writePackageDexUsageNow() {
mPackageDexUsage.writeNow();
+ mPackageDynamicCodeLoading.writeNow();
}
private void registerSettingObserver() {
diff --git a/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
new file mode 100644
index 0000000..f74aa1d
--- /dev/null
+++ b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
@@ -0,0 +1,612 @@
+/*
+ * Copyright 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.pm.dex;
+
+import android.util.AtomicFile;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastPrintWriter;
+import com.android.server.pm.AbstractStatsBase;
+
+import libcore.io.IoUtils;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Stats file which stores information about secondary code files that are dynamically loaded.
+ */
+class PackageDynamicCodeLoading extends AbstractStatsBase<Void> {
+ // Type code to indicate a secondary file containing DEX code. (The char value is how it
+ // is represented in the text file format.)
+ static final int FILE_TYPE_DEX = 'D';
+
+ private static final String TAG = "PackageDynamicCodeLoading";
+
+ private static final String FILE_VERSION_HEADER = "DCL1";
+ private static final String PACKAGE_PREFIX = "P:";
+
+ private static final char FIELD_SEPARATOR = ':';
+ private static final String PACKAGE_SEPARATOR = ",";
+
+ /**
+ * Regular expression to match the expected format of an input line describing one file.
+ * <p>Example: {@code D:10:package.name1,package.name2:/escaped/path}
+ * <p>The capturing groups are the file type, user ID, loading packages and escaped file path
+ * (in that order).
+ * <p>See {@link #write(OutputStream, Map)} below for more details of the format.
+ */
+ private static final Pattern PACKAGE_LINE_PATTERN =
+ Pattern.compile("([A-Z]):([0-9]+):([^:]*):(.*)");
+
+ private final Object mLock = new Object();
+
+ // Map from package name to data about loading of dynamic code files owned by that package.
+ // (Apps may load code files owned by other packages, subject to various access
+ // constraints.)
+ // Any PackageDynamicCode in this map will be non-empty.
+ @GuardedBy("mLock")
+ private Map<String, PackageDynamicCode> mPackageMap = new HashMap<>();
+
+ PackageDynamicCodeLoading() {
+ super("package-dcl.list", "PackageDynamicCodeLoading_DiskWriter", false);
+ }
+
+ /**
+ * Record dynamic code loading from a file.
+ *
+ * Note this is called when an app loads dex files and as such it should return
+ * as fast as possible.
+ *
+ * @param owningPackageName the package owning the file path
+ * @param filePath the path of the dex files being loaded
+ * @param fileType the type of code loading
+ * @param ownerUserId the user id which runs the code loading the file
+ * @param loadingPackageName the package performing the load
+ * @return whether new information has been recorded
+ * @throws IllegalArgumentException if clearly invalid information is detected
+ */
+ boolean record(String owningPackageName, String filePath, int fileType, int ownerUserId,
+ String loadingPackageName) {
+ if (fileType != FILE_TYPE_DEX) {
+ throw new IllegalArgumentException("Bad file type: " + fileType);
+ }
+ synchronized (mLock) {
+ PackageDynamicCode packageInfo = mPackageMap.get(owningPackageName);
+ if (packageInfo == null) {
+ packageInfo = new PackageDynamicCode();
+ mPackageMap.put(owningPackageName, packageInfo);
+ }
+ return packageInfo.add(filePath, (char) fileType, ownerUserId, loadingPackageName);
+ }
+ }
+
+ /**
+ * Return all packages that contain records of secondary dex files. (Note that data updates
+ * asynchronously, so {@link #getPackageDynamicCodeInfo} may still return null if passed
+ * one of these package names.)
+ */
+ Set<String> getAllPackagesWithDynamicCodeLoading() {
+ synchronized (mLock) {
+ return new HashSet<>(mPackageMap.keySet());
+ }
+ }
+
+ /**
+ * Return information about the dynamic code file usage of the specified package,
+ * or null if there is currently no usage information. The object returned is a copy of the
+ * live information that is not updated.
+ */
+ PackageDynamicCode getPackageDynamicCodeInfo(String packageName) {
+ synchronized (mLock) {
+ PackageDynamicCode info = mPackageMap.get(packageName);
+ return info == null ? null : new PackageDynamicCode(info);
+ }
+ }
+
+ /**
+ * Remove all information about all packages.
+ */
+ void clear() {
+ synchronized (mLock) {
+ mPackageMap.clear();
+ }
+ }
+
+ /**
+ * Remove the data associated with package {@code packageName}. Affects all users.
+ * @return true if the package usage was found and removed successfully
+ */
+ boolean removePackage(String packageName) {
+ synchronized (mLock) {
+ return mPackageMap.remove(packageName) != null;
+ }
+ }
+
+ /**
+ * Remove all the records about package {@code packageName} belonging to user {@code userId}.
+ * @return whether any data was actually removed
+ */
+ boolean removeUserPackage(String packageName, int userId) {
+ synchronized (mLock) {
+ PackageDynamicCode packageDynamicCode = mPackageMap.get(packageName);
+ if (packageDynamicCode == null) {
+ return false;
+ }
+ if (packageDynamicCode.removeUser(userId)) {
+ if (packageDynamicCode.mFileUsageMap.isEmpty()) {
+ mPackageMap.remove(packageName);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Remove the specified dynamic code file record belonging to the package {@code packageName}
+ * and user {@code userId}.
+ * @return whether data was actually removed
+ */
+ boolean removeFile(String packageName, String filePath, int userId) {
+ synchronized (mLock) {
+ PackageDynamicCode packageDynamicCode = mPackageMap.get(packageName);
+ if (packageDynamicCode == null) {
+ return false;
+ }
+ if (packageDynamicCode.removeFile(filePath, userId)) {
+ if (packageDynamicCode.mFileUsageMap.isEmpty()) {
+ mPackageMap.remove(packageName);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Syncs data with the set of installed packages. Data about packages that are no longer
+ * installed is removed.
+ * @param packageToUsersMap a map from all existing package names to the users who have the
+ * package installed
+ */
+ void syncData(Map<String, Set<Integer>> packageToUsersMap) {
+ synchronized (mLock) {
+ Iterator<Entry<String, PackageDynamicCode>> it = mPackageMap.entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<String, PackageDynamicCode> entry = it.next();
+ Set<Integer> packageUsers = packageToUsersMap.get(entry.getKey());
+ if (packageUsers == null) {
+ it.remove();
+ } else {
+ PackageDynamicCode packageDynamicCode = entry.getValue();
+ packageDynamicCode.syncData(packageToUsersMap, packageUsers);
+ if (packageDynamicCode.mFileUsageMap.isEmpty()) {
+ it.remove();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Request that data be written to persistent file at the next time allowed by write-limiting.
+ */
+ void maybeWriteAsync() {
+ super.maybeWriteAsync(null);
+ }
+
+ /**
+ * Writes data to persistent file immediately.
+ */
+ void writeNow() {
+ super.writeNow(null);
+ }
+
+ @Override
+ protected final void writeInternal(Void data) {
+ AtomicFile file = getFile();
+ FileOutputStream output = null;
+ try {
+ output = file.startWrite();
+ write(output);
+ file.finishWrite(output);
+ } catch (IOException e) {
+ file.failWrite(output);
+ Slog.e(TAG, "Failed to write dynamic usage for secondary code files.", e);
+ }
+ }
+
+ @VisibleForTesting
+ void write(OutputStream output) throws IOException {
+ // Make a deep copy to avoid holding the lock while writing to disk.
+ Map<String, PackageDynamicCode> copiedMap;
+ synchronized (mLock) {
+ copiedMap = new HashMap<>(mPackageMap.size());
+ for (Entry<String, PackageDynamicCode> entry : mPackageMap.entrySet()) {
+ PackageDynamicCode copiedValue = new PackageDynamicCode(entry.getValue());
+ copiedMap.put(entry.getKey(), copiedValue);
+ }
+ }
+
+ write(output, copiedMap);
+ }
+
+ /**
+ * Write the dynamic code loading data as a text file to {@code output}. The file format begins
+ * with a line indicating the file type and version - {@link #FILE_VERSION_HEADER}.
+ * <p>There is then one section for each owning package, introduced by a line beginning "P:".
+ * This is followed by a line for each file owned by the package this is dynamically loaded,
+ * containing the file type, user ID, loading package names and full path (with newlines and
+ * backslashes escaped - see {@link #escape}).
+ * <p>For example:
+ * <pre>{@code
+ * DCL1
+ * P:first.owning.package
+ * D:0:loading.package_1,loading.package_2:/path/to/file
+ * D:10:loading.package_1:/another/file
+ * P:second.owning.package
+ * D:0:loading.package:/third/file
+ * }</pre>
+ */
+ private static void write(OutputStream output, Map<String, PackageDynamicCode> packageMap)
+ throws IOException {
+ PrintWriter writer = new FastPrintWriter(output);
+
+ writer.println(FILE_VERSION_HEADER);
+ for (Entry<String, PackageDynamicCode> packageEntry : packageMap.entrySet()) {
+ writer.print(PACKAGE_PREFIX);
+ writer.println(packageEntry.getKey());
+
+ Map<String, DynamicCodeFile> mFileUsageMap = packageEntry.getValue().mFileUsageMap;
+ for (Entry<String, DynamicCodeFile> fileEntry : mFileUsageMap.entrySet()) {
+ String path = fileEntry.getKey();
+ DynamicCodeFile dynamicCodeFile = fileEntry.getValue();
+
+ writer.print(dynamicCodeFile.mFileType);
+ writer.print(FIELD_SEPARATOR);
+ writer.print(dynamicCodeFile.mUserId);
+ writer.print(FIELD_SEPARATOR);
+
+ String prefix = "";
+ for (String packageName : dynamicCodeFile.mLoadingPackages) {
+ writer.print(prefix);
+ writer.print(packageName);
+ prefix = PACKAGE_SEPARATOR;
+ }
+
+ writer.print(FIELD_SEPARATOR);
+ writer.println(escape(path));
+ }
+ }
+
+ writer.flush();
+ if (writer.checkError()) {
+ throw new IOException("Writer failed");
+ }
+ }
+
+ /**
+ * Read data from the persistent file. Replaces existing data completely if successful.
+ */
+ void read() {
+ super.read(null);
+ }
+
+ @Override
+ protected final void readInternal(Void data) {
+ AtomicFile file = getFile();
+
+ FileInputStream stream = null;
+ try {
+ stream = file.openRead();
+ read(stream);
+ } catch (FileNotFoundException expected) {
+ // The file may not be there. E.g. When we first take the OTA with this feature.
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to parse dynamic usage for secondary code files.", e);
+ } finally {
+ IoUtils.closeQuietly(stream);
+ }
+ }
+
+ @VisibleForTesting
+ void read(InputStream stream) throws IOException {
+ Map<String, PackageDynamicCode> newPackageMap = new HashMap<>();
+ read(stream, newPackageMap);
+ synchronized (mLock) {
+ mPackageMap = newPackageMap;
+ }
+ }
+
+ private static void read(InputStream stream, Map<String, PackageDynamicCode> packageMap)
+ throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+
+ String versionLine = reader.readLine();
+ if (!FILE_VERSION_HEADER.equals(versionLine)) {
+ throw new IOException("Incorrect version line: " + versionLine);
+ }
+
+ String line = reader.readLine();
+ if (line != null && !line.startsWith(PACKAGE_PREFIX)) {
+ throw new IOException("Malformed line: " + line);
+ }
+
+ while (line != null) {
+ String packageName = line.substring(PACKAGE_PREFIX.length());
+
+ PackageDynamicCode packageInfo = new PackageDynamicCode();
+ while (true) {
+ line = reader.readLine();
+ if (line == null || line.startsWith(PACKAGE_PREFIX)) {
+ break;
+ }
+ readFileInfo(line, packageInfo);
+ }
+
+ if (!packageInfo.mFileUsageMap.isEmpty()) {
+ packageMap.put(packageName, packageInfo);
+ }
+ }
+ }
+
+ private static void readFileInfo(String line, PackageDynamicCode output) throws IOException {
+ try {
+ Matcher matcher = PACKAGE_LINE_PATTERN.matcher(line);
+ if (!matcher.matches()) {
+ throw new IOException("Malformed line: " + line);
+ }
+
+ char type = matcher.group(1).charAt(0);
+ int user = Integer.parseInt(matcher.group(2));
+ String[] packages = matcher.group(3).split(PACKAGE_SEPARATOR);
+ String path = unescape(matcher.group(4));
+
+ if (packages.length == 0) {
+ throw new IOException("Malformed line: " + line);
+ }
+ if (type != FILE_TYPE_DEX) {
+ throw new IOException("Unknown file type: " + line);
+ }
+
+ output.mFileUsageMap.put(path, new DynamicCodeFile(type, user, packages));
+ } catch (RuntimeException e) {
+ // Just in case we get NumberFormatException, or various
+ // impossible out of bounds errors happen.
+ throw new IOException("Unable to parse line: " + line, e);
+ }
+ }
+
+ /**
+ * Escape any newline and backslash characters in path. A newline in a path is legal if unusual,
+ * and it would break our line-based file parsing.
+ */
+ @VisibleForTesting
+ static String escape(String path) {
+ if (path.indexOf('\\') == -1 && path.indexOf('\n') == -1 && path.indexOf('\r') == -1) {
+ return path;
+ }
+
+ StringBuilder result = new StringBuilder(path.length() + 10);
+ for (int i = 0; i < path.length(); i++) {
+ // Surrogates will never match the characters we care about, so it's ok to use chars
+ // not code points here.
+ char c = path.charAt(i);
+ switch (c) {
+ case '\\':
+ result.append("\\\\");
+ break;
+ case '\n':
+ result.append("\\n");
+ break;
+ case '\r':
+ result.append("\\r");
+ break;
+ default:
+ result.append(c);
+ break;
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Reverse the effect of {@link #escape}.
+ * @throws IOException if the input string is malformed
+ */
+ @VisibleForTesting
+ static String unescape(String escaped) throws IOException {
+ // As we move through the input string, start is the position of the first character
+ // after the previous escape sequence and finish is the position of the following backslash.
+ int start = 0;
+ int finish = escaped.indexOf('\\');
+ if (finish == -1) {
+ return escaped;
+ }
+
+ StringBuilder result = new StringBuilder(escaped.length());
+ while (true) {
+ if (finish >= escaped.length() - 1) {
+ // Backslash mustn't be the last character
+ throw new IOException("Unexpected \\ in: " + escaped);
+ }
+ result.append(escaped, start, finish);
+ switch (escaped.charAt(finish + 1)) {
+ case '\\':
+ result.append('\\');
+ break;
+ case 'r':
+ result.append('\r');
+ break;
+ case 'n':
+ result.append('\n');
+ break;
+ default:
+ throw new IOException("Bad escape in: " + escaped);
+ }
+
+ start = finish + 2;
+ finish = escaped.indexOf('\\', start);
+ if (finish == -1) {
+ result.append(escaped, start, escaped.length());
+ break;
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Represents the dynamic code usage of a single package.
+ */
+ static class PackageDynamicCode {
+ /**
+ * Map from secondary code file path to information about which packages dynamically load
+ * that file.
+ */
+ final Map<String, DynamicCodeFile> mFileUsageMap;
+
+ private PackageDynamicCode() {
+ mFileUsageMap = new HashMap<>();
+ }
+
+ private PackageDynamicCode(PackageDynamicCode original) {
+ mFileUsageMap = new HashMap<>(original.mFileUsageMap.size());
+ for (Entry<String, DynamicCodeFile> entry : original.mFileUsageMap.entrySet()) {
+ DynamicCodeFile newValue = new DynamicCodeFile(entry.getValue());
+ mFileUsageMap.put(entry.getKey(), newValue);
+ }
+ }
+
+ private boolean add(String path, char fileType, int userId, String loadingPackage) {
+ DynamicCodeFile fileInfo = mFileUsageMap.get(path);
+ if (fileInfo == null) {
+ fileInfo = new DynamicCodeFile(fileType, userId, loadingPackage);
+ mFileUsageMap.put(path, fileInfo);
+ return true;
+ } else {
+ if (fileInfo.mUserId != userId) {
+ // This should be impossible: private app files are always user-specific and
+ // can't be accessed from different users.
+ throw new IllegalArgumentException("Cannot change userId for '" + path
+ + "' from " + fileInfo.mUserId + " to " + userId);
+ }
+ // Changing file type (i.e. loading the same file in different ways is possible if
+ // unlikely. We allow it but ignore it.
+ return fileInfo.mLoadingPackages.add(loadingPackage);
+ }
+ }
+
+ private boolean removeUser(int userId) {
+ boolean updated = false;
+ Iterator<DynamicCodeFile> it = mFileUsageMap.values().iterator();
+ while (it.hasNext()) {
+ DynamicCodeFile fileInfo = it.next();
+ if (fileInfo.mUserId == userId) {
+ it.remove();
+ updated = true;
+ }
+ }
+ return updated;
+ }
+
+ private boolean removeFile(String filePath, int userId) {
+ DynamicCodeFile fileInfo = mFileUsageMap.get(filePath);
+ if (fileInfo == null || fileInfo.mUserId != userId) {
+ return false;
+ } else {
+ mFileUsageMap.remove(filePath);
+ return true;
+ }
+ }
+
+ private void syncData(Map<String, Set<Integer>> packageToUsersMap,
+ Set<Integer> owningPackageUsers) {
+ Iterator<DynamicCodeFile> fileIt = mFileUsageMap.values().iterator();
+ while (fileIt.hasNext()) {
+ DynamicCodeFile fileInfo = fileIt.next();
+ int fileUserId = fileInfo.mUserId;
+ if (!owningPackageUsers.contains(fileUserId)) {
+ fileIt.remove();
+ } else {
+ // Also remove information about any loading packages that are no longer
+ // installed for this user.
+ Iterator<String> loaderIt = fileInfo.mLoadingPackages.iterator();
+ while (loaderIt.hasNext()) {
+ String loader = loaderIt.next();
+ Set<Integer> loadingPackageUsers = packageToUsersMap.get(loader);
+ if (loadingPackageUsers == null
+ || !loadingPackageUsers.contains(fileUserId)) {
+ loaderIt.remove();
+ }
+ }
+ if (fileInfo.mLoadingPackages.isEmpty()) {
+ fileIt.remove();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Represents a single dynamic code file loaded by one or more packages. Note that it is
+ * possible for one app to dynamically load code from a different app's home dir, if the
+ * owning app:
+ * <ul>
+ * <li>Targets API 27 or lower and has shared its home dir.
+ * <li>Is a system app.
+ * <li>Has a shared UID with the loading app.
+ * </ul>
+ */
+ static class DynamicCodeFile {
+ final char mFileType;
+ final int mUserId;
+ final Set<String> mLoadingPackages;
+
+ private DynamicCodeFile(char type, int user, String... packages) {
+ mFileType = type;
+ mUserId = user;
+ mLoadingPackages = new HashSet<>(Arrays.asList(packages));
+ }
+
+ private DynamicCodeFile(DynamicCodeFile original) {
+ mFileType = original.mFileType;
+ mUserId = original.mUserId;
+ mLoadingPackages = new HashSet<>(original.mLoadingPackages);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/pm/permission/BasePermission.java b/services/core/java/com/android/server/pm/permission/BasePermission.java
index 996f42b..3a49412 100644
--- a/services/core/java/com/android/server/pm/permission/BasePermission.java
+++ b/services/core/java/com/android/server/pm/permission/BasePermission.java
@@ -191,7 +191,8 @@
}
public boolean isRemoved() {
- return perm.info != null && (perm.info.flags & PermissionInfo.FLAG_REMOVED) != 0;
+ return perm != null && perm.info != null
+ && (perm.info.flags & PermissionInfo.FLAG_REMOVED) != 0;
}
public boolean isSignature() {
@@ -243,6 +244,9 @@
public boolean isWellbeing() {
return (protectionLevel & PermissionInfo.PROTECTION_FLAG_WELLBEING) != 0;
}
+ public boolean isDocumenter() {
+ return (protectionLevel & PermissionInfo.PROTECTION_FLAG_DOCUMENTER) != 0;
+ }
public void transfer(@NonNull String origPackageName, @NonNull String newPackageName) {
if (!origPackageName.equals(sourcePackageName)) {
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index bc3c18d..31f5ce4 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -1647,6 +1647,13 @@
// Special permission granted only to the OEM specified wellbeing app
allowed = true;
}
+ if (!allowed && bp.isDocumenter()
+ && pkg.packageName.equals(mPackageManagerInt.getKnownPackageName(
+ PackageManagerInternal.PACKAGE_DOCUMENTER, UserHandle.USER_SYSTEM))) {
+ // If this permission is to be granted to the documenter and
+ // this app is the documenter, then it gets the permission.
+ allowed = true;
+ }
}
return allowed;
}
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 29d6237..565bb706 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -3221,6 +3221,20 @@
mNativeWrapper.nativeSendPowerHint(hintId, data);
}
+ @VisibleForTesting
+ boolean wasDeviceIdleForInternal(long ms) {
+ synchronized (mLock) {
+ return mLastUserActivityTime + ms < SystemClock.uptimeMillis();
+ }
+ }
+
+ @VisibleForTesting
+ void onUserActivity() {
+ synchronized (mLock) {
+ mLastUserActivityTime = SystemClock.uptimeMillis();
+ }
+ }
+
/**
* Low-level function turn the device off immediately, without trying
* to be clean. Most people should use {@link ShutdownThread} for a clean shutdown.
@@ -4874,5 +4888,10 @@
public void powerHint(int hintId, int data) {
powerHintInternal(hintId, data);
}
+
+ @Override
+ public boolean wasDeviceIdleFor(long ms) {
+ return wasDeviceIdleForInternal(ms);
+ }
}
}
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 07bebad..02689a9 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -250,14 +250,29 @@
}
}
+ private void shutdownIfNeededLocked(Temperature temperature) {
+ if (temperature.getStatus() != Temperature.THROTTLING_SHUTDOWN) {
+ return;
+ }
+ switch (temperature.getType()) {
+ case Temperature.TYPE_CPU:
+ // Fall through
+ case Temperature.TYPE_GPU:
+ // Fall through
+ case Temperature.TYPE_NPU:
+ // Fall through
+ case Temperature.TYPE_SKIN:
+ mPowerManager.shutdown(false, PowerManager.SHUTDOWN_THERMAL_STATE, false);
+ break;
+ case Temperature.TYPE_BATTERY:
+ mPowerManager.shutdown(false, PowerManager.SHUTDOWN_BATTERY_THERMAL_STATE, false);
+ break;
+ }
+ }
+
private void onTemperatureChanged(Temperature temperature, boolean sendStatus) {
synchronized (mLock) {
- // Thermal Shutdown for Skin temperature
- if (temperature.getStatus() == Temperature.THROTTLING_SHUTDOWN
- && temperature.getType() == Temperature.TYPE_SKIN) {
- mPowerManager.shutdown(false, PowerManager.SHUTDOWN_THERMAL_STATE, false);
- }
-
+ shutdownIfNeededLocked(temperature);
Temperature old = mTemperatureMap.put(temperature.getName(), temperature);
if (old != null) {
if (old.getStatus() != temperature.getStatus()) {
@@ -300,6 +315,8 @@
final IThermalService.Stub mService = new IThermalService.Stub() {
@Override
public boolean registerThermalEventListener(IThermalEventListener listener) {
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.DEVICE_POWER, null);
synchronized (mLock) {
final long token = Binder.clearCallingIdentity();
try {
@@ -320,6 +337,8 @@
@Override
public boolean registerThermalEventListenerWithType(IThermalEventListener listener,
int type) {
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.DEVICE_POWER, null);
synchronized (mLock) {
final long token = Binder.clearCallingIdentity();
try {
@@ -339,6 +358,8 @@
@Override
public boolean unregisterThermalEventListener(IThermalEventListener listener) {
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.DEVICE_POWER, null);
synchronized (mLock) {
final long token = Binder.clearCallingIdentity();
try {
@@ -351,6 +372,8 @@
@Override
public List<Temperature> getCurrentTemperatures() {
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.DEVICE_POWER, null);
final long token = Binder.clearCallingIdentity();
try {
if (!mHalReady) {
@@ -364,6 +387,8 @@
@Override
public List<Temperature> getCurrentTemperaturesWithType(int type) {
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.DEVICE_POWER, null);
final long token = Binder.clearCallingIdentity();
try {
if (!mHalReady) {
@@ -428,14 +453,15 @@
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
+ pw.println("IsStatusOverride: " + mIsStatusOverride);
pw.println("ThermalEventListeners:");
mThermalEventListeners.dump(pw, "\t");
pw.println("ThermalStatusListeners:");
mThermalStatusListeners.dump(pw, "\t");
- pw.println("Thermal Status: " + Integer.toString(mStatus));
+ pw.println("Thermal Status: " + mStatus);
pw.println("Cached temperatures:");
dumpTemperaturesLocked(pw, "\t", mTemperatureMap.values());
- pw.println("HAL Ready: " + Boolean.toString(mHalReady));
+ pw.println("HAL Ready: " + mHalReady);
if (mHalReady) {
pw.println("HAL connection:");
mHalWrapper.dump(pw, "\t");
@@ -507,7 +533,7 @@
return -1;
}
if (!Temperature.isValidStatus(status)) {
- pw.println("Invalid status: " + Integer.toString(status));
+ pw.println("Invalid status: " + status);
return -1;
}
synchronized (mLock) {
diff --git a/services/core/java/com/android/server/role/RoleManagerService.java b/services/core/java/com/android/server/role/RoleManagerService.java
index 8711ddf..35013de 100644
--- a/services/core/java/com/android/server/role/RoleManagerService.java
+++ b/services/core/java/com/android/server/role/RoleManagerService.java
@@ -22,8 +22,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.annotation.WorkerThread;
import android.app.ActivityManager;
import android.app.AppOpsManager;
+import android.app.role.IOnRoleHoldersChangedListener;
import android.app.role.IRoleManager;
import android.app.role.IRoleManagerCallback;
import android.app.role.RoleManager;
@@ -33,6 +35,9 @@
import android.content.IntentFilter;
import android.content.pm.PackageManagerInternal;
import android.content.pm.Signature;
+import android.os.Handler;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.UserHandle;
@@ -53,6 +58,8 @@
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import com.android.internal.util.dump.DualDumpOutputStream;
+import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
@@ -73,7 +80,7 @@
*
* @see RoleManager
*/
-public class RoleManagerService extends SystemService {
+public class RoleManagerService extends SystemService implements RoleUserState.Callback {
private static final String LOG_TAG = RoleManagerService.class.getSimpleName();
@@ -100,6 +107,17 @@
private final SparseArray<RemoteRoleControllerService> mControllerServices =
new SparseArray<>();
+ /**
+ * Maps user id to its list of listeners.
+ */
+ @GuardedBy("mLock")
+ @NonNull
+ private final SparseArray<RemoteCallbackList<IOnRoleHoldersChangedListener>> mListeners =
+ new SparseArray<>();
+
+ @NonNull
+ private final Handler mListenerHandler = FgThread.getHandler();
+
public RoleManagerService(@NonNull Context context) {
super(context);
@@ -188,7 +206,7 @@
}
@Nullable
- private String computeComponentStateHash(@UserIdInt int userId) {
+ private static String computeComponentStateHash(@UserIdInt int userId) {
PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class);
ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -223,7 +241,7 @@
synchronized (mLock) {
RoleUserState userState = mUserStates.get(userId);
if (userState == null) {
- userState = new RoleUserState(userId);
+ userState = new RoleUserState(userId, this);
mUserStates.put(userId, userState);
}
return userState;
@@ -242,17 +260,70 @@
}
}
+ @Nullable
+ private RemoteCallbackList<IOnRoleHoldersChangedListener> getListeners(@UserIdInt int userId) {
+ synchronized (mLock) {
+ return mListeners.get(userId);
+ }
+ }
+
+ @NonNull
+ private RemoteCallbackList<IOnRoleHoldersChangedListener> getOrCreateListeners(
+ @UserIdInt int userId) {
+ synchronized (mLock) {
+ RemoteCallbackList<IOnRoleHoldersChangedListener> listeners = mListeners.get(userId);
+ if (listeners == null) {
+ listeners = new RemoteCallbackList<>();
+ mListeners.put(userId, listeners);
+ }
+ return listeners;
+ }
+ }
+
private void onRemoveUser(@UserIdInt int userId) {
+ RemoteCallbackList<IOnRoleHoldersChangedListener> listeners;
RoleUserState userState;
synchronized (mLock) {
+ listeners = mListeners.removeReturnOld(userId);
mControllerServices.remove(userId);
userState = mUserStates.removeReturnOld(userId);
}
+ if (listeners != null) {
+ listeners.kill();
+ }
if (userState != null) {
userState.destroy();
}
}
+ @Override
+ public void onRoleHoldersChanged(@NonNull String roleName, @UserIdInt int userId) {
+ mListenerHandler.sendMessage(PooledLambda.obtainMessage(
+ RoleManagerService::notifyRoleHoldersChanged, this, roleName, userId));
+ }
+
+ @WorkerThread
+ private void notifyRoleHoldersChanged(@NonNull String roleName, @UserIdInt int userId) {
+ RemoteCallbackList<IOnRoleHoldersChangedListener> listeners = getListeners(userId);
+ if (listeners == null) {
+ return;
+ }
+
+ int broadcastCount = listeners.beginBroadcast();
+ try {
+ for (int i = 0; i < broadcastCount; i++) {
+ IOnRoleHoldersChangedListener listener = listeners.getBroadcastItem(i);
+ try {
+ listener.onRoleHoldersChanged(roleName, userId);
+ } catch (RemoteException e) {
+ Slog.e(LOG_TAG, "Error calling OnRoleHoldersChangedListener", e);
+ }
+ }
+ } finally {
+ listeners.finishBroadcast();
+ }
+ }
+
private class Stub extends IRoleManager.Stub {
@Override
@@ -357,6 +428,42 @@
}
@Override
+ public void addOnRoleHoldersChangedListenerAsUser(
+ @NonNull IOnRoleHoldersChangedListener listener, @UserIdInt int userId) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ if (!mUserManagerInternal.exists(userId)) {
+ Slog.e(LOG_TAG, "user " + userId + " does not exist");
+ return;
+ }
+ userId = handleIncomingUser(userId, "addOnRoleHoldersChangedListenerAsUser");
+ getContext().enforceCallingOrSelfPermission(Manifest.permission.OBSERVE_ROLE_HOLDERS,
+ "addOnRoleHoldersChangedListenerAsUser");
+
+ RemoteCallbackList<IOnRoleHoldersChangedListener> listeners = getOrCreateListeners(
+ userId);
+ listeners.register(listener);
+ }
+
+ @Override
+ public void removeOnRoleHoldersChangedListenerAsUser(
+ @NonNull IOnRoleHoldersChangedListener listener, @UserIdInt int userId) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ if (!mUserManagerInternal.exists(userId)) {
+ Slog.e(LOG_TAG, "user " + userId + " does not exist");
+ return;
+ }
+ userId = handleIncomingUser(userId, "removeOnRoleHoldersChangedListenerAsUser");
+ getContext().enforceCallingOrSelfPermission(Manifest.permission.OBSERVE_ROLE_HOLDERS,
+ "removeOnRoleHoldersChangedListenerAsUser");
+
+ RemoteCallbackList<IOnRoleHoldersChangedListener> listeners = getListeners(userId);
+ if (listener == null) {
+ return;
+ }
+ listeners.unregister(listener);
+ }
+
+ @Override
public void setRoleNamesFromController(@NonNull List<String> roleNames) {
Preconditions.checkNotNull(roleNames, "roleNames cannot be null");
getContext().enforceCallingOrSelfPermission(
diff --git a/services/core/java/com/android/server/role/RoleUserState.java b/services/core/java/com/android/server/role/RoleUserState.java
index ec614a4..d55e261 100644
--- a/services/core/java/com/android/server/role/RoleUserState.java
+++ b/services/core/java/com/android/server/role/RoleUserState.java
@@ -74,6 +74,9 @@
private final int mUserId;
@NonNull
+ private final Callback mCallback;
+
+ @NonNull
private final Object mLock = new Object();
@GuardedBy("mLock")
@@ -100,12 +103,14 @@
private final Handler mWriteHandler = new Handler(BackgroundThread.getHandler().getLooper());
/**
- * Create a new instance of user state, and read its state from disk if previously persisted.
+ * Create a new user state, and read its state from disk if previously persisted.
*
- * @param userId the user id for the new user state
+ * @param userId the user id for this user state
+ * @param callback the callback for this user state
*/
- public RoleUserState(@UserIdInt int userId) {
+ public RoleUserState(@UserIdInt int userId, @NonNull Callback callback) {
mUserId = userId;
+ mCallback = callback;
readFile();
}
@@ -116,6 +121,7 @@
public int getVersion() {
synchronized (mLock) {
throwIfDestroyedLocked();
+
return mVersion;
}
}
@@ -128,6 +134,7 @@
public void setVersion(int version) {
synchronized (mLock) {
throwIfDestroyedLocked();
+
if (mVersion == version) {
return;
}
@@ -156,6 +163,7 @@
public void setPackagesHash(@Nullable String packagesHash) {
synchronized (mLock) {
throwIfDestroyedLocked();
+
if (Objects.equals(mPackagesHash, packagesHash)) {
return;
}
@@ -174,6 +182,7 @@
public boolean isRoleAvailable(@NonNull String roleName) {
synchronized (mLock) {
throwIfDestroyedLocked();
+
return mRoles.containsKey(roleName);
}
}
@@ -189,6 +198,7 @@
public ArraySet<String> getRoleHolders(@NonNull String roleName) {
synchronized (mLock) {
throwIfDestroyedLocked();
+
return new ArraySet<>(mRoles.get(roleName));
}
}
@@ -201,29 +211,34 @@
public void setRoleNames(@NonNull List<String> roleNames) {
synchronized (mLock) {
throwIfDestroyedLocked();
+
boolean changed = false;
+
for (int i = mRoles.size() - 1; i >= 0; i--) {
String roleName = mRoles.keyAt(i);
+
if (!roleNames.contains(roleName)) {
ArraySet<String> packageNames = mRoles.valueAt(i);
if (!packageNames.isEmpty()) {
- Slog.e(LOG_TAG,
- "Holders of a removed role should have been cleaned up, role: "
- + roleName + ", holders: " + packageNames);
+ Slog.e(LOG_TAG, "Holders of a removed role should have been cleaned up,"
+ + " role: " + roleName + ", holders: " + packageNames);
}
mRoles.removeAt(i);
changed = true;
}
}
+
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;
}
}
+
if (changed) {
scheduleWriteFileLocked();
}
@@ -241,20 +256,27 @@
*/
@CheckResult
public boolean addRoleHolder(@NonNull String roleName, @NonNull String packageName) {
+ boolean changed;
+
synchronized (mLock) {
throwIfDestroyedLocked();
+
ArraySet<String> roleHolders = mRoles.get(roleName);
if (roleHolders == null) {
Slog.e(LOG_TAG, "Cannot add role holder for unknown role, role: " + roleName
+ ", package: " + packageName);
return false;
}
- boolean changed = roleHolders.add(packageName);
+ changed = roleHolders.add(packageName);
if (changed) {
scheduleWriteFileLocked();
}
- return true;
}
+
+ if (changed) {
+ mCallback.onRoleHoldersChanged(roleName, mUserId);
+ }
+ return true;
}
/**
@@ -268,20 +290,28 @@
*/
@CheckResult
public boolean removeRoleHolder(@NonNull String roleName, @NonNull String packageName) {
+ boolean changed;
+
synchronized (mLock) {
throwIfDestroyedLocked();
+
ArraySet<String> roleHolders = mRoles.get(roleName);
if (roleHolders == null) {
Slog.e(LOG_TAG, "Cannot remove role holder for unknown role, role: " + roleName
+ ", package: " + packageName);
return false;
}
- boolean changed = roleHolders.remove(packageName);
+
+ changed = roleHolders.remove(packageName);
if (changed) {
scheduleWriteFileLocked();
}
- return true;
}
+
+ if (changed) {
+ mCallback.onRoleHoldersChanged(roleName, mUserId);
+ }
+ return true;
}
/**
@@ -520,8 +550,8 @@
}
/**
- * Destroy this state and delete the corresponding file. Any pending writes to the file will be
- * cancelled and any future interaction with this state will throw an exception.
+ * Destroy this user state and delete the corresponding file. Any pending writes to the file
+ * will be cancelled, and any future interaction with this state will throw an exception.
*/
public void destroy() {
synchronized (mLock) {
@@ -542,4 +572,18 @@
private static @NonNull File getFile(@UserIdInt int userId) {
return new File(Environment.getUserSystemDirectory(userId), ROLES_FILE_NAME);
}
+
+ /**
+ * Callback for a user state.
+ */
+ public interface Callback {
+
+ /**
+ * Called when the holders of roles are changed.
+ *
+ * @param roleName the name of the role whose holders are changed
+ * @param userId the user id for this role holder change
+ */
+ void onRoleHoldersChanged(@NonNull String roleName, @UserIdInt int userId);
+ }
}
diff --git a/services/core/java/com/android/server/signedconfig/SignedConfig.java b/services/core/java/com/android/server/signedconfig/SignedConfig.java
index a3f452c..e6bb800 100644
--- a/services/core/java/com/android/server/signedconfig/SignedConfig.java
+++ b/services/core/java/com/android/server/signedconfig/SignedConfig.java
@@ -48,7 +48,7 @@
private static final String CONFIG_KEY_VALUE = "value";
/**
- * Represents config values targetting to an SDK range.
+ * Represents config values targeting an SDK range.
*/
public static class PerSdkConfig {
public final int minSdk;
@@ -90,11 +90,24 @@
/**
* Parse configuration from an APK.
*
- * @param config config as read from the APK metadata.
+ * @param config Config string as read from the APK metadata.
+ * @param allowedKeys Set of allowed keys in the config. Any key/value mapping for a key not in
+ * this set will result in an {@link InvalidConfigException} being thrown.
+ * @param keyValueMappers Mappings for values per key. The keys in the top level map should be
+ * a subset of {@code allowedKeys}. The keys in the inner map indicate
+ * the set of allowed values for that keys value. This map will be
+ * applied to the value in the configuration. This is intended to allow
+ * enum-like values to be encoded as strings in the configuration, and
+ * mapped back to integers when the configuration is parsed.
+ *
+ * <p>Any config key with a value that does not appear in the
+ * corresponding map will result in an {@link InvalidConfigException}
+ * being thrown.
* @return Parsed configuration.
* @throws InvalidConfigException If there's a problem parsing the config.
*/
- public static SignedConfig parse(String config, Set<String> allowedKeys)
+ public static SignedConfig parse(String config, Set<String> allowedKeys,
+ Map<String, Map<String, String>> keyValueMappers)
throws InvalidConfigException {
try {
JSONObject json = new JSONObject(config);
@@ -103,7 +116,8 @@
JSONArray perSdkConfig = json.getJSONArray(KEY_CONFIG);
List<PerSdkConfig> parsedConfigs = new ArrayList<>();
for (int i = 0; i < perSdkConfig.length(); ++i) {
- parsedConfigs.add(parsePerSdkConfig(perSdkConfig.getJSONObject(i), allowedKeys));
+ parsedConfigs.add(parsePerSdkConfig(perSdkConfig.getJSONObject(i), allowedKeys,
+ keyValueMappers));
}
return new SignedConfig(version, parsedConfigs);
@@ -113,8 +127,17 @@
}
+ private static CharSequence quoted(Object s) {
+ if (s == null) {
+ return "null";
+ } else {
+ return "\"" + s + "\"";
+ }
+ }
+
@VisibleForTesting
- static PerSdkConfig parsePerSdkConfig(JSONObject json, Set<String> allowedKeys)
+ static PerSdkConfig parsePerSdkConfig(JSONObject json, Set<String> allowedKeys,
+ Map<String, Map<String, String>> keyValueMappers)
throws JSONException, InvalidConfigException {
int minSdk = json.getInt(CONFIG_KEY_MIN_SDK);
int maxSdk = json.getInt(CONFIG_KEY_MAX_SDK);
@@ -123,12 +146,23 @@
for (int i = 0; i < valueArray.length(); ++i) {
JSONObject keyValuePair = valueArray.getJSONObject(i);
String key = keyValuePair.getString(CONFIG_KEY_KEY);
- String value = keyValuePair.has(CONFIG_KEY_VALUE)
- ? keyValuePair.getString(CONFIG_KEY_VALUE)
+ Object valueObject = keyValuePair.has(CONFIG_KEY_VALUE)
+ ? keyValuePair.get(CONFIG_KEY_VALUE)
: null;
+ String value = valueObject == JSONObject.NULL || valueObject == null
+ ? null
+ : valueObject.toString();
if (!allowedKeys.contains(key)) {
throw new InvalidConfigException("Config key " + key + " is not allowed");
}
+ if (keyValueMappers.containsKey(key)) {
+ Map<String, String> mapper = keyValueMappers.get(key);
+ if (!mapper.containsKey(value)) {
+ throw new InvalidConfigException(
+ "Config key " + key + " contains unsupported value " + quoted(value));
+ }
+ value = mapper.get(value);
+ }
values.put(key, value);
}
return new PerSdkConfig(minSdk, maxSdk, values);
diff --git a/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java b/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
index 7ce071f..e4d799a 100644
--- a/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
+++ b/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
@@ -17,12 +17,112 @@
package com.android.server.signedconfig;
import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.os.Build;
+import android.provider.Settings;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
class SignedConfigApplicator {
- static void applyConfig(Context context, String config, String signature) {
- //TODO verify signature
- //TODO parse & apply config
+ private static final String TAG = "SignedConfig";
+
+ private static final Set<String> ALLOWED_KEYS = Collections.unmodifiableSet(new ArraySet<>(
+ Arrays.asList(
+ Settings.Global.HIDDEN_API_POLICY,
+ Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS
+ )));
+
+ private static final Map<String, String> HIDDEN_API_POLICY_KEY_MAP = makeMap(
+ "DEFAULT", String.valueOf(ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT),
+ "DISABLED", String.valueOf(ApplicationInfo.HIDDEN_API_ENFORCEMENT_DISABLED),
+ "JUST_WARN", String.valueOf(ApplicationInfo.HIDDEN_API_ENFORCEMENT_JUST_WARN),
+ "ENABLED", String.valueOf(ApplicationInfo.HIDDEN_API_ENFORCEMENT_ENABLED)
+ );
+
+ private static final Map<String, Map<String, String>> KEY_VALUE_MAPPERS = makeMap(
+ Settings.Global.HIDDEN_API_POLICY, HIDDEN_API_POLICY_KEY_MAP
+ );
+
+ private static <K, V> Map<K, V> makeMap(Object... keyValuePairs) {
+ if (keyValuePairs.length % 2 != 0) {
+ throw new IllegalArgumentException();
+ }
+ final int len = keyValuePairs.length / 2;
+ ArrayMap<K, V> m = new ArrayMap<>(len);
+ for (int i = 0; i < len; ++i) {
+ m.put((K) keyValuePairs[i * 2], (V) keyValuePairs[(i * 2) + 1]);
+ }
+ return Collections.unmodifiableMap(m);
+
}
+ private final Context mContext;
+ private final String mSourcePackage;
+
+ SignedConfigApplicator(Context context, String sourcePackage) {
+ mContext = context;
+ mSourcePackage = sourcePackage;
+ }
+
+ private boolean checkSignature(String data, String signature) {
+ Slog.w(TAG, "SIGNATURE CHECK NOT IMPLEMENTED YET!");
+ return false;
+ }
+
+ private int getCurrentConfigVersion() {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.SIGNED_CONFIG_VERSION, 0);
+ }
+
+ private void updateCurrentConfig(int version, Map<String, String> values) {
+ for (Map.Entry<String, String> e: values.entrySet()) {
+ Settings.Global.putString(
+ mContext.getContentResolver(),
+ e.getKey(),
+ e.getValue());
+ }
+ Settings.Global.putInt(
+ mContext.getContentResolver(), Settings.Global.SIGNED_CONFIG_VERSION, version);
+ }
+
+
+ void applyConfig(String configStr, String signature) {
+ if (!checkSignature(configStr, signature)) {
+ Slog.e(TAG, "Signature check on signed configuration in package " + mSourcePackage
+ + " failed; ignoring");
+ return;
+ }
+ SignedConfig config;
+ try {
+ config = SignedConfig.parse(configStr, ALLOWED_KEYS, KEY_VALUE_MAPPERS);
+ } catch (InvalidConfigException e) {
+ Slog.e(TAG, "Failed to parse config from package " + mSourcePackage, e);
+ return;
+ }
+ int currentVersion = getCurrentConfigVersion();
+ if (currentVersion >= config.version) {
+ Slog.i(TAG, "Config from package " + mSourcePackage + " is older than existing: "
+ + config.version + "<=" + currentVersion);
+ return;
+ }
+ // We have new config!
+ Slog.i(TAG, "Got new signed config from package " + mSourcePackage + ": version "
+ + config.version + " replacing existing version " + currentVersion);
+ SignedConfig.PerSdkConfig matchedConfig =
+ config.getMatchingConfig(Build.VERSION.SDK_INT);
+ if (matchedConfig == null) {
+ Slog.i(TAG, "Config is not applicable to current SDK version; ignoring");
+ return;
+ }
+
+ Slog.i(TAG, "Updating signed config to version " + config.version);
+ updateCurrentConfig(config.version, matchedConfig.values);
+ }
}
diff --git a/services/core/java/com/android/server/signedconfig/SignedConfigService.java b/services/core/java/com/android/server/signedconfig/SignedConfigService.java
index 1485686..be1d41d 100644
--- a/services/core/java/com/android/server/signedconfig/SignedConfigService.java
+++ b/services/core/java/com/android/server/signedconfig/SignedConfigService.java
@@ -85,7 +85,8 @@
Slog.d(TAG, "Got signed config: " + config);
Slog.d(TAG, "Got config signature: " + signature);
}
- SignedConfigApplicator.applyConfig(mContext, config, signature);
+ new SignedConfigApplicator(mContext, packageName).applyConfig(
+ config, signature);
} else {
if (DBG) Slog.d(TAG, "Package has no config/signature.");
}
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index 7683172..aca9702 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -1797,7 +1797,7 @@
// focus). Also if there is an active pinned stack - we always want to notify it about
// task stack changes, because its positioning may depend on it.
if (mStackSupervisor.mAppVisibilitiesChangedSinceLastPause
- || getDisplay().hasPinnedStack()) {
+ || (getDisplay() != null && getDisplay().hasPinnedStack())) {
mService.getTaskChangeNotificationController().notifyTaskStackChanged();
mStackSupervisor.mAppVisibilitiesChangedSinceLastPause = false;
}
diff --git a/services/core/java/com/android/server/wm/BarController.java b/services/core/java/com/android/server/wm/BarController.java
index a335fa2..5b20af3 100644
--- a/services/core/java/com/android/server/wm/BarController.java
+++ b/services/core/java/com/android/server/wm/BarController.java
@@ -219,7 +219,7 @@
}
private boolean updateStateLw(final int state) {
- if (state != mState) {
+ if (mWin != null && state != mState) {
mState = state;
if (DEBUG) Slog.d(mTag, "mState: " + StatusBarManager.windowStateToString(state));
mHandler.post(new Runnable() {
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 639ed02..f9c9d33 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -1,5 +1,6 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
@@ -9,7 +10,6 @@
import android.os.Debug;
import android.os.IBinder;
import android.util.Slog;
-import android.view.InputApplicationHandle;
import android.view.KeyEvent;
import android.view.WindowManager;
@@ -204,6 +204,37 @@
+ WindowManagerService.TYPE_LAYER_OFFSET;
}
+ /** Callback to get pointer display id. */
+ @Override
+ public int getPointerDisplayId() {
+ synchronized (mService.mGlobalLock) {
+ // If desktop mode is not enabled, show on the default display.
+ if (!mService.mForceDesktopModeOnExternalDisplays) {
+ return DEFAULT_DISPLAY;
+ }
+
+ // Look for the topmost freeform display.
+ int firstExternalDisplayId = DEFAULT_DISPLAY;
+ for (int i = mService.mRoot.mChildren.size() - 1; i >= 0; --i) {
+ final DisplayContent displayContent = mService.mRoot.mChildren.get(i);
+ // Heuristic solution here. Currently when "Freeform windows" developer option is
+ // enabled we automatically put secondary displays in freeform mode and emulating
+ // "desktop mode". It also makes sense to show the pointer on the same display.
+ if (displayContent.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+ return displayContent.getDisplayId();
+ }
+
+ if (firstExternalDisplayId == DEFAULT_DISPLAY
+ && displayContent.getDisplayId() != DEFAULT_DISPLAY) {
+ firstExternalDisplayId = displayContent.getDisplayId();
+ }
+ }
+
+ // Look for the topmost non-default display
+ return firstExternalDisplayId;
+ }
+ }
+
/** Waits until the built-in input devices have been configured. */
public boolean waitForInputDevicesReady(long timeoutMillis) {
synchronized (mInputDevicesReadyMonitor) {
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 43d2dcf..bf83ca9 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -107,6 +107,7 @@
jmethodID getLongPressTimeout;
jmethodID getPointerLayer;
jmethodID getPointerIcon;
+ jmethodID getPointerDisplayId;
jmethodID getKeyboardLayoutOverlay;
jmethodID getDeviceAlias;
jmethodID getTouchCalibrationForInputDevice;
@@ -174,15 +175,6 @@
loadSystemIconAsSpriteWithPointerIcon(env, contextObj, style, &pointerIcon, outSpriteIcon);
}
-static void updatePointerControllerFromViewport(
- sp<PointerController> controller, const DisplayViewport* const viewport) {
- if (controller != nullptr && viewport != nullptr) {
- const int32_t width = viewport->logicalRight - viewport->logicalLeft;
- const int32_t height = viewport->logicalBottom - viewport->logicalTop;
- controller->setDisplayViewport(width, height, viewport->orientation);
- }
-}
-
enum {
WM_ACTION_PASS_TO_USER = 1,
};
@@ -242,6 +234,7 @@
jfloatArray matrixArr);
virtual TouchAffineTransformation getTouchAffineTransformation(
const std::string& inputDeviceDescriptor, int32_t surfaceRotation);
+ virtual void updatePointerDisplay();
/* --- InputDispatcherPolicyInterface implementation --- */
@@ -314,10 +307,11 @@
std::atomic<bool> mInteractive;
- void updateInactivityTimeoutLocked(const sp<PointerController>& controller);
+ void updateInactivityTimeoutLocked();
void handleInterceptActions(jint wmActions, nsecs_t when, uint32_t& policyFlags);
void ensureSpriteControllerLocked();
-
+ const DisplayViewport* findDisplayViewportLocked(int32_t displayId);
+ int32_t getPointerDisplayId();
static bool checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName);
static inline JNIEnv* jniEnv() {
@@ -391,9 +385,10 @@
return false;
}
-static const DisplayViewport* findInternalViewport(const std::vector<DisplayViewport>& viewports) {
- for (const DisplayViewport& v : viewports) {
- if (v.type == ViewportType::VIEWPORT_INTERNAL) {
+const DisplayViewport* NativeInputManager::findDisplayViewportLocked(int32_t displayId)
+ REQUIRES(mLock) {
+ for (const DisplayViewport& v : mLocked.viewports) {
+ if (v.displayId == displayId) {
return &v;
}
}
@@ -420,20 +415,10 @@
}
}
- const DisplayViewport* newInternalViewport = findInternalViewport(viewports);
- {
+ { // acquire lock
AutoMutex _l(mLock);
- const DisplayViewport* oldInternalViewport = findInternalViewport(mLocked.viewports);
- // Internal viewport has changed if there wasn't one earlier, and there is one now, or,
- // if they are different.
- const bool internalViewportChanged = (newInternalViewport != nullptr) &&
- (oldInternalViewport == nullptr || (*oldInternalViewport != *newInternalViewport));
- if (internalViewportChanged) {
- sp<PointerController> controller = mLocked.pointerController.promote();
- updatePointerControllerFromViewport(controller, newInternalViewport);
- }
mLocked.viewports = viewports;
- }
+ } // release lock
mInputManager->getReader()->requestRefreshConfiguration(
InputReaderConfiguration::CHANGE_DISPLAY_INFO);
@@ -556,15 +541,43 @@
controller = new PointerController(this, mLooper, mLocked.spriteController);
mLocked.pointerController = controller;
-
- const DisplayViewport* internalViewport = findInternalViewport(mLocked.viewports);
- updatePointerControllerFromViewport(controller, internalViewport);
-
- updateInactivityTimeoutLocked(controller);
+ updateInactivityTimeoutLocked();
}
+
return controller;
}
+int32_t NativeInputManager::getPointerDisplayId() {
+ JNIEnv* env = jniEnv();
+ jint pointerDisplayId = env->CallIntMethod(mServiceObj,
+ gServiceClassInfo.getPointerDisplayId);
+ if (checkAndClearExceptionFromCallback(env, "getPointerDisplayId")) {
+ pointerDisplayId = ADISPLAY_ID_DEFAULT;
+ }
+
+ return pointerDisplayId;
+}
+
+void NativeInputManager::updatePointerDisplay() {
+ ATRACE_CALL();
+
+ jint pointerDisplayId = getPointerDisplayId();
+
+ AutoMutex _l(mLock);
+ sp<PointerController> controller = mLocked.pointerController.promote();
+ if (controller != nullptr) {
+ const DisplayViewport* viewport = findDisplayViewportLocked(pointerDisplayId);
+ if (viewport == nullptr) {
+ ALOGW("Can't find pointer display viewport, fallback to default display.");
+ viewport = findDisplayViewportLocked(ADISPLAY_ID_DEFAULT);
+ }
+
+ if (viewport != nullptr) {
+ controller->setDisplayViewport(*viewport);
+ }
+ }
+}
+
void NativeInputManager::ensureSpriteControllerLocked() REQUIRES(mLock) {
if (mLocked.spriteController == nullptr) {
JNIEnv* env = jniEnv();
@@ -821,16 +834,16 @@
if (mLocked.systemUiVisibility != visibility) {
mLocked.systemUiVisibility = visibility;
-
- sp<PointerController> controller = mLocked.pointerController.promote();
- if (controller != nullptr) {
- updateInactivityTimeoutLocked(controller);
- }
+ updateInactivityTimeoutLocked();
}
}
-void NativeInputManager::updateInactivityTimeoutLocked(const sp<PointerController>& controller)
- REQUIRES(mLock) {
+void NativeInputManager::updateInactivityTimeoutLocked() REQUIRES(mLock) {
+ sp<PointerController> controller = mLocked.pointerController.promote();
+ if (controller == nullptr) {
+ return;
+ }
+
bool lightsOut = mLocked.systemUiVisibility & ASYSTEM_UI_VISIBILITY_STATUS_BAR_HIDDEN;
controller->setInactivityTimeout(lightsOut
? PointerController::INACTIVITY_TIMEOUT_SHORT
@@ -1824,6 +1837,9 @@
GET_METHOD_ID(gServiceClassInfo.getPointerIcon, clazz,
"getPointerIcon", "()Landroid/view/PointerIcon;");
+ GET_METHOD_ID(gServiceClassInfo.getPointerDisplayId, clazz,
+ "getPointerDisplayId", "()I");
+
GET_METHOD_ID(gServiceClassInfo.getKeyboardLayoutOverlay, clazz,
"getKeyboardLayoutOverlay",
"(Landroid/hardware/input/InputDeviceIdentifier;)[Ljava/lang/String;");
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
index c44f306..240b820 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
@@ -126,4 +126,14 @@
public List<String> getCrossProfileCalendarPackagesForUser(int userHandle) {
return Collections.emptyList();
}
+
+ @Override
+ public boolean isManagedKiosk() {
+ return false;
+ }
+
+ @Override
+ public boolean isUnattendedManagedKiosk() {
+ return false;
+ }
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index bfbaac9..bc550dc 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -448,6 +448,12 @@
private static final long MINIMUM_STRONG_AUTH_TIMEOUT_MS = TimeUnit.HOURS.toMillis(1);
/**
+ * The amount of ms that a managed kiosk must go without user interaction to be considered
+ * unattended.
+ */
+ private static final int UNATTENDED_MANAGED_KIOSK_MS = 30000;
+
+ /**
* Strings logged with {@link
* com.android.internal.logging.nano.MetricsProto.MetricsEvent#PROVISIONING_ENTRY_POINT_ADB}.
*/
@@ -5140,7 +5146,7 @@
mInjector.binderRestoreCallingIdentity(ident);
}
- mInjector.getPowerManagerInternal().setMaximumScreenOffTimeoutFromDeviceAdmin(
+ getPowerManagerInternal().setMaximumScreenOffTimeoutFromDeviceAdmin(
UserHandle.USER_SYSTEM, timeMs);
}
@@ -5159,7 +5165,7 @@
}
policy.mLastMaximumTimeToLock = timeMs;
- mInjector.getPowerManagerInternal().setMaximumScreenOffTimeoutFromDeviceAdmin(
+ getPowerManagerInternal().setMaximumScreenOffTimeoutFromDeviceAdmin(
userId, policy.mLastMaximumTimeToLock);
}
@@ -13577,4 +13583,79 @@
}
return Collections.emptyList();
}
+
+ @Override
+ public boolean isManagedKiosk() {
+ if (!mHasFeature) {
+ return false;
+ }
+ enforceManageUsers();
+ long id = mInjector.binderClearCallingIdentity();
+ try {
+ return isManagedKioskInternal();
+ } catch (RemoteException e) {
+ throw new IllegalStateException(e);
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ }
+
+ @Override
+ public boolean isUnattendedManagedKiosk() {
+ if (!mHasFeature) {
+ return false;
+ }
+ enforceManageUsers();
+ long id = mInjector.binderClearCallingIdentity();
+ try {
+ return isManagedKioskInternal()
+ && getPowerManagerInternal().wasDeviceIdleFor(UNATTENDED_MANAGED_KIOSK_MS);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(e);
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ }
+
+ /**
+ * Returns whether the device is currently being used as a publicly-accessible dedicated device.
+ * Assumes that feature checks and permission checks have already been performed, and that the
+ * calling identity has been cleared.
+ */
+ private boolean isManagedKioskInternal() throws RemoteException {
+ return mOwners.hasDeviceOwner()
+ && mInjector.getIActivityManager().getLockTaskModeState()
+ == ActivityManager.LOCK_TASK_MODE_LOCKED
+ && !isLockTaskFeatureEnabled(DevicePolicyManager.LOCK_TASK_FEATURE_SYSTEM_INFO)
+ && !deviceHasKeyguard()
+ && !inEphemeralUserSession();
+ }
+
+ private boolean isLockTaskFeatureEnabled(int lockTaskFeature) throws RemoteException {
+ int lockTaskFeatures =
+ getUserData(mInjector.getIActivityManager().getCurrentUser().id).mLockTaskFeatures;
+ return (lockTaskFeatures & lockTaskFeature) == lockTaskFeature;
+ }
+
+ private boolean deviceHasKeyguard() {
+ for (UserInfo userInfo : mUserManager.getUsers()) {
+ if (mLockPatternUtils.isSecure(userInfo.id)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean inEphemeralUserSession() {
+ for (UserInfo userInfo : mUserManager.getUsers()) {
+ if (mInjector.getUserManager().isUserEphemeral(userInfo.id)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private PowerManagerInternal getPowerManagerInternal() {
+ return mInjector.getPowerManagerInternal();
+ }
}
diff --git a/services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java b/services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java
index b01adc9..96ef0ce 100644
--- a/services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java
+++ b/services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java
@@ -22,7 +22,10 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+import static org.testng.Assert.expectThrows;
+import android.annotation.UserIdInt;
import android.app.Application;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IBackupObserver;
@@ -32,11 +35,14 @@
import android.content.Intent;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import com.android.server.backup.testing.BackupManagerServiceTestUtils;
import com.android.server.backup.testing.TransportData;
+import com.android.server.testing.shadows.ShadowBinder;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -44,6 +50,8 @@
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowContextWrapper;
import java.io.File;
import java.io.FileDescriptor;
@@ -51,14 +59,19 @@
/** Tests for the user-aware backup/restore system service {@link BackupManagerService}. */
@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBinder.class})
@Presubmit
public class BackupManagerServiceTest {
private static final String TEST_PACKAGE = "package";
private static final String TEST_TRANSPORT = "transport";
+ private static final int NON_USER_SYSTEM = UserHandle.USER_SYSTEM + 1;
+
+ private ShadowContextWrapper mShadowContext;
@Mock private UserBackupManagerService mUserBackupManagerService;
private BackupManagerService mBackupManagerService;
private Context mContext;
+ @UserIdInt private int mUserId;
/** Initialize {@link BackupManagerService}. */
@Before
@@ -67,6 +80,8 @@
Application application = RuntimeEnvironment.application;
mContext = application;
+ mShadowContext = shadowOf(application);
+ mUserId = NON_USER_SYSTEM;
mBackupManagerService =
new BackupManagerService(
application,
@@ -76,6 +91,15 @@
}
/**
+ * Clean up and reset state that was created for testing {@link BackupManagerService}
+ * operations.
+ */
+ @After
+ public void tearDown() throws Exception {
+ ShadowBinder.reset();
+ }
+
+ /**
* Test verifying that {@link BackupManagerService#MORE_DEBUG} is set to {@code false}.
* This is specifically to prevent overloading the logs in production.
*/
@@ -274,11 +298,41 @@
// ---------------------------------------------
// Settings tests
// ---------------------------------------------
+ /**
+ * Test verifying that {@link BackupManagerService#setBackupEnabled(int, boolean)} throws a
+ * {@link SecurityException} if the caller does not have INTERACT_ACROSS_USERS_FULL permission.
+ */
+ @Test
+ public void setBackupEnabled_withoutPermission_throwsSecurityException() {
+ mShadowContext.denyPermissions(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+
+ expectThrows(
+ SecurityException.class,
+ () -> mBackupManagerService.setBackupEnabled(mUserId, true));
+ }
+
+ /**
+ * Test verifying that {@link BackupManagerService#setBackupEnabled(int, boolean)} does not
+ * require the caller to have INTERACT_ACROSS_USERS_FULL permission when the calling user id is
+ * the same as the target user id.
+ */
+ @Test
+ public void setBackupEnabled_whenCallingUserIsTargetUser_doesntNeedPermission() {
+ ShadowBinder.setCallingUserHandle(UserHandle.of(mUserId));
+ mShadowContext.denyPermissions(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+
+ mBackupManagerService.setBackupEnabled(mUserId, true);
+
+ verify(mUserBackupManagerService).setBackupEnabled(true);
+ }
+
/** Test that the backup service routes methods correctly to the user that requests it. */
@Test
public void setBackupEnabled_callsSetBackupEnabledForUser() throws Exception {
- mBackupManagerService.setBackupEnabled(true);
+ mShadowContext.grantPermissions(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+
+ mBackupManagerService.setBackupEnabled(mUserId, true);
verify(mUserBackupManagerService).setBackupEnabled(true);
}
@@ -299,10 +353,25 @@
verify(mUserBackupManagerService).setBackupProvisioned(true);
}
+ /**
+ * Test verifying that {@link BackupManagerService#isBackupEnabled(int)} throws a
+ * {@link SecurityException} if the caller does not have INTERACT_ACROSS_USERS_FULL permission.
+ */
+ @Test
+ public void testIsBackupEnabled_withoutPermission_throwsSecurityException() {
+ mShadowContext.denyPermissions(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+
+ expectThrows(
+ SecurityException.class,
+ () -> mBackupManagerService.isBackupEnabled(mUserId));
+ }
+
/** Test that the backup service routes methods correctly to the user that requests it. */
@Test
public void testIsBackupEnabled_callsIsBackupEnabledForUser() throws Exception {
- mBackupManagerService.isBackupEnabled();
+ mShadowContext.grantPermissions(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+
+ mBackupManagerService.isBackupEnabled(mUserId);
verify(mUserBackupManagerService).isBackupEnabled();
}
@@ -330,30 +399,81 @@
verify(mUserBackupManagerService).filterAppsEligibleForBackup(packages);
}
+ /**
+ * Test verifying that {@link BackupManagerService#backupNow(int)} throws a
+ * {@link SecurityException} if the caller does not have INTERACT_ACROSS_USERS_FULL permission.
+ */
+ @Test
+ public void testBackupNow_withoutPermission_throwsSecurityException() {
+ mShadowContext.denyPermissions(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+
+ expectThrows(
+ SecurityException.class,
+ () -> mBackupManagerService.backupNow(mUserId));
+ }
+
/** Test that the backup service routes methods correctly to the user that requests it. */
@Test
public void testBackupNow_callsBackupNowForUser() throws Exception {
- mBackupManagerService.backupNow();
+ mShadowContext.grantPermissions(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+
+ mBackupManagerService.backupNow(mUserId);
verify(mUserBackupManagerService).backupNow();
}
- /** Test that the backup service routes methods correctly to the user that requests it. */
+ /**
+ * Test verifying that {@link BackupManagerService#requestBackup(int, String[], IBackupObserver,
+ * IBackupManagerMonitor, int)} throws a {@link SecurityException} if the caller does not have
+ * INTERACT_ACROSS_USERS_FULL permission.
+ */
@Test
- public void testRequestBackup_callsRequestBackupForUser() throws Exception {
+ public void testRequestBackup_withoutPermission_throwsSecurityException() {
+ mShadowContext.denyPermissions(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
String[] packages = {TEST_PACKAGE};
IBackupObserver observer = mock(IBackupObserver.class);
IBackupManagerMonitor monitor = mock(IBackupManagerMonitor.class);
- mBackupManagerService.requestBackup(packages, observer, monitor, /* flags */ 0);
+ expectThrows(
+ SecurityException.class,
+ () -> mBackupManagerService.requestBackup(mUserId, packages, observer, monitor, 0));
+ }
+
+
+ /** Test that the backup service routes methods correctly to the user that requests it. */
+ @Test
+ public void testRequestBackup_callsRequestBackupForUser() throws Exception {
+ mShadowContext.grantPermissions(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+ String[] packages = {TEST_PACKAGE};
+ IBackupObserver observer = mock(IBackupObserver.class);
+ IBackupManagerMonitor monitor = mock(IBackupManagerMonitor.class);
+
+ mBackupManagerService.requestBackup(mUserId, packages, observer, monitor,
+ /* flags */ 0);
verify(mUserBackupManagerService).requestBackup(packages, observer, monitor, /* flags */ 0);
}
+ /**
+ * Test verifying that {@link BackupManagerService#cancelBackups(int)} throws a
+ * {@link SecurityException} if the caller does not have INTERACT_ACROSS_USERS_FULL permission.
+ */
+ @Test
+ public void testCancelBackups_withoutPermission_throwsSecurityException() {
+ mShadowContext.denyPermissions(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+
+ expectThrows(
+ SecurityException.class,
+ () -> mBackupManagerService.cancelBackups(mUserId));
+ }
+
+
/** Test that the backup service routes methods correctly to the user that requests it. */
@Test
public void testCancelBackups_callsCancelBackupsForUser() throws Exception {
- mBackupManagerService.cancelBackups();
+ mShadowContext.grantPermissions(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+
+ mBackupManagerService.cancelBackups(mUserId);
verify(mUserBackupManagerService).cancelBackups();
}
diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowBinder.java b/services/robotests/src/com/android/server/testing/shadows/ShadowBinder.java
index 1ece49e..9de9f50 100644
--- a/services/robotests/src/com/android/server/testing/shadows/ShadowBinder.java
+++ b/services/robotests/src/com/android/server/testing/shadows/ShadowBinder.java
@@ -17,9 +17,11 @@
package com.android.server.testing.shadows;
import android.os.Binder;
+import android.os.UserHandle;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
/**
* Extends {@link org.robolectric.shadows.ShadowBinder} with {@link Binder#clearCallingIdentity()}
@@ -30,6 +32,7 @@
public class ShadowBinder extends org.robolectric.shadows.ShadowBinder {
public static final Integer LOCAL_UID = 1000;
private static Integer originalCallingUid;
+ private static UserHandle sCallingUserHandle;
@Implementation
protected static long clearCallingIdentity() {
@@ -42,4 +45,30 @@
protected static void restoreCallingIdentity(long token) {
setCallingUid(originalCallingUid);
}
+
+ public static void setCallingUserHandle(UserHandle userHandle) {
+ sCallingUserHandle = userHandle;
+ }
+
+ /**
+ * Shadows {@link Binder#getCallingUserHandle()}. If {@link ShadowBinder#sCallingUserHandle}
+ * is set, return that; otherwise mimic the default implementation.
+ */
+ @Implementation
+ public static UserHandle getCallingUserHandle() {
+ if (sCallingUserHandle != null) {
+ return sCallingUserHandle;
+ } else {
+ return UserHandle.of(UserHandle.getUserId(getCallingUid()));
+ }
+ }
+
+ /**
+ * Clean up and reset state that was created for testing.
+ */
+ @Resetter
+ public static void reset() {
+ sCallingUserHandle = null;
+ org.robolectric.shadows.ShadowBinder.reset();
+ }
}
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index d49e78a..d7b1cb4 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -88,7 +88,7 @@
"utils/**/*.java",
],
static_libs: [
- "android-support-test",
+ "junit",
"mockito-target-minus-junit4",
],
libs: [
diff --git a/services/tests/servicestests/src/com/android/server/BinderCallsStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/BinderCallsStatsServiceTest.java
index f70efdf..f75617e 100644
--- a/services/tests/servicestests/src/com/android/server/BinderCallsStatsServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/BinderCallsStatsServiceTest.java
@@ -18,7 +18,6 @@
import static org.junit.Assert.assertEquals;
-import android.os.Binder;
import android.os.Process;
import android.platform.test.annotations.Presubmit;
@@ -26,9 +25,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.internal.os.BinderInternal;
-import com.android.internal.os.BinderInternal.CallSession;
-import com.android.server.BinderCallsStatsService.WorkSourceProvider;
+import com.android.server.BinderCallsStatsService.AuthorizedWorkSourceProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,7 +36,7 @@
public class BinderCallsStatsServiceTest {
@Test
public void weTrustOurselves() {
- WorkSourceProvider workSourceProvider = new WorkSourceProvider() {
+ AuthorizedWorkSourceProvider workSourceProvider = new AuthorizedWorkSourceProvider() {
protected int getCallingUid() {
return Process.myUid();
}
@@ -55,7 +52,7 @@
@Test
public void workSourceSetIfCallerHasPermission() {
- WorkSourceProvider workSourceProvider = new WorkSourceProvider() {
+ AuthorizedWorkSourceProvider workSourceProvider = new AuthorizedWorkSourceProvider() {
protected int getCallingUid() {
// System process uid which as UPDATE_DEVICE_STATS.
return 1001;
@@ -72,7 +69,7 @@
@Test
public void workSourceResolvedToCallingUid() {
- WorkSourceProvider workSourceProvider = new WorkSourceProvider() {
+ AuthorizedWorkSourceProvider workSourceProvider = new AuthorizedWorkSourceProvider() {
protected int getCallingUid() {
// UID without permissions.
return Integer.MAX_VALUE;
@@ -86,40 +83,4 @@
assertEquals(Integer.MAX_VALUE, workSourceProvider.resolveWorkSourceUid());
}
-
- @Test
- public void workSourceSet() {
- TestObserver observer = new TestObserver();
- observer.callStarted(new Binder(), 0);
- assertEquals(true, observer.workSourceSet);
- }
-
- static class TestObserver extends BinderCallsStatsService.BinderCallsObserver {
- public boolean workSourceSet = false;
-
- TestObserver() {
- super(new NoopObserver(), new WorkSourceProvider());
- }
-
- @Override
- protected void setThreadLocalWorkSourceUid(int uid) {
- workSourceSet = true;
- }
- }
-
-
- static class NoopObserver implements BinderInternal.Observer {
- @Override
- public CallSession callStarted(Binder binder, int code) {
- return null;
- }
-
- @Override
- public void callEnded(CallSession s, int parcelRequestSize, int parcelReplySize) {
- }
-
- @Override
- public void callThrewException(CallSession s, Exception exception) {
- }
- }
}
diff --git a/services/tests/servicestests/src/com/android/server/backup/TrampolineTest.java b/services/tests/servicestests/src/com/android/server/backup/TrampolineTest.java
index fd010f1..751ed9b 100644
--- a/services/tests/servicestests/src/com/android/server/backup/TrampolineTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/TrampolineTest.java
@@ -29,6 +29,7 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import android.annotation.UserIdInt;
import android.app.backup.BackupManager;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IBackupObserver;
@@ -86,8 +87,9 @@
new ComponentName("package1", "class1"),
new ComponentName("package2", "class2")
};
- private final int NON_USER_SYSTEM = UserHandle.USER_SYSTEM + 1;
+ private static final int NON_USER_SYSTEM = UserHandle.USER_SYSTEM + 1;
+ @UserIdInt private int mUserId;
@Mock private BackupManagerService mBackupManagerServiceMock;
@Mock private Context mContextMock;
@Mock private File mSuppressFileMock;
@@ -110,11 +112,13 @@
TrampolineTestable.sBackupManagerServiceMock = mBackupManagerServiceMock;
TrampolineTestable.sSuppressFile = mSuppressFileMock;
+ TrampolineTestable.sCallingUserId = UserHandle.USER_SYSTEM;
TrampolineTestable.sCallingUid = Process.SYSTEM_UID;
TrampolineTestable.sBackupDisabled = false;
when(mSuppressFileMock.getParentFile()).thenReturn(mSuppressFileParentMock);
+ mUserId = NON_USER_SYSTEM;
mTrampoline = new TrampolineTestable(mContextMock);
mContentResolver = new MockContentResolver();
@@ -361,10 +365,22 @@
}
@Test
- public void setBackupEnabled_forwarded() throws RemoteException {
+ public void setBackupEnabledForUser_forwarded() throws RemoteException {
mTrampoline.initializeService(UserHandle.USER_SYSTEM);
+
+ mTrampoline.setBackupEnabledForUser(mUserId, true);
+
+ verify(mBackupManagerServiceMock).setBackupEnabled(mUserId, true);
+ }
+
+ @Test
+ public void setBackupEnabled_forwardedToCallingUserId() throws RemoteException {
+ TrampolineTestable.sCallingUserId = mUserId;
+ mTrampoline.initializeService(UserHandle.USER_SYSTEM);
+
mTrampoline.setBackupEnabled(true);
- verify(mBackupManagerServiceMock).setBackupEnabled(true);
+
+ verify(mBackupManagerServiceMock).setBackupEnabled(mUserId, true);
}
@Test
@@ -400,10 +416,22 @@
}
@Test
- public void isBackupEnabled_forwarded() throws RemoteException {
+ public void isBackupEnabledForUser_forwarded() throws RemoteException {
mTrampoline.initializeService(UserHandle.USER_SYSTEM);
+
+ mTrampoline.isBackupEnabledForUser(mUserId);
+
+ verify(mBackupManagerServiceMock).isBackupEnabled(mUserId);
+ }
+
+ @Test
+ public void isBackupEnabled_forwardedToCallingUserId() throws RemoteException {
+ TrampolineTestable.sCallingUserId = mUserId;
+ mTrampoline.initializeService(UserHandle.USER_SYSTEM);
+
mTrampoline.isBackupEnabled();
- verify(mBackupManagerServiceMock).isBackupEnabled();
+
+ verify(mBackupManagerServiceMock).isBackupEnabled(mUserId);
}
@Test
@@ -439,10 +467,22 @@
}
@Test
- public void backupNow_forwarded() throws RemoteException {
+ public void backupNowForUser_forwarded() throws RemoteException {
mTrampoline.initializeService(UserHandle.USER_SYSTEM);
+
+ mTrampoline.backupNowForUser(mUserId);
+
+ verify(mBackupManagerServiceMock).backupNow(mUserId);
+ }
+
+ @Test
+ public void backupNow_forwardedToCallingUserId() throws RemoteException {
+ TrampolineTestable.sCallingUserId = mUserId;
+ mTrampoline.initializeService(UserHandle.USER_SYSTEM);
+
mTrampoline.backupNow();
- verify(mBackupManagerServiceMock).backupNow();
+
+ verify(mBackupManagerServiceMock).backupNow(mUserId);
}
@Test
@@ -798,15 +838,28 @@
}
@Test
- public void requestBackup_forwarded() throws RemoteException {
- when(mBackupManagerServiceMock.requestBackup(PACKAGE_NAMES, mBackupObserverMock,
- mBackupManagerMonitorMock, 123)).thenReturn(456);
-
+ public void requestBackupForUser_forwarded() throws RemoteException {
+ when(mBackupManagerServiceMock.requestBackup(mUserId, PACKAGE_NAMES,
+ mBackupObserverMock, mBackupManagerMonitorMock, 123)).thenReturn(456);
mTrampoline.initializeService(UserHandle.USER_SYSTEM);
- assertEquals(456, mTrampoline.requestBackup(PACKAGE_NAMES, mBackupObserverMock,
- mBackupManagerMonitorMock, 123));
- verify(mBackupManagerServiceMock).requestBackup(PACKAGE_NAMES, mBackupObserverMock,
- mBackupManagerMonitorMock, 123);
+
+ assertEquals(456, mTrampoline.requestBackupForUser(mUserId, PACKAGE_NAMES,
+ mBackupObserverMock, mBackupManagerMonitorMock, 123));
+ verify(mBackupManagerServiceMock).requestBackup(mUserId, PACKAGE_NAMES,
+ mBackupObserverMock, mBackupManagerMonitorMock, 123);
+ }
+
+ @Test
+ public void requestBackup_forwardedToCallingUserId() throws RemoteException {
+ TrampolineTestable.sCallingUserId = mUserId;
+ when(mBackupManagerServiceMock.requestBackup(NON_USER_SYSTEM, PACKAGE_NAMES,
+ mBackupObserverMock, mBackupManagerMonitorMock, 123)).thenReturn(456);
+ mTrampoline.initializeService(UserHandle.USER_SYSTEM);
+
+ assertEquals(456, mTrampoline.requestBackup(PACKAGE_NAMES,
+ mBackupObserverMock, mBackupManagerMonitorMock, 123));
+ verify(mBackupManagerServiceMock).requestBackup(mUserId, PACKAGE_NAMES,
+ mBackupObserverMock, mBackupManagerMonitorMock, 123);
}
@Test
@@ -816,10 +869,22 @@
}
@Test
- public void cancelBackups_forwarded() throws RemoteException {
+ public void cancelBackupsForUser_forwarded() throws RemoteException {
mTrampoline.initializeService(UserHandle.USER_SYSTEM);
+
+ mTrampoline.cancelBackupsForUser(mUserId);
+
+ verify(mBackupManagerServiceMock).cancelBackups(mUserId);
+ }
+
+ @Test
+ public void cancelBackups_forwardedToCallingUserId() throws RemoteException {
+ TrampolineTestable.sCallingUserId = mUserId;
+ mTrampoline.initializeService(UserHandle.USER_SYSTEM);
+
mTrampoline.cancelBackups();
- verify(mBackupManagerServiceMock).cancelBackups();
+
+ verify(mBackupManagerServiceMock).cancelBackups(mUserId);
}
@Test
@@ -891,6 +956,7 @@
private static class TrampolineTestable extends Trampoline {
static boolean sBackupDisabled = false;
static File sSuppressFile = null;
+ static int sCallingUserId = -1;
static int sCallingUid = -1;
static BackupManagerService sBackupManagerServiceMock = null;
private int mCreateServiceCallsCount = 0;
@@ -909,6 +975,10 @@
return sSuppressFile;
}
+ protected int binderGetCallingUserId() {
+ return sCallingUserId;
+ }
+
@Override
protected int binderGetCallingUid() {
return sCallingUid;
diff --git a/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
index c4c2ad9..839b25f 100644
--- a/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
@@ -212,7 +212,29 @@
mIApplicationThread,
PACKAGE_ONE,
ACTIVITY_COMPONENT,
- UserHandle.of(PRIMARY_USER)));
+ UserHandle.of(PRIMARY_USER).getIdentifier(),
+ true));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ any(Intent.class),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_currentUser() {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PRIMARY_USER).getIdentifier(),
+ false));
verify(mActivityTaskManagerInternal, never())
.startActivityAsUser(
@@ -234,7 +256,31 @@
mIApplicationThread,
PACKAGE_ONE,
ACTIVITY_COMPONENT,
- UserHandle.of(PROFILE_OF_PRIMARY_USER)));
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ true));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ any(Intent.class),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_profile_notInstalled() {
+ mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, false);
+
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ false));
verify(mActivityTaskManagerInternal, never())
.startActivityAsUser(
@@ -254,7 +300,29 @@
mIApplicationThread,
PACKAGE_TWO,
ACTIVITY_COMPONENT,
- UserHandle.of(PROFILE_OF_PRIMARY_USER)));
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ true));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ any(Intent.class),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_profile_fakeCaller() {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_TWO,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ false));
verify(mActivityTaskManagerInternal, never())
.startActivityAsUser(
@@ -276,7 +344,31 @@
mIApplicationThread,
PACKAGE_ONE,
ACTIVITY_COMPONENT,
- UserHandle.of(PROFILE_OF_PRIMARY_USER)));
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ true));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ any(Intent.class),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_profile_notExported() {
+ mActivityInfo.exported = false;
+
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ false));
verify(mActivityTaskManagerInternal, never())
.startActivityAsUser(
@@ -296,7 +388,29 @@
mIApplicationThread,
PACKAGE_ONE,
new ComponentName(PACKAGE_TWO, "test"),
- UserHandle.of(PROFILE_OF_PRIMARY_USER)));
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ true));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ any(Intent.class),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_profile_anotherPackage() {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ new ComponentName(PACKAGE_TWO, "test"),
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ false));
verify(mActivityTaskManagerInternal, never())
.startActivityAsUser(
@@ -316,7 +430,29 @@
mIApplicationThread,
PACKAGE_ONE,
ACTIVITY_COMPONENT,
- UserHandle.of(SECONDARY_USER)));
+ UserHandle.of(SECONDARY_USER).getIdentifier(),
+ true));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ any(Intent.class),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_secondaryUser() {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(SECONDARY_USER).getIdentifier(),
+ false));
verify(mActivityTaskManagerInternal, never())
.startActivityAsUser(
@@ -335,7 +471,8 @@
mIApplicationThread,
PACKAGE_ONE,
ACTIVITY_COMPONENT,
- UserHandle.of(PRIMARY_USER));
+ UserHandle.of(PRIMARY_USER).getIdentifier(),
+ true);
verify(mActivityTaskManagerInternal)
.startActivityAsUser(
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
index dad7b93..fd07cb0 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
@@ -18,10 +18,14 @@
import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.DynamicCodeFile;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -41,6 +45,10 @@
import com.android.server.pm.Installer;
+import dalvik.system.DelegateLastClassLoader;
+import dalvik.system.PathClassLoader;
+import dalvik.system.VMRuntime;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -50,10 +58,6 @@
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;
-import dalvik.system.DelegateLastClassLoader;
-import dalvik.system.PathClassLoader;
-import dalvik.system.VMRuntime;
-
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
@@ -129,6 +133,9 @@
// Package is not used by others, so we should get nothing back.
assertNoUseInfo(mFooUser0);
+
+ // A package loading its own code is not stored as DCL.
+ assertNoDclInfo(mFooUser0);
}
@Test
@@ -140,6 +147,8 @@
PackageUseInfo pui = getPackageUseInfo(mBarUser0);
assertIsUsedByOtherApps(mBarUser0, pui, true);
assertTrue(pui.getDexUseInfoMap().isEmpty());
+
+ assertHasDclInfo(mBarUser0, mFooUser0, mBarUser0.getBaseAndSplitDexPaths());
}
@Test
@@ -152,6 +161,8 @@
assertIsUsedByOtherApps(mFooUser0, pui, false);
assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0);
+
+ assertHasDclInfo(mFooUser0, mFooUser0, fooSecondaries);
}
@Test
@@ -164,6 +175,8 @@
assertIsUsedByOtherApps(mBarUser0, pui, false);
assertEquals(barSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(mFooUser0, pui, barSecondaries, /*isUsedByOtherApps*/true, mUser0);
+
+ assertHasDclInfo(mBarUser0, mFooUser0, barSecondaries);
}
@Test
@@ -200,9 +213,10 @@
}
@Test
- public void testPackageUseInfoNotFound() {
+ public void testNoNotify() {
// Assert we don't get back data we did not previously record.
assertNoUseInfo(mFooUser0);
+ assertNoDclInfo(mFooUser0);
}
@Test
@@ -210,6 +224,7 @@
// Notifying with an invalid ISA should be ignored.
notifyDexLoad(mInvalidIsa, mInvalidIsa.getSecondaryDexPaths(), mUser0);
assertNoUseInfo(mInvalidIsa);
+ assertNoDclInfo(mInvalidIsa);
}
@Test
@@ -218,6 +233,7 @@
// register in DexManager#load should be ignored.
notifyDexLoad(mDoesNotExist, mDoesNotExist.getBaseAndSplitDexPaths(), mUser0);
assertNoUseInfo(mDoesNotExist);
+ assertNoDclInfo(mDoesNotExist);
}
@Test
@@ -226,6 +242,8 @@
// Request should be ignored.
notifyDexLoad(mBarUser1, mBarUser0.getSecondaryDexPaths(), mUser1);
assertNoUseInfo(mBarUser1);
+
+ assertNoDclInfo(mBarUser1);
}
@Test
@@ -235,6 +253,10 @@
// still check that nothing goes unexpected in DexManager.
notifyDexLoad(mBarUser0, mFooUser0.getBaseAndSplitDexPaths(), mUser1);
assertNoUseInfo(mBarUser1);
+ assertNoUseInfo(mFooUser0);
+
+ assertNoDclInfo(mBarUser1);
+ assertNoDclInfo(mFooUser0);
}
@Test
@@ -247,6 +269,7 @@
// is trying to load something from it we should not find it.
notifyDexLoad(mFooUser0, newSecondaries, mUser0);
assertNoUseInfo(newPackage);
+ assertNoDclInfo(newPackage);
// Notify about newPackage install and let mFoo load its dexes.
mDexManager.notifyPackageInstalled(newPackage.mPackageInfo, mUser0);
@@ -257,6 +280,7 @@
assertIsUsedByOtherApps(newPackage, pui, false);
assertEquals(newSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(newPackage, pui, newSecondaries, /*isUsedByOtherApps*/true, mUser0);
+ assertHasDclInfo(newPackage, mFooUser0, newSecondaries);
}
@Test
@@ -273,6 +297,7 @@
assertIsUsedByOtherApps(newPackage, pui, false);
assertEquals(newSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(newPackage, pui, newSecondaries, /*isUsedByOtherApps*/false, mUser0);
+ assertHasDclInfo(newPackage, newPackage, newSecondaries);
}
@Test
@@ -305,6 +330,7 @@
// We shouldn't find yet the new split as we didn't notify the package update.
notifyDexLoad(mFooUser0, newSplits, mUser0);
assertNoUseInfo(mBarUser0);
+ assertNoDclInfo(mBarUser0);
// Notify that bar is updated. splitSourceDirs will contain the updated path.
mDexManager.notifyPackageUpdated(mBarUser0.getPackageName(),
@@ -314,8 +340,8 @@
// Now, when the split is loaded we will find it and we should mark Bar as usedByOthers.
notifyDexLoad(mFooUser0, newSplits, mUser0);
PackageUseInfo pui = getPackageUseInfo(mBarUser0);
- assertNotNull(pui);
assertIsUsedByOtherApps(newSplits, pui, true);
+ assertHasDclInfo(mBarUser0, mFooUser0, newSplits);
}
@Test
@@ -326,11 +352,15 @@
mDexManager.notifyPackageDataDestroyed(mBarUser0.getPackageName(), mUser0);
- // Bar should not be around since it was removed for all users.
+ // Data for user 1 should still be present
PackageUseInfo pui = getPackageUseInfo(mBarUser1);
- assertNotNull(pui);
assertSecondaryUse(mBarUser1, pui, mBarUser1.getSecondaryDexPaths(),
/*isUsedByOtherApps*/false, mUser1);
+ assertHasDclInfo(mBarUser1, mBarUser1, mBarUser1.getSecondaryDexPaths());
+
+ // But not user 0
+ assertNoUseInfo(mBarUser0, mUser0);
+ assertNoDclInfo(mBarUser0, mUser0);
}
@Test
@@ -349,6 +379,8 @@
PackageUseInfo pui = getPackageUseInfo(mFooUser0);
assertIsUsedByOtherApps(mFooUser0, pui, true);
assertTrue(pui.getDexUseInfoMap().isEmpty());
+
+ assertNoDclInfo(mFooUser0);
}
@Test
@@ -362,6 +394,7 @@
// Foo should not be around since all its secondary dex info were deleted
// and it is not used by other apps.
assertNoUseInfo(mFooUser0);
+ assertNoDclInfo(mFooUser0);
}
@Test
@@ -374,6 +407,7 @@
// Bar should not be around since it was removed for all users.
assertNoUseInfo(mBarUser0);
+ assertNoDclInfo(mBarUser0);
}
@Test
@@ -383,6 +417,7 @@
notifyDexLoad(mFooUser0, Arrays.asList(frameworkDex), mUser0);
// The dex file should not be recognized as a package.
assertFalse(mDexManager.hasInfoOnPackage(frameworkDex));
+ assertNull(mDexManager.getPackageDynamicCodeInfo(frameworkDex));
}
@Test
@@ -395,6 +430,8 @@
assertIsUsedByOtherApps(mFooUser0, pui, false);
assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0);
+
+ assertHasDclInfo(mFooUser0, mFooUser0, fooSecondaries);
}
@Test
@@ -402,7 +439,12 @@
List<String> secondaries = mBarUser0UnsupportedClassLoader.getSecondaryDexPaths();
notifyDexLoad(mBarUser0UnsupportedClassLoader, secondaries, mUser0);
+ // We don't record the dex usage
assertNoUseInfo(mBarUser0UnsupportedClassLoader);
+
+ // But we do record this as an intance of dynamic code loading
+ assertHasDclInfo(
+ mBarUser0UnsupportedClassLoader, mBarUser0UnsupportedClassLoader, secondaries);
}
@Test
@@ -414,6 +456,8 @@
notifyDexLoad(mBarUser0, classLoaders, classPaths, mUser0);
assertNoUseInfo(mBarUser0);
+
+ assertHasDclInfo(mBarUser0, mBarUser0, mBarUser0.getSecondaryDexPaths());
}
@Test
@@ -421,6 +465,7 @@
notifyDexLoad(mBarUser0, null, mUser0);
assertNoUseInfo(mBarUser0);
+ assertNoDclInfo(mBarUser0);
}
@Test
@@ -455,12 +500,14 @@
notifyDexLoad(mBarUser0, secondaries, mUser0);
PackageUseInfo pui = getPackageUseInfo(mBarUser0);
assertSecondaryUse(mBarUser0, pui, secondaries, /*isUsedByOtherApps*/false, mUser0);
+ assertHasDclInfo(mBarUser0, mBarUser0, secondaries);
// Record bar secondaries again with an unsupported class loader. This should not change the
// context.
notifyDexLoad(mBarUser0UnsupportedClassLoader, secondaries, mUser0);
pui = getPackageUseInfo(mBarUser0);
assertSecondaryUse(mBarUser0, pui, secondaries, /*isUsedByOtherApps*/false, mUser0);
+ assertHasDclInfo(mBarUser0, mBarUser0, secondaries);
}
@Test
@@ -533,13 +580,53 @@
private PackageUseInfo getPackageUseInfo(TestData testData) {
assertTrue(mDexManager.hasInfoOnPackage(testData.getPackageName()));
- return mDexManager.getPackageUseInfoOrDefault(testData.getPackageName());
+ PackageUseInfo pui = mDexManager.getPackageUseInfoOrDefault(testData.getPackageName());
+ assertNotNull(pui);
+ return pui;
}
private void assertNoUseInfo(TestData testData) {
assertFalse(mDexManager.hasInfoOnPackage(testData.getPackageName()));
}
+ private void assertNoUseInfo(TestData testData, int userId) {
+ if (!mDexManager.hasInfoOnPackage(testData.getPackageName())) {
+ return;
+ }
+ PackageUseInfo pui = getPackageUseInfo(testData);
+ for (DexUseInfo dexUseInfo : pui.getDexUseInfoMap().values()) {
+ assertNotEquals(userId, dexUseInfo.getOwnerUserId());
+ }
+ }
+
+ private void assertNoDclInfo(TestData testData) {
+ assertNull(mDexManager.getPackageDynamicCodeInfo(testData.getPackageName()));
+ }
+
+ private void assertNoDclInfo(TestData testData, int userId) {
+ PackageDynamicCode info = mDexManager.getPackageDynamicCodeInfo(testData.getPackageName());
+ if (info == null) {
+ return;
+ }
+
+ for (DynamicCodeFile fileInfo : info.mFileUsageMap.values()) {
+ assertNotEquals(userId, fileInfo.mUserId);
+ }
+ }
+
+ private void assertHasDclInfo(TestData owner, TestData loader, List<String> paths) {
+ PackageDynamicCode info = mDexManager.getPackageDynamicCodeInfo(owner.getPackageName());
+ assertNotNull("No DCL data for owner " + owner.getPackageName(), info);
+ for (String path : paths) {
+ DynamicCodeFile fileInfo = info.mFileUsageMap.get(path);
+ assertNotNull("No DCL data for path " + path, fileInfo);
+ assertEquals(PackageDynamicCodeLoading.FILE_TYPE_DEX, fileInfo.mFileType);
+ assertEquals(owner.mUserId, fileInfo.mUserId);
+ assertTrue("No DCL data for loader " + loader.getPackageName(),
+ fileInfo.mLoadingPackages.contains(loader.getPackageName()));
+ }
+ }
+
private static PackageInfo getMockPackageInfo(String packageName, int userId) {
PackageInfo pi = new PackageInfo();
pi.packageName = packageName;
@@ -563,11 +650,13 @@
private final PackageInfo mPackageInfo;
private final String mLoaderIsa;
private final String mClassLoader;
+ private final int mUserId;
private TestData(String packageName, String loaderIsa, int userId, String classLoader) {
mPackageInfo = getMockPackageInfo(packageName, userId);
mLoaderIsa = loaderIsa;
mClassLoader = classLoader;
+ mUserId = userId;
}
private TestData(String packageName, String loaderIsa, int userId) {
@@ -603,9 +692,7 @@
List<String> getBaseAndSplitDexPaths() {
List<String> paths = new ArrayList<>();
paths.add(mPackageInfo.applicationInfo.sourceDir);
- for (String split : mPackageInfo.applicationInfo.splitSourceDirs) {
- paths.add(split);
- }
+ Collections.addAll(paths, mPackageInfo.applicationInfo.splitSourceDirs);
return paths;
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java
new file mode 100644
index 0000000..eb4cc4e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java
@@ -0,0 +1,560 @@
+/*
+ * Copyright 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.pm.dex;
+
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.escape;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.unescape;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.pm.dex.PackageDynamicCodeLoading.DynamicCodeFile;
+import com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PackageDynamicCodeLoadingTests {
+
+ // Deliberately making a copy here since we're testing identity and
+ // string literals have a tendency to be identical.
+ private static final String TRIVIAL_STRING = new String("hello/world");
+ private static final Entry[] NO_ENTRIES = {};
+ private static final String[] NO_PACKAGES = {};
+
+ @Test
+ public void testRecord() {
+ Entry[] entries = {
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1"),
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2"),
+ new Entry("owning.package1", "/path/file2", 'D', 5, "loading.package1"),
+ new Entry("owning.package2", "/path/file3", 'D', 0, "loading.package2"),
+ };
+
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+ assertHasEntries(info, entries);
+ }
+
+ @Test
+ public void testRecord_returnsHasChanged() {
+ Entry owner1Path1Loader1 =
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1");
+ Entry owner1Path1Loader2 =
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2");
+ Entry owner1Path2Loader1 =
+ new Entry("owning.package1", "/path/file2", 'D', 10, "loading.package1");
+ Entry owner2Path1Loader1 =
+ new Entry("owning.package2", "/path/file1", 'D', 10, "loading.package2");
+
+ PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
+
+ assertTrue(record(info, owner1Path1Loader1));
+ assertFalse(record(info, owner1Path1Loader1));
+
+ assertTrue(record(info, owner1Path1Loader2));
+ assertFalse(record(info, owner1Path1Loader2));
+
+ assertTrue(record(info, owner1Path2Loader1));
+ assertFalse(record(info, owner1Path2Loader1));
+
+ assertTrue(record(info, owner2Path1Loader1));
+ assertFalse(record(info, owner2Path1Loader1));
+
+ assertHasEntries(info,
+ owner1Path1Loader1, owner1Path1Loader2, owner1Path2Loader1, owner2Path1Loader1);
+ }
+
+ @Test
+ public void testRecord_changeUserForFile_throws() {
+ Entry entry1 = new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1");
+ Entry entry2 = new Entry("owning.package1", "/path/file1", 'D', 20, "loading.package1");
+
+ PackageDynamicCodeLoading info = makePackageDcl(entry1);
+
+ assertThrows(() -> record(info, entry2));
+ assertHasEntries(info, entry1);
+ }
+
+ @Test
+ public void testRecord_badFileType_throws() {
+ Entry entry = new Entry("owning.package", "/path/file", 'Z', 10, "loading.package");
+ PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
+
+ assertThrows(() -> record(info, entry));
+ }
+
+ @Test
+ public void testClear() {
+ Entry[] entries = {
+ new Entry("owner1", "file1", 'D', 10, "loader1"),
+ new Entry("owner2", "file2", 'D', 20, "loader2"),
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ info.clear();
+ assertHasEntries(info, NO_ENTRIES);
+ }
+
+ @Test
+ public void testRemovePackage_present() {
+ Entry other = new Entry("other", "file", 'D', 0, "loader");
+ Entry[] entries = {
+ new Entry("owner", "file1", 'D', 10, "loader1"),
+ new Entry("owner", "file2", 'D', 20, "loader2"),
+ other
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertTrue(info.removePackage("owner"));
+ assertHasEntries(info, other);
+ assertHasPackages(info, "other");
+ }
+
+ @Test
+ public void testRemovePackage_notPresent() {
+ Entry[] entries = { new Entry("owner", "file", 'D', 0, "loader") };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertFalse(info.removePackage("other"));
+ assertHasEntries(info, entries);
+ }
+
+ @Test
+ public void testRemoveUserPackage_notPresent() {
+ Entry[] entries = { new Entry("owner", "file", 'D', 0, "loader") };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertFalse(info.removeUserPackage("other", 0));
+ assertHasEntries(info, entries);
+ }
+
+ @Test
+ public void testRemoveUserPackage_presentWithNoOtherUsers() {
+ Entry other = new Entry("other", "file", 'D', 0, "loader");
+ Entry[] entries = {
+ new Entry("owner", "file1", 'D', 0, "loader1"),
+ new Entry("owner", "file2", 'D', 0, "loader2"),
+ other
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertTrue(info.removeUserPackage("owner", 0));
+ assertHasEntries(info, other);
+ assertHasPackages(info, "other");
+ }
+
+ @Test
+ public void testRemoveUserPackage_presentWithUsers() {
+ Entry other = new Entry("owner", "file", 'D', 1, "loader");
+ Entry[] entries = {
+ new Entry("owner", "file1", 'D', 0, "loader1"),
+ new Entry("owner", "file2", 'D', 0, "loader2"),
+ other
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertTrue(info.removeUserPackage("owner", 0));
+ assertHasEntries(info, other);
+ }
+
+ @Test
+ public void testRemoveFile_present() {
+ Entry[] entries = {
+ new Entry("package1", "file1", 'D', 0, "loader1"),
+ new Entry("package1", "file2", 'D', 0, "loader1"),
+ new Entry("package2", "file1", 'D', 0, "loader2"),
+ };
+ Entry[] expectedSurvivors = {
+ new Entry("package1", "file2", 'D', 0, "loader1"),
+ new Entry("package2", "file1", 'D', 0, "loader2"),
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertTrue(info.removeFile("package1", "file1", 0));
+ assertHasEntries(info, expectedSurvivors);
+ }
+
+ @Test
+ public void testRemoveFile_onlyEntry() {
+ Entry[] entries = {
+ new Entry("package1", "file1", 'D', 0, "loader1"),
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertTrue(info.removeFile("package1", "file1", 0));
+ assertHasEntries(info, NO_ENTRIES);
+ assertHasPackages(info, NO_PACKAGES);
+ }
+
+ @Test
+ public void testRemoveFile_notPresent() {
+ Entry[] entries = {
+ new Entry("package1", "file2", 'D', 0, "loader1"),
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertFalse(info.removeFile("package1", "file1", 0));
+ assertHasEntries(info, entries);
+ }
+
+ @Test
+ public void testRemoveFile_wrongUser() {
+ Entry[] entries = {
+ new Entry("package1", "file1", 'D', 10, "loader1"),
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertFalse(info.removeFile("package1", "file1", 0));
+ assertHasEntries(info, entries);
+ }
+
+ @Test
+ public void testSyncData() {
+ Map<String, Set<Integer>> packageToUsersMap = ImmutableMap.of(
+ "package1", ImmutableSet.of(10, 20),
+ "package2", ImmutableSet.of(20));
+
+ Entry[] entries = {
+ new Entry("deleted.packaged", "file1", 'D', 10, "package1"),
+ new Entry("package1", "file2", 'D', 20, "package2"),
+ new Entry("package1", "file3", 'D', 10, "package2"),
+ new Entry("package1", "file3", 'D', 10, "deleted.package"),
+ new Entry("package2", "file4", 'D', 20, "deleted.package"),
+ };
+
+ Entry[] expectedSurvivors = {
+ new Entry("package1", "file2", 'D', 20, "package2"),
+ };
+
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+ info.syncData(packageToUsersMap);
+ assertHasEntries(info, expectedSurvivors);
+ assertHasPackages(info, "package1");
+ }
+
+ @Test
+ public void testRead_onlyHeader_emptyResult() throws Exception {
+ assertHasEntries(read("DCL1"), NO_ENTRIES);
+ }
+
+ @Test
+ public void testRead_noHeader_throws() {
+ assertThrows(IOException.class, () -> read(""));
+ }
+
+ @Test
+ public void testRead_wrongHeader_throws() {
+ assertThrows(IOException.class, () -> read("DCL2"));
+ }
+
+ @Test
+ public void testRead_oneEntry() throws Exception {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package\n"
+ + "D:10:loading.package:/path/fi\\\\le\n";
+ assertHasEntries(read(inputText),
+ new Entry("owning.package", "/path/fi\\le", 'D', 10, "loading.package"));
+ }
+
+ @Test
+ public void testRead_emptyPackage() throws Exception {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package\n";
+ PackageDynamicCodeLoading info = read(inputText);
+ assertHasEntries(info, NO_ENTRIES);
+ assertHasPackages(info, NO_PACKAGES);
+ }
+
+ @Test
+ public void testRead_complex() throws Exception {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package1\n"
+ + "D:10:loading.package1,loading.package2:/path/file1\n"
+ + "D:5:loading.package1:/path/file2\n"
+ + "P:owning.package2\n"
+ + "D:0:loading.package2:/path/file3";
+ assertHasEntries(read(inputText),
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1"),
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2"),
+ new Entry("owning.package1", "/path/file2", 'D', 5, "loading.package1"),
+ new Entry("owning.package2", "/path/file3", 'D', 0, "loading.package2"));
+ }
+
+ @Test
+ public void testRead_missingPackageLine_throws() {
+ String inputText = ""
+ + "DCL1\n"
+ + "D:10:loading.package:/path/file\n";
+ assertThrows(IOException.class, () -> read(inputText));
+ }
+
+ @Test
+ public void testRead_malformedFile_throws() {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package\n"
+ + "Hello world!\n";
+ assertThrows(IOException.class, () -> read(inputText));
+ }
+
+ @Test
+ public void testRead_badFileType_throws() {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package\n"
+ + "X:10:loading.package:/path/file\n";
+ assertThrows(IOException.class, () -> read(inputText));
+ }
+
+ @Test
+ public void testRead_badUserId_throws() {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package\n"
+ + "D:999999999999999999:loading.package:/path/file\n";
+ assertThrows(IOException.class, () -> read(inputText));
+ }
+
+ @Test
+ public void testRead_missingPackages_throws() {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package\n"
+ + "D:1:,:/path/file\n";
+ assertThrows(IOException.class, () -> read(inputText));
+ }
+
+ @Test
+ public void testWrite_empty() throws Exception {
+ assertEquals("DCL1\n", write(NO_ENTRIES));
+ }
+
+ @Test
+ public void testWrite_oneEntry() throws Exception {
+ String expected = ""
+ + "DCL1\n"
+ + "P:owning.package\n"
+ + "D:10:loading.package:/path/fi\\\\le\n";
+ String actual = write(
+ new Entry("owning.package", "/path/fi\\le", 'D', 10, "loading.package"));
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testWrite_complex_roundTrips() throws Exception {
+ // There isn't a canonical order for the output in the presence of multiple items.
+ // So we just check that if we read back what we write we end up where we started.
+ Entry[] entries = {
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1"),
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2"),
+ new Entry("owning.package1", "/path/file2", 'D', 5, "loading.package1"),
+ new Entry("owning.package2", "/path/fi\\le3", 'D', 0, "loading.package2")
+ };
+ assertHasEntries(read(write(entries)), entries);
+ }
+
+ @Test
+ public void testWrite_failure_throws() {
+ PackageDynamicCodeLoading info = makePackageDcl(
+ new Entry("owning.package", "/path/fi\\le", 'D', 10, "loading.package"));
+ assertThrows(IOException.class, () -> info.write(new ThrowingOutputStream()));
+ }
+
+ @Test
+ public void testEscape_trivialCase_returnsSameString() {
+ assertSame(TRIVIAL_STRING, escape(TRIVIAL_STRING));
+ }
+
+ @Test
+ public void testEscape() {
+ String input = "backslash\\newline\nreturn\r";
+ String expected = "backslash\\\\newline\\nreturn\\r";
+ assertEquals(expected, escape(input));
+ }
+
+ @Test
+ public void testUnescape_trivialCase_returnsSameString() throws Exception {
+ assertSame(TRIVIAL_STRING, unescape(TRIVIAL_STRING));
+ }
+
+ @Test
+ public void testUnescape() throws Exception {
+ String input = "backslash\\\\newline\\nreturn\\r";
+ String expected = "backslash\\newline\nreturn\r";
+ assertEquals(expected, unescape(input));
+ }
+
+ @Test
+ public void testUnescape_badEscape_throws() {
+ assertThrows(IOException.class, () -> unescape("this is \\bad"));
+ }
+
+ @Test
+ public void testUnescape_trailingBackslash_throws() {
+ assertThrows(IOException.class, () -> unescape("don't do this\\"));
+ }
+
+ @Test
+ public void testEscapeUnescape_roundTrips() throws Exception {
+ assertRoundTripsWithEscape("foo");
+ assertRoundTripsWithEscape("\\\\\n\n\r");
+ assertRoundTripsWithEscape("\\a\\b\\");
+ assertRoundTripsWithEscape("\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\");
+ }
+
+ private void assertRoundTripsWithEscape(String original) throws Exception {
+ assertEquals(original, unescape(escape(original)));
+ }
+
+ private boolean record(PackageDynamicCodeLoading info, Entry entry) {
+ return info.record(entry.mOwningPackage, entry.mPath, entry.mFileType, entry.mUserId,
+ entry.mLoadingPackage);
+ }
+
+ private PackageDynamicCodeLoading read(String inputText) throws Exception {
+ ByteArrayInputStream inputStream =
+ new ByteArrayInputStream(inputText.getBytes(UTF_8));
+
+ PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
+ info.read(inputStream);
+
+ return info;
+ }
+
+ private String write(Entry... entries) throws Exception {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ makePackageDcl(entries).write(output);
+ return new String(output.toByteArray(), UTF_8);
+ }
+
+ private Set<Entry> entriesFrom(PackageDynamicCodeLoading info) {
+ ImmutableSet.Builder<Entry> entries = ImmutableSet.builder();
+ for (String owningPackage : info.getAllPackagesWithDynamicCodeLoading()) {
+ PackageDynamicCode packageInfo = info.getPackageDynamicCodeInfo(owningPackage);
+ Map<String, DynamicCodeFile> usageMap = packageInfo.mFileUsageMap;
+ for (Map.Entry<String, DynamicCodeFile> fileEntry : usageMap.entrySet()) {
+ String path = fileEntry.getKey();
+ DynamicCodeFile fileInfo = fileEntry.getValue();
+ for (String loadingPackage : fileInfo.mLoadingPackages) {
+ entries.add(new Entry(owningPackage, path, fileInfo.mFileType, fileInfo.mUserId,
+ loadingPackage));
+ }
+ }
+ }
+
+ return entries.build();
+ }
+
+ private PackageDynamicCodeLoading makePackageDcl(Entry... entries) {
+ PackageDynamicCodeLoading result = new PackageDynamicCodeLoading();
+ for (Entry entry : entries) {
+ result.record(entry.mOwningPackage, entry.mPath, entry.mFileType, entry.mUserId,
+ entry.mLoadingPackage);
+ }
+ return result;
+
+ }
+
+ private void assertHasEntries(PackageDynamicCodeLoading info, Entry... expected) {
+ assertEquals(ImmutableSet.copyOf(expected), entriesFrom(info));
+ }
+
+ private void assertHasPackages(PackageDynamicCodeLoading info, String... expected) {
+ assertEquals(ImmutableSet.copyOf(expected), info.getAllPackagesWithDynamicCodeLoading());
+ }
+
+ /**
+ * Immutable representation of one entry in the dynamic code loading data (one package
+ * owning one file loaded by one package). Has well-behaved equality, hash and toString
+ * for ease of use in assertions.
+ */
+ private static class Entry {
+ private final String mOwningPackage;
+ private final String mPath;
+ private final char mFileType;
+ private final int mUserId;
+ private final String mLoadingPackage;
+
+ private Entry(String owningPackage, String path, char fileType, int userId,
+ String loadingPackage) {
+ mOwningPackage = owningPackage;
+ mPath = path;
+ mFileType = fileType;
+ mUserId = userId;
+ mLoadingPackage = loadingPackage;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Entry that = (Entry) o;
+ return mFileType == that.mFileType
+ && mUserId == that.mUserId
+ && Objects.equals(mOwningPackage, that.mOwningPackage)
+ && Objects.equals(mPath, that.mPath)
+ && Objects.equals(mLoadingPackage, that.mLoadingPackage);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mOwningPackage, mPath, mFileType, mUserId, mLoadingPackage);
+ }
+
+ @Override
+ public String toString() {
+ return "Entry("
+ + "\"" + mOwningPackage + '"'
+ + ", \"" + mPath + '"'
+ + ", '" + mFileType + '\''
+ + ", " + mUserId
+ + ", \"" + mLoadingPackage + '\"'
+ + ')';
+ }
+ }
+
+ private static class ThrowingOutputStream extends OutputStream {
+ @Override
+ public void write(int b) throws IOException {
+ throw new IOException("Intentional failure");
+ }
+ }
+}
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 8ac2930..561c61f 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -37,6 +37,7 @@
import android.os.SystemClock;
import android.os.SystemProperties;
import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import com.android.internal.app.IBatteryStats;
@@ -193,4 +194,19 @@
assertThat(mService.getWakefulness()).isEqualTo(WAKEFULNESS_ASLEEP);
}
+
+ @MediumTest
+ public void testWasDeviceIdleFor_true() {
+ int interval = 1000;
+ mService.onUserActivity();
+ SystemClock.sleep(interval);
+ assertThat(mService.wasDeviceIdleForInternal(interval)).isTrue();
+ }
+
+ @SmallTest
+ public void testWasDeviceIdleFor_false() {
+ int interval = 1000;
+ mService.onUserActivity();
+ assertThat(mService.wasDeviceIdleForInternal(interval)).isFalse();
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
index c1963da..94d293e 100644
--- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
@@ -281,6 +281,10 @@
mFakeHal.mCallback.onValues(newSkin);
verify(mIPowerManagerMock, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
.times(1)).shutdown(false, PowerManager.SHUTDOWN_THERMAL_STATE, false);
+ Temperature newBattery = new Temperature(60, Temperature.TYPE_BATTERY, "batt", status);
+ mFakeHal.mCallback.onValues(newBattery);
+ verify(mIPowerManagerMock, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
+ .times(1)).shutdown(false, PowerManager.SHUTDOWN_BATTERY_THERMAL_STATE, false);
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java b/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java
index a9d4519..70fadd1 100644
--- a/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java
@@ -20,8 +20,11 @@
import static org.junit.Assert.fail;
+import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
+import android.util.ArrayMap;
+
import androidx.test.runner.AndroidJUnit4;
import com.google.common.collect.Sets;
@@ -33,6 +36,7 @@
import java.util.Arrays;
import java.util.Collections;
+import java.util.Map;
import java.util.Set;
@@ -46,10 +50,25 @@
return Sets.newHashSet(values);
}
+ private static <K, V> Map<K, V> mapOf(Object... keyValuePairs) {
+ if (keyValuePairs.length % 2 != 0) {
+ throw new IllegalArgumentException();
+ }
+ final int len = keyValuePairs.length / 2;
+ ArrayMap<K, V> m = new ArrayMap<>(len);
+ for (int i = 0; i < len; ++i) {
+ m.put((K) keyValuePairs[i * 2], (V) keyValuePairs[(i * 2) + 1]);
+ }
+ return Collections.unmodifiableMap(m);
+
+ }
+
+
@Test
public void testParsePerSdkConfigSdkMinMax() throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject("{\"minSdk\":2, \"maxSdk\": 3, \"values\": []}");
- SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, emptySet());
+ SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, emptySet(),
+ emptyMap());
assertThat(config.minSdk).isEqualTo(2);
assertThat(config.maxSdk).isEqualTo(3);
}
@@ -58,7 +77,7 @@
public void testParsePerSdkConfigNoMinSdk() throws JSONException {
JSONObject json = new JSONObject("{\"maxSdk\": 3, \"values\": []}");
try {
- SignedConfig.parsePerSdkConfig(json, emptySet());
+ SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -69,7 +88,7 @@
public void testParsePerSdkConfigNoMaxSdk() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"values\": []}");
try {
- SignedConfig.parsePerSdkConfig(json, emptySet());
+ SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -80,7 +99,7 @@
public void testParsePerSdkConfigNoValues() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 3}");
try {
- SignedConfig.parsePerSdkConfig(json, emptySet());
+ SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -88,10 +107,10 @@
}
@Test
- public void testParsePerSdkConfigSdkNullMinSdk() throws JSONException, InvalidConfigException {
+ public void testParsePerSdkConfigSdkNullMinSdk() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\":null, \"maxSdk\": 3, \"values\": []}");
try {
- SignedConfig.parsePerSdkConfig(json, emptySet());
+ SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -99,10 +118,10 @@
}
@Test
- public void testParsePerSdkConfigSdkNullMaxSdk() throws JSONException, InvalidConfigException {
+ public void testParsePerSdkConfigSdkNullMaxSdk() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\":1, \"maxSdk\": null, \"values\": []}");
try {
- SignedConfig.parsePerSdkConfig(json, emptySet());
+ SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -113,7 +132,7 @@
public void testParsePerSdkConfigNullValues() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 3, \"values\": null}");
try {
- SignedConfig.parsePerSdkConfig(json, emptySet());
+ SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -124,7 +143,8 @@
public void testParsePerSdkConfigZeroValues()
throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 3, \"values\": []}");
- SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"));
+ SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
+ emptyMap());
assertThat(config.values).hasSize(0);
}
@@ -133,18 +153,30 @@
throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
- SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"));
+ SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
+ emptyMap());
assertThat(config.values).containsExactly("a", "1");
}
@Test
+ public void testParsePerSdkConfigSingleKeyNullValue()
+ throws JSONException, InvalidConfigException {
+ JSONObject json = new JSONObject(
+ "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": null}]}");
+ SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
+ emptyMap());
+ assertThat(config.values.keySet()).containsExactly("a");
+ assertThat(config.values.get("a")).isNull();
+ }
+
+ @Test
public void testParsePerSdkConfigMultiKeys()
throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}, "
+ "{\"key\":\"c\", \"value\": \"2\"}]}");
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(
- json, setOf("a", "b", "c"));
+ json, setOf("a", "b", "c"), emptyMap());
assertThat(config.values).containsExactly("a", "1", "c", "2");
}
@@ -153,7 +185,7 @@
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
try {
- SignedConfig.parsePerSdkConfig(json, setOf("b"));
+ SignedConfig.parsePerSdkConfig(json, setOf("b"), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -161,11 +193,67 @@
}
@Test
+ public void testParsePerSdkConfigSingleKeyWithMap()
+ throws JSONException, InvalidConfigException {
+ JSONObject json = new JSONObject(
+ "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
+ SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a"),
+ mapOf("a", mapOf("1", "one")));
+ assertThat(config.values).containsExactly("a", "one");
+ }
+
+ @Test
+ public void testParsePerSdkConfigSingleKeyWithMapInvalidValue() throws JSONException {
+ JSONObject json = new JSONObject(
+ "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"2\"}]}");
+ try {
+ SignedConfig.parsePerSdkConfig(json, setOf("b"), mapOf("a", mapOf("1", "one")));
+ fail("Expected InvalidConfigException");
+ } catch (InvalidConfigException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testParsePerSdkConfigMultiKeysWithMap()
+ throws JSONException, InvalidConfigException {
+ JSONObject json = new JSONObject(
+ "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"},"
+ + "{\"key\":\"b\", \"value\": \"1\"}]}");
+ SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
+ mapOf("a", mapOf("1", "one")));
+ assertThat(config.values).containsExactly("a", "one", "b", "1");
+ }
+
+ @Test
+ public void testParsePerSdkConfigSingleKeyWithMapToNull()
+ throws JSONException, InvalidConfigException {
+ JSONObject json = new JSONObject(
+ "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
+ SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a"),
+ mapOf("a", mapOf("1", null)));
+ assertThat(config.values).containsExactly("a", null);
+ }
+
+ @Test
+ public void testParsePerSdkConfigSingleKeyWithMapFromNull()
+ throws JSONException, InvalidConfigException {
+ assertThat(mapOf(null, "allitnil")).containsExactly(null, "allitnil");
+ assertThat(mapOf(null, "allitnil").containsKey(null)).isTrue();
+ JSONObject json = new JSONObject(
+ "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": null}]}");
+ SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a"),
+ mapOf("a", mapOf(null, "allitnil")));
+ assertThat(config.values).containsExactly("a", "allitnil");
+ }
+
+ @Test
public void testParsePerSdkConfigSingleKeyNoValue()
throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\"}]}");
- SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"));
+ SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
+ emptyMap());
assertThat(config.values).containsExactly("a", null);
}
@@ -173,7 +261,7 @@
public void testParsePerSdkConfigValuesInvalid() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 1, \"values\": \"foo\"}");
try {
- SignedConfig.parsePerSdkConfig(json, emptySet());
+ SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -184,7 +272,7 @@
public void testParsePerSdkConfigConfigEntryInvalid() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [1, 2]}");
try {
- SignedConfig.parsePerSdkConfig(json, emptySet());
+ SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -195,7 +283,7 @@
public void testParsePerSdkConfigConfigEntryNull() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [null]}");
try {
- SignedConfig.parsePerSdkConfig(json, emptySet());
+ SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -205,14 +293,15 @@
@Test
public void testParseVersion() throws InvalidConfigException {
SignedConfig config = SignedConfig.parse(
- "{\"version\": 1, \"config\": []}", emptySet());
+ "{\"version\": 1, \"config\": []}", emptySet(), emptyMap());
assertThat(config.version).isEqualTo(1);
}
@Test
public void testParseVersionInvalid() {
try {
- SignedConfig.parse("{\"version\": \"notanint\", \"config\": []}", emptySet());
+ SignedConfig.parse("{\"version\": \"notanint\", \"config\": []}", emptySet(),
+ emptyMap());
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
//expected
@@ -222,7 +311,7 @@
@Test
public void testParseNoVersion() {
try {
- SignedConfig.parse("{\"config\": []}", emptySet());
+ SignedConfig.parse("{\"config\": []}", emptySet(), emptyMap());
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
//expected
@@ -232,7 +321,7 @@
@Test
public void testParseNoConfig() {
try {
- SignedConfig.parse("{\"version\": 1}", emptySet());
+ SignedConfig.parse("{\"version\": 1}", emptySet(), emptyMap());
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
//expected
@@ -242,7 +331,7 @@
@Test
public void testParseConfigNull() {
try {
- SignedConfig.parse("{\"version\": 1, \"config\": null}", emptySet());
+ SignedConfig.parse("{\"version\": 1, \"config\": null}", emptySet(), emptyMap());
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
//expected
@@ -252,7 +341,7 @@
@Test
public void testParseVersionNull() {
try {
- SignedConfig.parse("{\"version\": null, \"config\": []}", emptySet());
+ SignedConfig.parse("{\"version\": null, \"config\": []}", emptySet(), emptyMap());
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
//expected
@@ -262,7 +351,7 @@
@Test
public void testParseConfigInvalidEntry() {
try {
- SignedConfig.parse("{\"version\": 1, \"config\": [{}]}", emptySet());
+ SignedConfig.parse("{\"version\": 1, \"config\": [{}]}", emptySet(), emptyMap());
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
//expected
@@ -273,7 +362,7 @@
public void testParseSdkConfigSingle() throws InvalidConfigException {
SignedConfig config = SignedConfig.parse(
"{\"version\": 1, \"config\":[{\"minSdk\": 1, \"maxSdk\": 1, \"values\": []}]}",
- emptySet());
+ emptySet(), emptyMap());
assertThat(config.perSdkConfig).hasSize(1);
}
@@ -281,7 +370,8 @@
public void testParseSdkConfigMultiple() throws InvalidConfigException {
SignedConfig config = SignedConfig.parse(
"{\"version\": 1, \"config\":[{\"minSdk\": 1, \"maxSdk\": 1, \"values\": []}, "
- + "{\"minSdk\": 2, \"maxSdk\": 2, \"values\": []}]}", emptySet());
+ + "{\"minSdk\": 2, \"maxSdk\": 2, \"values\": []}]}", emptySet(),
+ emptyMap());
assertThat(config.perSdkConfig).hasSize(2);
}
@@ -314,7 +404,6 @@
SignedConfig config = new SignedConfig(0, Arrays.asList(sdk13, sdk46));
assertThat(config.getMatchingConfig(2)).isEqualTo(sdk13);
}
-
@Test
public void testGetMatchingConfigNoMatch() {
SignedConfig.PerSdkConfig sdk1 = new SignedConfig.PerSdkConfig(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index d950360..9556111 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -238,6 +238,11 @@
protected void reportUserInteraction(NotificationRecord r) {
return;
}
+
+ @Override
+ protected void handleSavePolicyFile() {
+ return;
+ }
}
private class TestableToastCallback extends ITransientNotification.Stub {
@@ -1809,8 +1814,7 @@
mListener = mock(ManagedServices.ManagedServiceInfo.class);
when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false);
when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
-
- try {
+try {
mBinderService.getNotificationChannelGroupsFromPrivilegedListener(
null, PKG, Process.myUserHandle());
fail("listeners that don't have a companion device shouldn't be able to call this");
diff --git a/services/usb/java/com/android/server/usb/UsbHostManager.java b/services/usb/java/com/android/server/usb/UsbHostManager.java
index 613ba00..904d55e 100644
--- a/services/usb/java/com/android/server/usb/UsbHostManager.java
+++ b/services/usb/java/com/android/server/usb/UsbHostManager.java
@@ -32,7 +32,9 @@
import android.service.usb.UsbHostManagerProto;
import android.service.usb.UsbIsHeadsetProto;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.Slog;
+import android.util.StatsLog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.IndentingPrintWriter;
@@ -86,6 +88,7 @@
private int mNumConnects; // TOTAL # of connect/disconnect
private final LinkedList<ConnectionRecord> mConnections = new LinkedList<ConnectionRecord>();
private ConnectionRecord mLastConnect;
+ private final ArrayMap<String, ConnectionRecord> mConnected = new ArrayMap<>();
/*
* ConnectionRecord
@@ -300,6 +303,11 @@
if (mode != ConnectionRecord.DISCONNECT) {
mLastConnect = rec;
}
+ if (mode == ConnectionRecord.CONNECT) {
+ mConnected.put(deviceAddress, rec);
+ } else if (mode == ConnectionRecord.DISCONNECT) {
+ mConnected.remove(deviceAddress);
+ }
}
private void logUsbDevice(UsbDescriptorParser descriptorParser) {
@@ -408,6 +416,14 @@
// Tracking
addConnectionRecord(deviceAddress, ConnectionRecord.CONNECT,
parser.getRawDescriptors());
+
+ // Stats collection
+ if (parser.hasAudioInterface()) {
+ StatsLog.write(StatsLog.USB_DEVICE_ATTACHED, newDevice.getVendorId(),
+ newDevice.getProductId(), parser.hasAudioInterface(),
+ parser.hasHIDInterface(), parser.hasStorageInterface(),
+ StatsLog.USB_DEVICE_ATTACHED__STATE__STATE_CONNECTED, 0);
+ }
}
}
@@ -432,9 +448,22 @@
mUsbAlsaManager.usbDeviceRemoved(deviceAddress);
mSettingsManager.usbDeviceRemoved(device);
getCurrentUserSettings().usbDeviceRemoved(device);
-
+ ConnectionRecord current = mConnected.get(deviceAddress);
// Tracking
addConnectionRecord(deviceAddress, ConnectionRecord.DISCONNECT, null);
+
+ if (current != null) {
+ UsbDescriptorParser parser = new UsbDescriptorParser(deviceAddress,
+ current.mDescriptors);
+ if (parser.hasAudioInterface()) {
+ // Stats collection
+ StatsLog.write(StatsLog.USB_DEVICE_ATTACHED, device.getVendorId(),
+ device.getProductId(), parser.hasAudioInterface(),
+ parser.hasHIDInterface(), parser.hasStorageInterface(),
+ StatsLog.USB_DEVICE_ATTACHED__STATE__STATE_DISCONNECTED,
+ System.currentTimeMillis() - current.mTimestamp);
+ }
+ }
} else {
Slog.d(TAG, "Removed device at " + deviceAddress + " was already gone");
}
diff --git a/services/usb/java/com/android/server/usb/UsbPortManager.java b/services/usb/java/com/android/server/usb/UsbPortManager.java
index 33da403..96618f5 100644
--- a/services/usb/java/com/android/server/usb/UsbPortManager.java
+++ b/services/usb/java/com/android/server/usb/UsbPortManager.java
@@ -41,12 +41,14 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.service.usb.UsbPortInfoProto;
import android.service.usb.UsbPortManagerProto;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
+import android.util.StatsLog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.IndentingPrintWriter;
@@ -54,6 +56,7 @@
import com.android.server.FgThread;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.NoSuchElementException;
/**
@@ -117,6 +120,10 @@
private final ArrayMap<String, RawPortInfo> mSimulatedPorts =
new ArrayMap<>();
+ // Maintains the current connected status of the port.
+ // Uploads logs only when the connection status is changes.
+ private final HashMap<String, Boolean> mConnected = new HashMap<>();
+
public UsbPortManager(Context context) {
mContext = context;
try {
@@ -700,6 +707,18 @@
// Guard against possible reentrance by posting the broadcast from the handler
// instead of from within the critical section.
mHandler.post(() -> mContext.sendBroadcastAsUser(intent, UserHandle.ALL));
+
+ // Log to statsd
+ if (!mConnected.containsKey(portInfo.mUsbPort.getId())
+ || (mConnected.get(portInfo.mUsbPort.getId())
+ != portInfo.mUsbPortStatus.isConnected())) {
+ mConnected.put(portInfo.mUsbPort.getId(), portInfo.mUsbPortStatus.isConnected());
+ StatsLog.write(StatsLog.USB_CONNECTOR_STATE_CHANGED,
+ portInfo.mUsbPortStatus.isConnected()
+ ? StatsLog.USB_CONNECTOR_STATE_CHANGED__STATE__STATE_CONNECTED :
+ StatsLog.USB_CONNECTOR_STATE_CHANGED__STATE__STATE_DISCONNECTED,
+ portInfo.mUsbPort.getId(), portInfo.mLastConnectDurationMillis);
+ }
}
private static void logAndPrint(int priority, IndentingPrintWriter pw, String msg) {
@@ -746,7 +765,12 @@
public boolean mCanChangeMode;
public boolean mCanChangePowerRole;
public boolean mCanChangeDataRole;
- public int mDisposition; // default initialized to 0 which means added
+ // default initialized to 0 which means added
+ public int mDisposition;
+ // Tracks elapsedRealtime() of when the port was connected
+ public long mConnectedAtMillis;
+ // 0 when port is connected. Else reports the last connected duration
+ public long mLastConnectDurationMillis;
public PortInfo(String portId, int supportedModes) {
mUsbPort = new UsbPort(portId, supportedModes);
@@ -756,6 +780,8 @@
int currentPowerRole, boolean canChangePowerRole,
int currentDataRole, boolean canChangeDataRole,
int supportedRoleCombinations) {
+ boolean dispositionChanged = false;
+
mCanChangeMode = canChangeMode;
mCanChangePowerRole = canChangePowerRole;
mCanChangeDataRole = canChangeDataRole;
@@ -767,9 +793,18 @@
!= supportedRoleCombinations) {
mUsbPortStatus = new UsbPortStatus(currentMode, currentPowerRole, currentDataRole,
supportedRoleCombinations);
- return true;
+ dispositionChanged = true;
}
- return false;
+
+ if (mUsbPortStatus.isConnected() && mConnectedAtMillis == 0) {
+ mConnectedAtMillis = SystemClock.elapsedRealtime();
+ mLastConnectDurationMillis = 0;
+ } else if (!mUsbPortStatus.isConnected() && mConnectedAtMillis != 0) {
+ mLastConnectDurationMillis = SystemClock.elapsedRealtime() - mConnectedAtMillis;
+ mConnectedAtMillis = 0;
+ }
+
+ return dispositionChanged;
}
void dump(@NonNull DualDumpOutputStream dump, @NonNull String idName, long id) {
@@ -782,6 +817,10 @@
mCanChangePowerRole);
dump.write("can_change_data_role", UsbPortInfoProto.CAN_CHANGE_DATA_ROLE,
mCanChangeDataRole);
+ dump.write("connected_at_millis",
+ UsbPortInfoProto.CONNECTED_AT_MILLIS, mConnectedAtMillis);
+ dump.write("last_connect_duration_millis",
+ UsbPortInfoProto.LAST_CONNECT_DURATION_MILLIS, mLastConnectDurationMillis);
dump.end(token);
}
@@ -791,7 +830,9 @@
return "port=" + mUsbPort + ", status=" + mUsbPortStatus
+ ", canChangeMode=" + mCanChangeMode
+ ", canChangePowerRole=" + mCanChangePowerRole
- + ", canChangeDataRole=" + mCanChangeDataRole;
+ + ", canChangeDataRole=" + mCanChangeDataRole
+ + ", connectedAtMillis=" + mConnectedAtMillis
+ + ", lastConnectDurationMillis=" + mLastConnectDurationMillis;
}
}
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
index d617de0..36d0188 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -122,10 +122,21 @@
* The key to retrieve the optional {@code PhoneAccount}s Telecom can bundle with its Call
* extras. Used to pass the phone accounts to display on the front end to the user in order to
* select phone accounts to (for example) place a call.
+ * @deprecated Use the list from {@link #EXTRA_SUGGESTED_PHONE_ACCOUNTS} instead.
*/
+ @Deprecated
public static final String AVAILABLE_PHONE_ACCOUNTS = "selectPhoneAccountAccounts";
/**
+ * Key for extra used to pass along a list of {@link PhoneAccountSuggestion}s to the in-call
+ * UI when a call enters the {@link #STATE_SELECT_PHONE_ACCOUNT} state. The list included here
+ * will have the same length and be in the same order as the list passed with
+ * {@link #AVAILABLE_PHONE_ACCOUNTS}.
+ */
+ public static final String EXTRA_SUGGESTED_PHONE_ACCOUNTS =
+ "android.telecom.extra.SUGGESTED_PHONE_ACCOUNTS";
+
+ /**
* Extra key used to indicate the time (in milliseconds since midnight, January 1, 1970 UTC)
* when the last outgoing emergency call was made. This is used to identify potential emergency
* callbacks.
diff --git a/telecomm/java/android/telecom/PhoneAccountSuggestion.java b/telecomm/java/android/telecom/PhoneAccountSuggestion.java
new file mode 100644
index 0000000..4e6a178
--- /dev/null
+++ b/telecomm/java/android/telecom/PhoneAccountSuggestion.java
@@ -0,0 +1,135 @@
+/*
+ * 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.telecom;
+
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+public final class PhoneAccountSuggestion implements Parcelable {
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {REASON_NONE, REASON_INTRA_CARRIER, REASON_FREQUENT,
+ REASON_USER_SET, REASON_OTHER}, prefix = { "REASON_" })
+ public @interface SuggestionReason {}
+
+ /**
+ * Indicates that this account is not suggested for use, but is still available.
+ */
+ public static final int REASON_NONE = 0;
+
+ /**
+ * Indicates that the {@link PhoneAccountHandle} is suggested because the number we're calling
+ * is on the same carrier, and therefore may have lower rates.
+ */
+ public static final int REASON_INTRA_CARRIER = 1;
+
+ /**
+ * Indicates that the {@link PhoneAccountHandle} is suggested because the user uses it
+ * frequently for the number that we are calling.
+ */
+ public static final int REASON_FREQUENT = 2;
+
+ /**
+ * Indicates that the {@link PhoneAccountHandle} is suggested because the user explicitly
+ * specified that it be used for the number we are calling.
+ */
+ public static final int REASON_USER_SET = 3;
+
+ /**
+ * Indicates that the {@link PhoneAccountHandle} is suggested for a reason not otherwise
+ * enumerated here.
+ */
+ public static final int REASON_OTHER = 4;
+
+ private PhoneAccountHandle mHandle;
+ private int mReason;
+ private boolean mShouldAutoSelect;
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public PhoneAccountSuggestion(PhoneAccountHandle handle, @SuggestionReason int reason,
+ boolean shouldAutoSelect) {
+ this.mHandle = handle;
+ this.mReason = reason;
+ this.mShouldAutoSelect = shouldAutoSelect;
+ }
+
+ private PhoneAccountSuggestion(Parcel in) {
+ mHandle = in.readParcelable(PhoneAccountHandle.class.getClassLoader());
+ mReason = in.readInt();
+ mShouldAutoSelect = in.readByte() != 0;
+ }
+
+ public static final Creator<PhoneAccountSuggestion> CREATOR =
+ new Creator<PhoneAccountSuggestion>() {
+ @Override
+ public PhoneAccountSuggestion createFromParcel(Parcel in) {
+ return new PhoneAccountSuggestion(in);
+ }
+
+ @Override
+ public PhoneAccountSuggestion[] newArray(int size) {
+ return new PhoneAccountSuggestion[size];
+ }
+ };
+
+ /**
+ * @return The {@link PhoneAccountHandle} for this suggestion.
+ */
+ public PhoneAccountHandle getPhoneAccountHandle() {
+ return mHandle;
+ }
+
+ /**
+ * @return The reason for this suggestion
+ */
+ public @SuggestionReason int getReason() {
+ return mReason;
+ }
+
+ /**
+ * Suggests whether the dialer should automatically place the call using this account without
+ * user interaction. This may be set on multiple {@link PhoneAccountSuggestion}s, and the dialer
+ * is free to choose which one to use.
+ * @return {@code true} if the hint is to auto-select, {@code false} otherwise.
+ */
+ public boolean shouldAutoSelect() {
+ return mShouldAutoSelect;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mHandle, flags);
+ dest.writeInt(mReason);
+ dest.writeByte((byte) (mShouldAutoSelect ? 1 : 0));
+ }
+}
diff --git a/tools/hiddenapi/exclude.sh b/tools/hiddenapi/exclude.sh
index 2291e5a..4ffcf68 100755
--- a/tools/hiddenapi/exclude.sh
+++ b/tools/hiddenapi/exclude.sh
@@ -11,6 +11,7 @@
android.system \
com.android.bouncycastle \
com.android.conscrypt \
+ com.android.i18n.phonenumbers \
com.android.okhttp \
com.sun \
dalvik \
diff --git a/tools/processors/view_inspector/Android.bp b/tools/processors/view_inspector/Android.bp
new file mode 100644
index 0000000..ca6b3c4
--- /dev/null
+++ b/tools/processors/view_inspector/Android.bp
@@ -0,0 +1,27 @@
+java_library_host {
+ name: "view-inspector-annotation-processor",
+
+ srcs: ["src/java/**/*.java"],
+ java_resource_dirs: ["src/resources"],
+
+ static_libs: [
+ "javapoet",
+ ],
+
+ use_tools_jar: true,
+}
+
+java_test_host {
+ name: "view-inspector-annotation-processor-test",
+
+ srcs: ["test/java/**/*.java"],
+ java_resource_dirs: ["test/resources"],
+
+ static_libs: [
+ "guava",
+ "junit",
+ "view-inspector-annotation-processor",
+ ],
+
+ test_suites: ["general-tests"],
+}
diff --git a/tools/processors/view_inspector/TEST_MAPPING b/tools/processors/view_inspector/TEST_MAPPING
new file mode 100644
index 0000000..a91b5b4
--- /dev/null
+++ b/tools/processors/view_inspector/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "view-inspector-annotation-processor-test"
+ }
+ ]
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/AnnotationUtils.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/AnnotationUtils.java
new file mode 100644
index 0000000..f157949
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/AnnotationUtils.java
@@ -0,0 +1,117 @@
+/*
+ * 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.processor.view.inspector;
+
+import java.util.Map;
+import java.util.Optional;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/**
+ * Utilities for working with {@link AnnotationMirror}.
+ */
+final class AnnotationUtils {
+ private final Elements mElementUtils;
+ private final Types mTypeUtils;
+
+ AnnotationUtils(ProcessingEnvironment processingEnv) {
+ mElementUtils = processingEnv.getElementUtils();
+ mTypeUtils = processingEnv.getTypeUtils();
+ }
+
+ /**
+ * Get a {@link AnnotationMirror} specified by name from an {@link Element}.
+ *
+ * @param qualifiedName The fully qualified name of the annotation to search for
+ * @param element The element to search for annotations on
+ * @return The mirror of the requested annotation
+ * @throws ProcessingException If there is not exactly one of the requested annotation.
+ */
+ AnnotationMirror exactlyOneMirror(String qualifiedName, Element element) {
+ final Element targetTypeElment = mElementUtils.getTypeElement(qualifiedName);
+ final TypeMirror targetType = targetTypeElment.asType();
+ AnnotationMirror result = null;
+
+ for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
+ final TypeMirror annotationType = annotation.getAnnotationType().asElement().asType();
+ if (mTypeUtils.isSameType(annotationType, targetType)) {
+ if (result == null) {
+ result = annotation;
+ } else {
+ final String message = String.format(
+ "Element had multiple instances of @%s, expected exactly one",
+ targetTypeElment.getSimpleName());
+
+ throw new ProcessingException(message, element, annotation);
+ }
+ }
+ }
+
+ if (result == null) {
+ final String message = String.format(
+ "Expected an @%s annotation, found none", targetTypeElment.getSimpleName());
+ throw new ProcessingException(message, element);
+ } else {
+ return result;
+ }
+ }
+
+ /**
+ * Extract a string-valued property from an {@link AnnotationMirror}.
+ *
+ * @param propertyName The name of the requested property
+ * @param annotationMirror The mirror to search for the property
+ * @return The String value of the annotation or null
+ */
+ Optional<String> stringProperty(String propertyName, AnnotationMirror annotationMirror) {
+ final AnnotationValue value = valueByName(propertyName, annotationMirror);
+ if (value != null) {
+ return Optional.of((String) value.getValue());
+ } else {
+ return Optional.empty();
+ }
+ }
+
+
+ /**
+ * Extract a {@link AnnotationValue} from a mirror by string property name.
+ *
+ * @param propertyName The name of the property requested property
+ * @param annotationMirror
+ * @return
+ */
+ AnnotationValue valueByName(String propertyName, AnnotationMirror annotationMirror) {
+ final Map<? extends ExecutableElement, ? extends AnnotationValue> valueMap =
+ annotationMirror.getElementValues();
+
+ for (ExecutableElement method : valueMap.keySet()) {
+ if (method.getSimpleName().contentEquals(propertyName)) {
+ return valueMap.get(method);
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableClassModel.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableClassModel.java
new file mode 100644
index 0000000..f0b0ff6
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableClassModel.java
@@ -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.processor.view.inspector;
+
+import com.squareup.javapoet.ClassName;
+
+import java.util.Optional;
+
+/**
+ * Model of an inspectable class derived from annotations.
+ *
+ * This class does not use any {javax.lang.model} objects to facilitate building models for testing
+ * {@link InspectionCompanionGenerator}.
+ */
+public final class InspectableClassModel {
+ private final ClassName mClassName;
+ private Optional<String> mNodeName = Optional.empty();
+
+ /**
+ * @param className The name of the modeled class
+ */
+ public InspectableClassModel(ClassName className) {
+ mClassName = className;
+ }
+
+ public ClassName getClassName() {
+ return mClassName;
+ }
+
+ public Optional<String> getNodeName() {
+ return mNodeName;
+ }
+
+ public void setNodeName(Optional<String> nodeName) {
+ mNodeName = nodeName;
+ }
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableNodeNameProcessor.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableNodeNameProcessor.java
new file mode 100644
index 0000000..a186a82
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableNodeNameProcessor.java
@@ -0,0 +1,76 @@
+/*
+ * 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.processor.view.inspector;
+
+import java.util.Optional;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.Element;
+
+/**
+ * Process {InspectableNodeName} annotations
+ *
+ * @see android.view.inspector.InspectableNodeName
+ */
+public final class InspectableNodeNameProcessor implements ModelProcessor {
+ private final String mQualifiedName;
+ private final ProcessingEnvironment mProcessingEnv;
+ private final AnnotationUtils mAnnotationUtils;
+
+ /**
+ * @param annotationQualifiedName The qualified name of the annotation to process
+ * @param processingEnv The processing environment from the parent processor
+ */
+ public InspectableNodeNameProcessor(
+ String annotationQualifiedName,
+ ProcessingEnvironment processingEnv) {
+ mQualifiedName = annotationQualifiedName;
+ mProcessingEnv = processingEnv;
+ mAnnotationUtils = new AnnotationUtils(processingEnv);
+ }
+
+ /**
+ * Set the node name on the model if one is supplied.
+ *
+ * If the model already has a different node name, the node name will not be updated, and
+ * the processor will print an error the the messager.
+ *
+ * @param element The annotated element to operate on
+ * @param model The model this element should be merged into
+ */
+ @Override
+ public void process(Element element, InspectableClassModel model) {
+ try {
+ final AnnotationMirror mirror =
+ mAnnotationUtils.exactlyOneMirror(mQualifiedName, element);
+ final Optional<String> nodeName = mAnnotationUtils.stringProperty("value", mirror);
+
+ if (!model.getNodeName().isPresent() || model.getNodeName().equals(nodeName)) {
+ model.setNodeName(nodeName);
+ } else {
+ final String message = String.format(
+ "Node name was already set to \"%s\", refusing to change it to \"%s\".",
+ model.getNodeName().get(),
+ nodeName);
+ throw new ProcessingException(message, element, mirror);
+ }
+ } catch (ProcessingException processingException) {
+ processingException.print(mProcessingEnv.getMessager());
+ }
+ }
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectionCompanionGenerator.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectionCompanionGenerator.java
new file mode 100644
index 0000000..fe0153d
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectionCompanionGenerator.java
@@ -0,0 +1,181 @@
+/*
+ * 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.processor.view.inspector;
+
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.JavaFile;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeSpec;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import javax.annotation.processing.Filer;
+import javax.lang.model.element.Modifier;
+
+/**
+ * Generates a source file defining a {@link android.view.inspector.InspectionCompanion}.
+ */
+public final class InspectionCompanionGenerator {
+ private final Filer mFiler;
+ private final Class mRequestingClass;
+
+ /**
+ * @param filer A filer to write the generated source to
+ * @param requestingClass A class object representing the class that invoked the generator
+ */
+ public InspectionCompanionGenerator(final Filer filer, final Class requestingClass) {
+ mFiler = filer;
+ mRequestingClass = requestingClass;
+ }
+
+ /**
+ * Generate and write an inspection companion.
+ *
+ * @param model The model to generated
+ * @throws IOException From the Filer
+ */
+ public void generate(InspectableClassModel model) throws IOException {
+ generateFile(model).writeTo(mFiler);
+ }
+
+ /**
+ * Generate a {@link JavaFile} from a model.
+ *
+ * This is package-public for testing.
+ *
+ * @param model The model to generate from
+ * @return A generated file of an {@link android.view.inspector.InspectionCompanion}
+ */
+ JavaFile generateFile(InspectableClassModel model) {
+ return JavaFile
+ .builder(model.getClassName().packageName(), generateTypeSpec(model))
+ .indent(" ")
+ .build();
+ }
+
+ /**
+ * Generate a {@link TypeSpec} for the {@link android.view.inspector.InspectionCompanion}
+ * for the supplied model.
+ *
+ * @param model The model to generate from
+ * @return A TypeSpec of the inspection companion
+ */
+ private TypeSpec generateTypeSpec(InspectableClassModel model) {
+ TypeSpec.Builder builder = TypeSpec
+ .classBuilder(generateClassName(model))
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addSuperinterface(ParameterizedTypeName.get(
+ ClassName.get("android.view.inspector", "InspectionCompanion"),
+ model.getClassName()))
+ .addJavadoc("Inspection companion for {@link $T}.\n\n", model.getClassName())
+ .addJavadoc("Generated by {@link $T}\n", getClass())
+ .addJavadoc("on behalf of {@link $T}.\n", mRequestingClass)
+ .addMethod(generateMapProperties(model))
+ .addMethod(generateReadProperties(model));
+
+ generateGetNodeName(model).ifPresent(builder::addMethod);
+
+ return builder.build();
+ }
+
+ /**
+ * Generate a method definition for
+ * {@link android.view.inspector.InspectionCompanion#getNodeName()}, if needed.
+ *
+ * If {@link InspectableClassModel#getNodeName()} is empty, This method returns an empty
+ * optional, otherwise, it generates a simple method that returns the string value of the
+ * node name.
+ *
+ * @param model The model to generate from
+ * @return The method definition or an empty Optional
+ */
+ private Optional<MethodSpec> generateGetNodeName(InspectableClassModel model) {
+ return model.getNodeName().map(nodeName -> MethodSpec.methodBuilder("getNodeName")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(String.class)
+ .addStatement("return $S", nodeName)
+ .build());
+ }
+
+ /**
+ * Generate a method definition for
+ * {@link android.view.inspector.InspectionCompanion#mapProperties(
+ * android.view.inspector.PropertyMapper)}.
+ *
+ * TODO: implement
+ *
+ * @param model The model to generate from
+ * @return The method definition
+ */
+ private MethodSpec generateMapProperties(InspectableClassModel model) {
+ final ClassName propertyMapper = ClassName.get(
+ "android.view.inspector", "PropertyMapper");
+
+ return MethodSpec.methodBuilder("mapProperties")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(propertyMapper, "propertyMapper")
+ // TODO: add method body
+ .build();
+ }
+
+ /**
+ * Generate a method definition for
+ * {@link android.view.inspector.InspectionCompanion#readProperties(
+ * Object, android.view.inspector.PropertyReader)}.
+ *
+ * TODO: implement
+ *
+ * @param model The model to generate from
+ * @return The method definition
+ */
+ private MethodSpec generateReadProperties(InspectableClassModel model) {
+ final ClassName propertyReader = ClassName.get(
+ "android.view.inspector", "PropertyReader");
+
+ return MethodSpec.methodBuilder("readProperties")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(model.getClassName(), "inspectable")
+ .addParameter(propertyReader, "propertyReader")
+ // TODO: add method body
+ .build();
+ }
+
+ /**
+ * Generate the final class name for the inspection companion from the model's class name.
+ *
+ * The generated class is added to the same package as the source class. If the class in the
+ * model is a nested class, the nested class names are joined with {"$"}. The suffix
+ * {"$$InspectionCompanion"} is always added the the generated name. E.g.: For modeled class
+ * {com.example.Outer.Inner}, the generated class name will be
+ * {com.example.Outer$Inner$$InspectionCompanion}.
+ *
+ * @param model The model to generate from
+ * @return A class name for the generated inspection companion class
+ */
+ private ClassName generateClassName(final InspectableClassModel model) {
+ final ClassName className = model.getClassName();
+
+ return ClassName.get(
+ className.packageName(),
+ String.join("$", className.simpleNames()) + "$$InspectionCompanion");
+ }
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/ModelProcessor.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/ModelProcessor.java
new file mode 100644
index 0000000..3ffcff8
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/ModelProcessor.java
@@ -0,0 +1,32 @@
+/*
+ * 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.processor.view.inspector;
+
+import javax.lang.model.element.Element;
+
+/**
+ * An interface for annotation processors that operate on a single element and a class model.
+ */
+public interface ModelProcessor {
+ /**
+ * Process the supplied element, mutating the model as needed.
+ *
+ * @param element The annotated element to operate on
+ * @param model The model this element should be merged into
+ */
+ void process(Element element, InspectableClassModel model);
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/PlatformInspectableProcessor.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/PlatformInspectableProcessor.java
new file mode 100644
index 0000000..e531b67
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/PlatformInspectableProcessor.java
@@ -0,0 +1,161 @@
+/*
+ * 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.processor.view.inspector;
+
+import static javax.tools.Diagnostic.Kind.ERROR;
+
+import com.squareup.javapoet.ClassName;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.TypeElement;
+
+
+/**
+ * An annotation processor for the platform inspectable annotations.
+ *
+ * It mostly delegates to {@link ModelProcessor} and {@link InspectionCompanionGenerator}. This
+ * modular architecture allows the core generation code to be reused for comparable annotations
+ * outside the platform, such as in AndroidX.
+ *
+ * @see android.view.inspector.InspectableNodeName
+ * @see android.view.inspector.InspectableProperty
+ */
+@SupportedAnnotationTypes({
+ PlatformInspectableProcessor.NODE_NAME_QUALIFIED_NAME
+})
+public final class PlatformInspectableProcessor extends AbstractProcessor {
+ static final String NODE_NAME_QUALIFIED_NAME =
+ "android.view.inspector.InspectableNodeName";
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latest();
+ }
+
+ @Override
+ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ final Map<String, InspectableClassModel> modelMap = new HashMap<>();
+
+ for (TypeElement annotation : annotations) {
+ if (annotation.getQualifiedName().contentEquals(NODE_NAME_QUALIFIED_NAME)) {
+ runModelProcessor(
+ roundEnv.getElementsAnnotatedWith(annotation),
+ new InspectableNodeNameProcessor(NODE_NAME_QUALIFIED_NAME, processingEnv),
+ modelMap);
+
+
+ } else {
+ fail("Unexpected annotation type", annotation);
+ }
+ }
+
+ final InspectionCompanionGenerator generator =
+ new InspectionCompanionGenerator(processingEnv.getFiler(), getClass());
+
+ for (InspectableClassModel model : modelMap.values()) {
+ try {
+ generator.generate(model);
+ } catch (IOException ioException) {
+ fail(String.format(
+ "Unable to generate inspection companion for %s due to %s",
+ model.getClassName().toString(),
+ ioException.getMessage()));
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Run a {@link ModelProcessor} for a set of elements
+ *
+ * @param elements Elements to process, should be annotated correctly
+ * @param processor The processor to use
+ * @param modelMap A map of qualified class names to models
+ */
+ private void runModelProcessor(
+ Set<? extends Element> elements,
+ ModelProcessor processor,
+ Map<String, InspectableClassModel> modelMap) {
+ for (Element element : elements) {
+ final Optional<TypeElement> classElement = enclosingClassElement(element);
+
+ if (!classElement.isPresent()) {
+ fail("Element not contained in a class", element);
+ break;
+ }
+
+ final InspectableClassModel model = modelMap.computeIfAbsent(
+ classElement.get().getQualifiedName().toString(),
+ k -> new InspectableClassModel(ClassName.get(classElement.get())));
+
+ processor.process(element, model);
+ }
+ }
+
+ /**
+ * Get the nearest enclosing class if there is one.
+ *
+ * If {@param element} represents a class, it will be returned wrapped in an optional.
+ *
+ * @param element An element to search from
+ * @return A TypeElement of the nearest enclosing class or an empty optional
+ */
+ private static Optional<TypeElement> enclosingClassElement(Element element) {
+ Element cursor = element;
+
+ while (cursor != null) {
+ if (cursor.getKind() == ElementKind.CLASS) {
+ return Optional.of((TypeElement) cursor);
+ }
+
+ cursor = cursor.getEnclosingElement();
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * Print message and fail the build.
+ *
+ * @param message Message to print
+ */
+ private void fail(String message) {
+ processingEnv.getMessager().printMessage(ERROR, message);
+ }
+
+ /**
+ * Print message and fail the build.
+ *
+ * @param message Message to print
+ * @param element The element that failed
+ */
+ private void fail(String message, Element element) {
+ processingEnv.getMessager().printMessage(ERROR, message, element);
+ }
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/ProcessingException.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/ProcessingException.java
new file mode 100644
index 0000000..6360e0a
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/ProcessingException.java
@@ -0,0 +1,78 @@
+/*
+ * 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.processor.view.inspector;
+
+import static javax.tools.Diagnostic.Kind.ERROR;
+
+import javax.annotation.processing.Messager;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+
+/**
+ * Internal exception used to signal an error processing an annotation.
+ */
+final class ProcessingException extends RuntimeException {
+ private final Element mElement;
+ private final AnnotationMirror mAnnotationMirror;
+ private final AnnotationValue mAnnotationValue;
+
+ ProcessingException(String message) {
+ this(message, null, null, null);
+ }
+
+ ProcessingException(String message, Element element) {
+ this(message, element, null, null);
+ }
+
+ ProcessingException(String message, Element element, AnnotationMirror annotationMirror) {
+ this(message, element, annotationMirror, null);
+ }
+
+ ProcessingException(
+ String message,
+ Element element,
+ AnnotationMirror annotationMirror,
+ AnnotationValue annotationValue) {
+ super(message);
+ mElement = element;
+ mAnnotationMirror = annotationMirror;
+ mAnnotationValue = annotationValue;
+ }
+
+ /**
+ * Prints the exception to a Messager.
+ *
+ * @param messager A Messager to print to
+ */
+ void print(Messager messager) {
+ if (mElement != null) {
+ if (mAnnotationMirror != null) {
+ if (mAnnotationValue != null) {
+ messager.printMessage(
+ ERROR, getMessage(), mElement, mAnnotationMirror, mAnnotationValue);
+ } else {
+ messager.printMessage(ERROR, getMessage(), mElement, mAnnotationMirror);
+ }
+ } else {
+ messager.printMessage(ERROR, getMessage(), mElement);
+ }
+ } else {
+ messager.printMessage(ERROR, getMessage());
+ }
+ }
+}
diff --git a/tools/processors/view_inspector/src/resources/META-INF/services/javax.annotation.processing.Processor b/tools/processors/view_inspector/src/resources/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 0000000..fa4f71f
--- /dev/null
+++ b/tools/processors/view_inspector/src/resources/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+android.processor.inspector.view.PlatformInspectableProcessor
diff --git a/tools/processors/view_inspector/test/java/android/processor/view/inspector/InspectionCompanionGeneratorTest.java b/tools/processors/view_inspector/test/java/android/processor/view/inspector/InspectionCompanionGeneratorTest.java
new file mode 100644
index 0000000..c02b0bd
--- /dev/null
+++ b/tools/processors/view_inspector/test/java/android/processor/view/inspector/InspectionCompanionGeneratorTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.processor.view.inspector;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.TestCase.fail;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Resources;
+import com.squareup.javapoet.ClassName;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Optional;
+
+/**
+ * Tests for {@link InspectionCompanionGenerator}
+ */
+public class InspectionCompanionGeneratorTest {
+ private static final String RESOURCE_PATH_TEMPLATE =
+ "android/processor/view/inspector/InspectionCompanionGeneratorTest/%s.java.txt";
+ private static final ClassName TEST_CLASS_NAME =
+ ClassName.get("com.android.inspectable", "TestInspectable");
+ private InspectableClassModel mModel;
+ private InspectionCompanionGenerator mGenerator;
+
+ @Before
+ public void setup() {
+ mModel = new InspectableClassModel(TEST_CLASS_NAME);
+ mGenerator = new InspectionCompanionGenerator(null, getClass());
+ }
+
+ @Test
+ public void testNodeName() {
+ mModel.setNodeName(Optional.of("NodeName"));
+ assertGeneratedFileEquals("NodeName");
+ }
+
+ @Test
+ public void testNestedClass() {
+ mModel = new InspectableClassModel(
+ ClassName.get("com.android.inspectable", "Outer", "Inner"));
+ assertGeneratedFileEquals("NestedClass");
+ }
+
+ private void assertGeneratedFileEquals(String fileName) {
+ assertEquals(
+ loadTextResource(String.format(RESOURCE_PATH_TEMPLATE, fileName)),
+ mGenerator.generateFile(mModel).toString());
+ }
+
+ private String loadTextResource(String path) {
+ try {
+ final URL url = Resources.getResource(path);
+ assertNotNull(String.format("Resource file not found: %s", path), url);
+ return Resources.toString(url, Charsets.UTF_8);
+ } catch (IOException e) {
+ fail(e.getMessage());
+ return null;
+ }
+ }
+}
diff --git a/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NestedClass.java.txt b/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NestedClass.java.txt
new file mode 100644
index 0000000..e5fb6a2
--- /dev/null
+++ b/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NestedClass.java.txt
@@ -0,0 +1,22 @@
+package com.android.inspectable;
+
+import android.view.inspector.InspectionCompanion;
+import android.view.inspector.PropertyMapper;
+import android.view.inspector.PropertyReader;
+import java.lang.Override;
+
+/**
+ * Inspection companion for {@link Outer.Inner}.
+ *
+ * Generated by {@link android.processor.view.inspector.InspectionCompanionGenerator}
+ * on behalf of {@link android.processor.view.inspector.InspectionCompanionGeneratorTest}.
+ */
+public final class Outer$Inner$$InspectionCompanion implements InspectionCompanion<Outer.Inner> {
+ @Override
+ public void mapProperties(PropertyMapper propertyMapper) {
+ }
+
+ @Override
+ public void readProperties(Outer.Inner inspectable, PropertyReader propertyReader) {
+ }
+}
diff --git a/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NodeName.java.txt b/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NodeName.java.txt
new file mode 100644
index 0000000..a334f50
--- /dev/null
+++ b/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NodeName.java.txt
@@ -0,0 +1,28 @@
+package com.android.inspectable;
+
+import android.view.inspector.InspectionCompanion;
+import android.view.inspector.PropertyMapper;
+import android.view.inspector.PropertyReader;
+import java.lang.Override;
+import java.lang.String;
+
+/**
+ * Inspection companion for {@link TestInspectable}.
+ *
+ * Generated by {@link android.processor.view.inspector.InspectionCompanionGenerator}
+ * on behalf of {@link android.processor.view.inspector.InspectionCompanionGeneratorTest}.
+ */
+public final class TestInspectable$$InspectionCompanion implements InspectionCompanion<TestInspectable> {
+ @Override
+ public void mapProperties(PropertyMapper propertyMapper) {
+ }
+
+ @Override
+ public void readProperties(TestInspectable inspectable, PropertyReader propertyReader) {
+ }
+
+ @Override
+ public String getNodeName() {
+ return "NodeName";
+ }
+}