Merge "Revoke 'always allow' adb grants after period of inactivity"
diff --git a/api/current.txt b/api/current.txt
index 9f39f3a..872ec8b 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -3941,7 +3941,8 @@
method public boolean isBackgroundRestricted();
method @Deprecated public boolean isInLockTaskMode();
method public boolean isLowRamDevice();
- method public static boolean isRunningInTestHarness();
+ method @Deprecated public static boolean isRunningInTestHarness();
+ method public static boolean isRunningInUserTestHarness();
method public static boolean isUserAMonkey();
method @RequiresPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES) public void killBackgroundProcesses(String);
method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int);
@@ -6822,6 +6823,7 @@
field public static final String EXTRA_ADD_EXPLANATION = "android.app.extra.ADD_EXPLANATION";
field public static final String EXTRA_DELEGATION_SCOPES = "android.app.extra.DELEGATION_SCOPES";
field public static final String EXTRA_DEVICE_ADMIN = "android.app.extra.DEVICE_ADMIN";
+ field @RequiresPermission(android.Manifest.permission.GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY) public static final String EXTRA_PASSWORD_COMPLEXITY = "android.app.extra.PASSWORD_COMPLEXITY";
field public static final String EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE = "android.app.extra.PROVISIONING_ACCOUNT_TO_MIGRATE";
field public static final String EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE = "android.app.extra.PROVISIONING_ADMIN_EXTRAS_BUNDLE";
field public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME = "android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME";
@@ -10346,6 +10348,7 @@
field public static final int EXTRA_DOCK_STATE_LE_DESK = 3; // 0x3
field public static final int EXTRA_DOCK_STATE_UNDOCKED = 0; // 0x0
field public static final String EXTRA_DONT_KILL_APP = "android.intent.extra.DONT_KILL_APP";
+ field public static final String EXTRA_DURATION_MILLIS = "android.intent.extra.DURATION_MILLIS";
field public static final String EXTRA_EMAIL = "android.intent.extra.EMAIL";
field public static final String EXTRA_EXCLUDE_COMPONENTS = "android.intent.extra.EXCLUDE_COMPONENTS";
field public static final String EXTRA_FROM_STORAGE = "android.intent.extra.FROM_STORAGE";
@@ -11497,7 +11500,7 @@
method public abstract java.util.List<android.content.pm.PermissionGroupInfo> getAllPermissionGroups(int);
method public abstract android.graphics.drawable.Drawable getApplicationBanner(android.content.pm.ApplicationInfo);
method public abstract android.graphics.drawable.Drawable getApplicationBanner(String) throws android.content.pm.PackageManager.NameNotFoundException;
- method public abstract int getApplicationEnabledSetting(String);
+ method public abstract int getApplicationEnabledSetting(@NonNull String);
method public abstract android.graphics.drawable.Drawable getApplicationIcon(android.content.pm.ApplicationInfo);
method public abstract android.graphics.drawable.Drawable getApplicationIcon(String) throws android.content.pm.PackageManager.NameNotFoundException;
method public abstract android.content.pm.ApplicationInfo getApplicationInfo(String, int) throws android.content.pm.PackageManager.NameNotFoundException;
@@ -11505,7 +11508,7 @@
method public abstract android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
method public abstract android.graphics.drawable.Drawable getApplicationLogo(String) throws android.content.pm.PackageManager.NameNotFoundException;
method @Nullable public abstract android.content.pm.ChangedPackages getChangedPackages(@IntRange(from=0) int);
- method public abstract int getComponentEnabledSetting(android.content.ComponentName);
+ method public abstract int getComponentEnabledSetting(@NonNull android.content.ComponentName);
method public abstract android.graphics.drawable.Drawable getDefaultActivityIcon();
method public abstract android.graphics.drawable.Drawable getDrawable(String, @DrawableRes int, android.content.pm.ApplicationInfo);
method public abstract java.util.List<android.content.pm.ApplicationInfo> getInstalledApplications(int);
@@ -11571,8 +11574,8 @@
method public abstract android.content.pm.ProviderInfo resolveContentProvider(String, int);
method public abstract android.content.pm.ResolveInfo resolveService(android.content.Intent, int);
method public abstract void setApplicationCategoryHint(@NonNull String, int);
- method @RequiresPermission(value=android.Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE, conditional=true) public abstract void setApplicationEnabledSetting(String, int, int);
- method @RequiresPermission(value=android.Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE, conditional=true) public abstract void setComponentEnabledSetting(android.content.ComponentName, int, int);
+ method @RequiresPermission(value=android.Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE, conditional=true) public abstract void setApplicationEnabledSetting(@NonNull String, int, int);
+ method @RequiresPermission(value=android.Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE, conditional=true) public abstract void setComponentEnabledSetting(@NonNull android.content.ComponentName, int, int);
method public abstract void setInstallerPackageName(String, String);
method public abstract void updateInstantAppCookie(@Nullable byte[]);
method public abstract void verifyPendingInstall(int, int);
@@ -11652,6 +11655,7 @@
field public static final String FEATURE_SCREEN_LANDSCAPE = "android.hardware.screen.landscape";
field public static final String FEATURE_SCREEN_PORTRAIT = "android.hardware.screen.portrait";
field public static final String FEATURE_SECURELY_REMOVES_USERS = "android.software.securely_removes_users";
+ field public static final String FEATURE_SECURE_LOCK_SCREEN = "android.software.secure_lock_screen";
field public static final String FEATURE_SENSOR_ACCELEROMETER = "android.hardware.sensor.accelerometer";
field public static final String FEATURE_SENSOR_AMBIENT_TEMPERATURE = "android.hardware.sensor.ambient_temperature";
field public static final String FEATURE_SENSOR_BAROMETER = "android.hardware.sensor.barometer";
@@ -22253,7 +22257,7 @@
method public void onUpdateExtractingViews(android.view.inputmethod.EditorInfo);
method public void onUpdateExtractingVisibility(android.view.inputmethod.EditorInfo);
method public void onUpdateSelection(int, int, int, int, int, int);
- method public void onViewClicked(boolean);
+ method @Deprecated public void onViewClicked(boolean);
method public void onWindowHidden();
method public void onWindowShown();
method public void requestHideSelf(int);
@@ -25912,6 +25916,23 @@
method @Nullable public android.media.Session2Command.Result onSessionCommand(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo, @NonNull android.media.Session2Command, @Nullable android.os.Bundle);
}
+ public abstract class MediaSession2Service extends android.app.Service {
+ ctor public MediaSession2Service();
+ method public final void addSession(@NonNull android.media.MediaSession2);
+ method @NonNull public final java.util.List<android.media.MediaSession2> getSessions();
+ method @CallSuper @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent);
+ method @NonNull public abstract android.media.MediaSession2 onGetPrimarySession();
+ method @Nullable public abstract android.media.MediaSession2Service.MediaNotification onUpdateNotification(@NonNull android.media.MediaSession2);
+ method public final void removeSession(@NonNull android.media.MediaSession2);
+ field public static final String SERVICE_INTERFACE = "android.media.MediaSession2Service";
+ }
+
+ public static class MediaSession2Service.MediaNotification {
+ ctor public MediaSession2Service.MediaNotification(int, @NonNull android.app.Notification);
+ method @NonNull public android.app.Notification getNotification();
+ method public int getNotificationId();
+ }
+
public final class MediaSync {
ctor public MediaSync();
method @NonNull public android.view.Surface createInputSurface();
@@ -26574,12 +26595,9 @@
field public static final int SUCCESS = 0; // 0x0
}
- public static final class AudioEffect.Descriptor implements android.os.Parcelable {
+ public static class AudioEffect.Descriptor {
ctor public AudioEffect.Descriptor();
ctor public AudioEffect.Descriptor(String, String, String, String, String);
- method public int describeContents();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<android.media.audiofx.AudioEffect.Descriptor> CREATOR;
field public String connectMode;
field public String implementor;
field public String name;
@@ -27443,7 +27461,6 @@
public static final class MediaSessionManager.RemoteUserInfo {
ctor public MediaSessionManager.RemoteUserInfo(@NonNull String, int, int);
- ctor public MediaSessionManager.RemoteUserInfo(String, int, int, android.os.IBinder);
method public String getPackageName();
method public int getPid();
method public int getUid();
@@ -28561,6 +28578,28 @@
field public int serverAddress;
}
+ public final class DnsResolver {
+ method public static android.net.DnsResolver getInstance();
+ method public void query(@Nullable android.net.Network, @NonNull byte[], int, @NonNull android.os.Handler, @NonNull android.net.DnsResolver.RawAnswerListener) throws android.system.ErrnoException;
+ method public void query(@Nullable android.net.Network, @NonNull String, int, int, int, @NonNull android.os.Handler, @NonNull android.net.DnsResolver.RawAnswerListener) throws android.system.ErrnoException;
+ method public void query(@Nullable android.net.Network, @NonNull String, int, @NonNull android.os.Handler, @NonNull android.net.DnsResolver.InetAddressAnswerListener) throws android.system.ErrnoException;
+ field public static final int CLASS_IN = 1; // 0x1
+ field public static final int FLAG_EMPTY = 0; // 0x0
+ field public static final int FLAG_NO_CACHE_LOOKUP = 4; // 0x4
+ field public static final int FLAG_NO_CACHE_STORE = 2; // 0x2
+ field public static final int FLAG_NO_RETRY = 1; // 0x1
+ field public static final int TYPE_A = 1; // 0x1
+ field public static final int TYPE_AAAA = 28; // 0x1c
+ }
+
+ public static interface DnsResolver.InetAddressAnswerListener {
+ method public void onAnswer(@NonNull java.util.List<java.net.InetAddress>);
+ }
+
+ public static interface DnsResolver.RawAnswerListener {
+ method public void onAnswer(@Nullable byte[]);
+ }
+
public class InetAddresses {
method public static boolean isNumericAddress(String);
method public static java.net.InetAddress parseNumericAddress(String);
@@ -43596,6 +43635,7 @@
method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public java.util.List<android.telecom.PhoneAccountHandle> getSelfManagedPhoneAccounts();
method public android.telecom.PhoneAccountHandle getSimCallManager();
method public String getSystemDialerPackage();
+ method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.telecom.PhoneAccountHandle getUserSelectedOutgoingPhoneAccount();
method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getVoiceMailNumber(android.telecom.PhoneAccountHandle);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean handleMmi(String);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean handleMmi(String, android.telecom.PhoneAccountHandle);
@@ -49674,6 +49714,7 @@
}
public class Surface implements android.os.Parcelable {
+ ctor public Surface(android.view.SurfaceControl);
ctor public Surface(android.graphics.SurfaceTexture);
method public int describeContents();
method public boolean isValid();
@@ -49696,6 +49737,38 @@
ctor public Surface.OutOfResourcesException(String);
}
+ public final class SurfaceControl implements android.os.Parcelable {
+ method public int describeContents();
+ method public boolean isValid();
+ method public void readFromParcel(android.os.Parcel);
+ method public void release();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.view.SurfaceControl> CREATOR;
+ }
+
+ public static class SurfaceControl.Builder {
+ ctor public SurfaceControl.Builder();
+ method public android.view.SurfaceControl build();
+ method public android.view.SurfaceControl.Builder setBufferSize(@IntRange(from=0) int, @IntRange(from=0) int);
+ method @NonNull public android.view.SurfaceControl.Builder setFormat(int);
+ method public android.view.SurfaceControl.Builder setName(String);
+ method @NonNull public android.view.SurfaceControl.Builder setOpaque(boolean);
+ method @NonNull public android.view.SurfaceControl.Builder setParent(@Nullable android.view.SurfaceControl);
+ }
+
+ public static class SurfaceControl.Transaction implements java.io.Closeable {
+ ctor public SurfaceControl.Transaction();
+ method public void apply();
+ method public void close();
+ method @NonNull public android.view.SurfaceControl.Transaction merge(@NonNull android.view.SurfaceControl.Transaction);
+ method @NonNull public android.view.SurfaceControl.Transaction reparent(@NonNull android.view.SurfaceControl, @Nullable android.view.SurfaceControl);
+ method @NonNull public android.view.SurfaceControl.Transaction setAlpha(@NonNull android.view.SurfaceControl, @FloatRange(from=0.0, to=1.0) float);
+ method @NonNull public android.view.SurfaceControl.Transaction setBufferSize(@NonNull android.view.SurfaceControl, @IntRange(from=0) int, @IntRange(from=0) int);
+ method @NonNull public android.view.SurfaceControl.Transaction setGeometry(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect, @Nullable android.graphics.Rect, int);
+ method @NonNull public android.view.SurfaceControl.Transaction setLayer(@NonNull android.view.SurfaceControl, @IntRange(from=java.lang.Integer.MIN_VALUE, to=java.lang.Integer.MAX_VALUE) int);
+ method @NonNull public android.view.SurfaceControl.Transaction setVisibility(@NonNull android.view.SurfaceControl, boolean);
+ }
+
public interface SurfaceHolder {
method public void addCallback(android.view.SurfaceHolder.Callback);
method public android.view.Surface getSurface();
@@ -49740,6 +49813,7 @@
ctor public SurfaceView(android.content.Context, android.util.AttributeSet, int, int);
method public boolean gatherTransparentRegion(android.graphics.Region);
method public android.view.SurfaceHolder getHolder();
+ method public android.view.SurfaceControl getSurfaceControl();
method public void setSecure(boolean);
method public void setZOrderMediaOverlay(boolean);
method public void setZOrderOnTop(boolean);
@@ -52924,7 +52998,7 @@
method public void updateCursorAnchorInfo(android.view.View, android.view.inputmethod.CursorAnchorInfo);
method public void updateExtractedText(android.view.View, int, android.view.inputmethod.ExtractedText);
method public void updateSelection(android.view.View, int, int, int, int);
- method public void viewClicked(android.view.View);
+ method @Deprecated public void viewClicked(android.view.View);
field public static final int HIDE_IMPLICIT_ONLY = 1; // 0x1
field public static final int HIDE_NOT_ALWAYS = 2; // 0x2
field public static final int RESULT_HIDDEN = 3; // 0x3
diff --git a/api/system-current.txt b/api/system-current.txt
index 2088c14..0d7465c 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -1136,19 +1136,46 @@
method @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean enableNoAutoConnect();
method public boolean isBleScanAlwaysAvailable();
method public boolean isLeEnabled();
+ method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean registerMetadataListener(android.bluetooth.BluetoothDevice, android.bluetooth.BluetoothAdapter.MetadataListener, android.os.Handler);
+ method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean unregisterMetadataListener(android.bluetooth.BluetoothDevice);
field public static final String ACTION_BLE_STATE_CHANGED = "android.bluetooth.adapter.action.BLE_STATE_CHANGED";
field public static final String ACTION_REQUEST_BLE_SCAN_ALWAYS_AVAILABLE = "android.bluetooth.adapter.action.REQUEST_BLE_SCAN_ALWAYS_AVAILABLE";
}
+ public abstract class BluetoothAdapter.MetadataListener {
+ ctor public BluetoothAdapter.MetadataListener();
+ method public void onMetadataChanged(android.bluetooth.BluetoothDevice, int, String);
+ }
+
public final class BluetoothDevice implements android.os.Parcelable {
method @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean cancelBondProcess();
+ method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public String getMetadata(int);
method @RequiresPermission(android.Manifest.permission.BLUETOOTH) public boolean isConnected();
method @RequiresPermission(android.Manifest.permission.BLUETOOTH) public boolean isEncrypted();
method @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean removeBond();
+ method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean setMetadata(int, String);
method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean setPhonebookAccessPermission(int);
field public static final int ACCESS_ALLOWED = 1; // 0x1
field public static final int ACCESS_REJECTED = 2; // 0x2
field public static final int ACCESS_UNKNOWN = 0; // 0x0
+ field public static final int METADATA_COMPANION_APP = 4; // 0x4
+ field public static final int METADATA_ENHANCED_SETTINGS_UI_URI = 16; // 0x10
+ field public static final int METADATA_HARDWARE_VERSION = 3; // 0x3
+ field public static final int METADATA_IS_UNTHETHERED_HEADSET = 6; // 0x6
+ field public static final int METADATA_MAIN_ICON = 5; // 0x5
+ field public static final int METADATA_MANUFACTURER_NAME = 0; // 0x0
+ field public static final int METADATA_MAX_LENGTH = 2048; // 0x800
+ field public static final int METADATA_MODEL_NAME = 1; // 0x1
+ field public static final int METADATA_SOFTWARE_VERSION = 2; // 0x2
+ field public static final int METADATA_UNTHETHERED_CASE_BATTERY = 12; // 0xc
+ field public static final int METADATA_UNTHETHERED_CASE_CHARGING = 15; // 0xf
+ field public static final int METADATA_UNTHETHERED_CASE_ICON = 9; // 0x9
+ field public static final int METADATA_UNTHETHERED_LEFT_BATTERY = 10; // 0xa
+ field public static final int METADATA_UNTHETHERED_LEFT_CHARGING = 13; // 0xd
+ field public static final int METADATA_UNTHETHERED_LEFT_ICON = 7; // 0x7
+ field public static final int METADATA_UNTHETHERED_RIGHT_BATTERY = 11; // 0xb
+ field public static final int METADATA_UNTHETHERED_RIGHT_CHARGING = 14; // 0xe
+ field public static final int METADATA_UNTHETHERED_RIGHT_ICON = 8; // 0x8
}
public final class BluetoothHeadset implements android.bluetooth.BluetoothProfile {
@@ -1645,22 +1672,17 @@
package android.content.rollback {
public final class PackageRollbackInfo implements android.os.Parcelable {
- ctor public PackageRollbackInfo(String, android.content.rollback.PackageRollbackInfo.PackageVersion, android.content.rollback.PackageRollbackInfo.PackageVersion);
method public int describeContents();
+ method public String getPackageName();
+ method public android.content.pm.VersionedPackage getVersionRolledBackFrom();
+ method public android.content.pm.VersionedPackage getVersionRolledBackTo();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.content.rollback.PackageRollbackInfo> CREATOR;
- field public final android.content.rollback.PackageRollbackInfo.PackageVersion higherVersion;
- field public final android.content.rollback.PackageRollbackInfo.PackageVersion lowerVersion;
- field public final String packageName;
- }
-
- public static class PackageRollbackInfo.PackageVersion {
- ctor public PackageRollbackInfo.PackageVersion(long);
- field public final long versionCode;
}
public final class RollbackInfo implements android.os.Parcelable {
method public int describeContents();
+ method public int getRollbackId();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.content.rollback.RollbackInfo> CREATOR;
field public final android.content.rollback.PackageRollbackInfo targetPackage;
@@ -5529,6 +5551,12 @@
method public void onPropertyChanged(String, String, String);
}
+ public static interface DeviceConfig.Telephony {
+ field public static final String NAMESPACE = "telephony";
+ field public static final String PROPERTY_ENABLE_RAMPING_RINGER = "enable_ramping_ringer";
+ field public static final String PROPERTY_RAMPING_RINGER_DURATION = "ramping_duration";
+ }
+
public final class DocumentsContract {
method public static boolean isManageMode(android.net.Uri);
method public static android.net.Uri setManageMode(android.net.Uri);
@@ -6164,12 +6192,15 @@
method public abstract int onSwitchToSubscription(int, @Nullable String, boolean);
method public abstract int onUpdateSubscriptionNickname(int, String, String);
field public static final String ACTION_BIND_CARRIER_PROVISIONING_SERVICE = "android.service.euicc.action.BIND_CARRIER_PROVISIONING_SERVICE";
+ field public static final String ACTION_DELETE_SUBSCRIPTION_PRIVILEGED = "android.service.euicc.action.DELETE_SUBSCRIPTION_PRIVILEGED";
field public static final String ACTION_MANAGE_EMBEDDED_SUBSCRIPTIONS = "android.service.euicc.action.MANAGE_EMBEDDED_SUBSCRIPTIONS";
field public static final String ACTION_PROVISION_EMBEDDED_SUBSCRIPTION = "android.service.euicc.action.PROVISION_EMBEDDED_SUBSCRIPTION";
+ field public static final String ACTION_RENAME_SUBSCRIPTION_PRIVILEGED = "android.service.euicc.action.RENAME_SUBSCRIPTION_PRIVILEGED";
field @Deprecated public static final String ACTION_RESOLVE_CONFIRMATION_CODE = "android.service.euicc.action.RESOLVE_CONFIRMATION_CODE";
field public static final String ACTION_RESOLVE_DEACTIVATE_SIM = "android.service.euicc.action.RESOLVE_DEACTIVATE_SIM";
field public static final String ACTION_RESOLVE_NO_PRIVILEGES = "android.service.euicc.action.RESOLVE_NO_PRIVILEGES";
field public static final String ACTION_RESOLVE_RESOLVABLE_ERRORS = "android.service.euicc.action.RESOLVE_RESOLVABLE_ERRORS";
+ field public static final String ACTION_TOGGLE_SUBSCRIPTION_PRIVILEGED = "android.service.euicc.action.TOGGLE_SUBSCRIPTION_PRIVILEGED";
field public static final String CATEGORY_EUICC_UI = "android.service.euicc.category.EUICC_UI";
field public static final String EUICC_SERVICE_INTERFACE = "android.service.euicc.EuiccService";
field public static final String EXTRA_RESOLUTION_ALLOW_POLICY_RULES = "android.service.euicc.extra.RESOLUTION_ALLOW_POLICY_RULES";
@@ -6747,6 +6778,7 @@
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean isInEmergencyCall();
method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public boolean isRinging();
method @Deprecated @RequiresPermission(allOf={android.Manifest.permission.MODIFY_PHONE_STATE, android.Manifest.permission.WRITE_SECURE_SETTINGS}) public boolean setDefaultDialer(String);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUserSelectedOutgoingPhoneAccount(android.telecom.PhoneAccountHandle);
field public static final String EXTRA_CALL_BACK_INTENT = "android.telecom.extra.CALL_BACK_INTENT";
field public static final String EXTRA_CLEAR_MISSED_CALLS_INTENT = "android.telecom.extra.CLEAR_MISSED_CALLS_INTENT";
field public static final String EXTRA_CONNECTION_SERVICE = "android.telecom.extra.CONNECTION_SERVICE";
@@ -7912,9 +7944,12 @@
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void getDefaultDownloadableSubscriptionList(android.app.PendingIntent);
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void getDownloadableSubscriptionMetadata(android.telephony.euicc.DownloadableSubscription, android.app.PendingIntent);
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public int getOtaStatus();
+ field public static final String ACTION_DELETE_SUBSCRIPTION_PRIVILEGED = "android.telephony.euicc.action.DELETE_SUBSCRIPTION_PRIVILEGED";
field @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public static final String ACTION_OTA_STATUS_CHANGED = "android.telephony.euicc.action.OTA_STATUS_CHANGED";
field public static final String ACTION_PROFILE_SELECTION = "android.telephony.euicc.action.PROFILE_SELECTION";
field public static final String ACTION_PROVISION_EMBEDDED_SUBSCRIPTION = "android.telephony.euicc.action.PROVISION_EMBEDDED_SUBSCRIPTION";
+ field public static final String ACTION_RENAME_SUBSCRIPTION_PRIVILEGED = "android.telephony.euicc.action.RENAME_SUBSCRIPTION_PRIVILEGED";
+ field public static final String ACTION_TOGGLE_SUBSCRIPTION_PRIVILEGED = "android.telephony.euicc.action.TOGGLE_SUBSCRIPTION_PRIVILEGED";
field public static final int EUICC_ACTIVATION_TYPE_BACKUP = 2; // 0x2
field public static final int EUICC_ACTIVATION_TYPE_DEFAULT = 1; // 0x1
field public static final int EUICC_ACTIVATION_TYPE_TRANSFER = 3; // 0x3
@@ -7925,7 +7960,10 @@
field public static final int EUICC_OTA_SUCCEEDED = 3; // 0x3
field public static final String EXTRA_ACTIVATION_TYPE = "android.telephony.euicc.extra.ACTIVATION_TYPE";
field public static final String EXTRA_EMBEDDED_SUBSCRIPTION_DOWNLOADABLE_SUBSCRIPTIONS = "android.telephony.euicc.extra.EMBEDDED_SUBSCRIPTION_DOWNLOADABLE_SUBSCRIPTIONS";
+ field public static final String EXTRA_ENABLE_SUBSCRIPTION = "android.telephony.euicc.extra.ENABLE_SUBSCRIPTION";
field public static final String EXTRA_FORCE_PROVISION = "android.telephony.euicc.extra.FORCE_PROVISION";
+ field public static final String EXTRA_SUBSCRIPTION_ID = "android.telephony.euicc.extra.SUBSCRIPTION_ID";
+ field public static final String EXTRA_SUBSCRIPTION_NICKNAME = "android.telephony.euicc.extra.SUBSCRIPTION_NICKNAME";
}
@IntDef(prefix={"EUICC_OTA_"}, value={android.telephony.euicc.EuiccManager.EUICC_OTA_IN_PROGRESS, android.telephony.euicc.EuiccManager.EUICC_OTA_FAILED, android.telephony.euicc.EuiccManager.EUICC_OTA_SUCCEEDED, android.telephony.euicc.EuiccManager.EUICC_OTA_NOT_NEEDED, android.telephony.euicc.EuiccManager.EUICC_OTA_STATUS_UNAVAILABLE}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccManager.OtaStatus {
diff --git a/api/test-current.txt b/api/test-current.txt
index d089831..eca7a7a 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -772,6 +772,11 @@
field public static final java.util.UUID EFFECT_TYPE_NULL;
}
+ public static class AudioEffect.Descriptor {
+ ctor public AudioEffect.Descriptor(android.os.Parcel);
+ method public void writeToParcel(android.os.Parcel);
+ }
+
public static interface AudioEffect.OnParameterChangeListener {
method public void onParameterChange(android.media.audiofx.AudioEffect, int, byte[], byte[]);
}
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 3c1be7e..60b2e25 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -4468,7 +4468,7 @@
// SIMx or eSEx.
optional string terminal = 2;
- optional string packageName = 3;
+ optional string package_name = 3;
}
/**
diff --git a/cmds/telecom/src/com/android/commands/telecom/Telecom.java b/cmds/telecom/src/com/android/commands/telecom/Telecom.java
index 4174ad7..1b7fbfe 100644
--- a/cmds/telecom/src/com/android/commands/telecom/Telecom.java
+++ b/cmds/telecom/src/com/android/commands/telecom/Telecom.java
@@ -23,8 +23,10 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
+import android.telecom.Log;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
import com.android.internal.os.BaseCommand;
import com.android.internal.telecom.ITelecomService;
@@ -45,6 +47,8 @@
private static final String COMMAND_SET_PHONE_ACCOUNT_ENABLED = "set-phone-account-enabled";
private static final String COMMAND_SET_PHONE_ACCOUNT_DISABLED = "set-phone-account-disabled";
private static final String COMMAND_REGISTER_PHONE_ACCOUNT = "register-phone-account";
+ private static final String COMMAND_SET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT =
+ "set-user-selected-outgoing-phone-account";
private static final String COMMAND_REGISTER_SIM_PHONE_ACCOUNT = "register-sim-phone-account";
private static final String COMMAND_SET_TEST_CALL_REDIRECTION_APP = "set-test-call-redirection-app";
private static final String COMMAND_SET_TEST_CALL_SCREENING_APP = "set-test-call-screening-app";
@@ -70,6 +74,8 @@
+ "usage: telecom set-phone-account-enabled <COMPONENT> <ID> <USER_SN>\n"
+ "usage: telecom set-phone-account-disabled <COMPONENT> <ID> <USER_SN>\n"
+ "usage: telecom register-phone-account <COMPONENT> <ID> <USER_SN> <LABEL>\n"
+ + "usage: telecom set-user-selected-outgoing-phone-account <COMPONENT> <ID> "
+ + "<USER_SN>\n"
+ "usage: telecom set-test-call-redirection-app <PACKAGE>\n"
+ "usage: telecom set-test-call-screening-app <PACKAGE>\n"
+ "usage: telecom set-test-auto-mode-app <PACKAGE>\n"
@@ -104,16 +110,18 @@
mTelecomService = ITelecomService.Stub.asInterface(
ServiceManager.getService(Context.TELECOM_SERVICE));
if (mTelecomService == null) {
+ Log.w(this, "onRun: Can't access telecom manager.");
showError("Error: Could not access the Telecom Manager. Is the system running?");
return;
}
mUserManager = IUserManager.Stub
.asInterface(ServiceManager.getService(Context.USER_SERVICE));
if (mUserManager == null) {
+ Log.w(this, "onRun: Can't access user manager.");
showError("Error: Could not access the User Manager. Is the system running?");
return;
}
-
+ Log.i(this, "onRun: parsing command.");
String command = nextArgRequired();
switch (command) {
case COMMAND_SET_PHONE_ACCOUNT_ENABLED:
@@ -143,6 +151,9 @@
case COMMAND_REGISTER_SIM_PHONE_ACCOUNT:
runRegisterSimPhoneAccount();
break;
+ case COMMAND_SET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT:
+ runSetUserSelectedOutgoingPhoneAccount();
+ break;
case COMMAND_UNREGISTER_PHONE_ACCOUNT:
runUnregisterPhoneAccount();
break;
@@ -159,6 +170,7 @@
runWaitOnHandler();
break;
default:
+ Log.w(this, "onRun: unknown command: %s", command);
throw new IllegalArgumentException ("unknown command '" + command + "'");
}
}
@@ -227,6 +239,13 @@
mTelecomService.setTestPhoneAcctSuggestionComponent(componentName);
}
+ private void runSetUserSelectedOutgoingPhoneAccount() throws RemoteException {
+ Log.i(this, "runSetUserSelectedOutgoingPhoneAccount");
+ final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+ mTelecomService.setUserSelectedOutgoingPhoneAccount(handle);
+ System.out.println("Success - " + handle + " set as default outgoing account.");
+ }
+
private void runUnregisterPhoneAccount() throws RemoteException {
final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
mTelecomService.unregisterPhoneAccount(handle);
@@ -256,7 +275,10 @@
}
- private PhoneAccountHandle getPhoneAccountHandleFromArgs() throws RemoteException{
+ private PhoneAccountHandle getPhoneAccountHandleFromArgs() throws RemoteException {
+ if (TextUtils.isEmpty(mArgs.peekNextArg())) {
+ return null;
+ }
final ComponentName component = parseComponentName(nextArgRequired());
final String accountId = nextArgRequired();
final String userSnInStr = nextArgRequired();
@@ -265,6 +287,7 @@
final int userSn = Integer.parseInt(userSnInStr);
userHandle = UserHandle.of(mUserManager.getUserHandle(userSn));
} catch (NumberFormatException ex) {
+ Log.w(this, "getPhoneAccountHandleFromArgs - invalid user %s", userSnInStr);
throw new IllegalArgumentException ("Invalid user serial number " + userSnInStr);
}
return new PhoneAccountHandle(component, accountId, userHandle);
@@ -277,4 +300,4 @@
}
return cn;
}
-}
\ No newline at end of file
+}
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index e0b8d78..1045b7a 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -3529,12 +3529,32 @@
/**
* Returns "true" if device is running in a test harness.
+ *
+ * @deprecated this method is false for all user builds. Users looking to check if their device
+ * is running in a device farm should see {@link #isRunningInUserTestHarness()}.
*/
+ @Deprecated
public static boolean isRunningInTestHarness() {
return SystemProperties.getBoolean("ro.test_harness", false);
}
/**
+ * Returns "true" if the device is running in Test Harness Mode.
+ *
+ * <p>Test Harness Mode is a feature that allows devices to run without human interaction in a
+ * device farm/testing harness (such as Firebase Test Lab). You should check this method if you
+ * want your app to behave differently when running in a test harness to skip setup screens that
+ * would impede UI testing. e.g. a keyboard application that has a full screen setup page for
+ * the first time it is launched.
+ *
+ * <p>Note that you should <em>not</em> use this to determine whether or not your app is running
+ * an instrumentation test, as it is not set for a standard device running a test.
+ */
+ public static boolean isRunningInUserTestHarness() {
+ return SystemProperties.getBoolean("persist.sys.test_harness", false);
+ }
+
+ /**
* Unsupported compiled sdk warning should always be shown for the intput activity
* even in cases where the system would normally not show the warning. E.g. when running in a
* test harness.
diff --git a/core/java/android/app/ActivityView.java b/core/java/android/app/ActivityView.java
index ab8f234..4d3711a 100644
--- a/core/java/android/app/ActivityView.java
+++ b/core/java/android/app/ActivityView.java
@@ -328,7 +328,7 @@
}
} else {
mTmpTransaction.reparent(mRootSurfaceControl,
- mSurfaceView.getSurfaceControl().getHandle()).apply();
+ mSurfaceView.getSurfaceControl()).apply();
}
if (mVirtualDisplay != null) {
@@ -390,7 +390,7 @@
.build();
try {
- wm.reparentDisplayContent(displayId, mRootSurfaceControl.getHandle());
+ wm.reparentDisplayContent(displayId, mRootSurfaceControl);
wm.dontOverrideDisplayInfo(displayId);
if (mSingleTaskInstance) {
mActivityTaskManager.setDisplayToSingleTaskInstance(displayId);
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
index 1ae0f52..75c9054 100644
--- a/core/java/android/app/KeyguardManager.java
+++ b/core/java/android/app/KeyguardManager.java
@@ -19,6 +19,7 @@
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
@@ -124,6 +125,7 @@
*
* @return the intent for launching the activity or null if no password is required.
**/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public Intent createConfirmDeviceCredentialIntent(CharSequence title, CharSequence description) {
if (!isDeviceSecure()) return null;
Intent intent = new Intent(ACTION_CONFIRM_DEVICE_CREDENTIAL);
@@ -176,6 +178,7 @@
* @throws IllegalStateException if the device has already been provisioned
* @hide
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
@SystemApi
public Intent createConfirmFactoryResetCredentialIntent(
CharSequence title, CharSequence description, CharSequence alternateButtonLabel) {
@@ -231,6 +234,7 @@
* secure notifications cannot be shown if {@code false}
* @hide
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
@RequiresPermission(Manifest.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS)
@SystemApi
public void setPrivateNotificationsAllowed(boolean allow) {
@@ -249,6 +253,7 @@
* By default, private notifications are allowed.
* @hide
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
@RequiresPermission(Manifest.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS)
@SystemApi
public boolean getPrivateNotificationsAllowed() {
diff --git a/core/java/android/app/VrManager.java b/core/java/android/app/VrManager.java
index 5b87de4..5f1a94c 100644
--- a/core/java/android/app/VrManager.java
+++ b/core/java/android/app/VrManager.java
@@ -215,19 +215,12 @@
}
/**
- * Start VR Input method for the packageName in {@link ComponentName}.
- * This method notifies InputMethodManagerService to use VR IME instead of
- * regular phone IME.
- * @param componentName ComponentName of a VR InputMethod that should be set as selected
- * input by InputMethodManagerService.
+ * This method is not implemented.
+ *
+ * @param componentName not used
*/
@RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS)
public void setVrInputMethod(ComponentName componentName) {
- try {
- mService.setVrInputMethod(componentName);
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
- }
}
/**
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 86db99bf..b0a08ed 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -1362,16 +1362,23 @@
public static final String EXTRA_RESTRICTION = "android.app.extra.RESTRICTION";
/**
- * Activity action: have the user enter a new password. This activity should
- * be launched after using {@link #setPasswordQuality(ComponentName, int)},
- * or {@link #setPasswordMinimumLength(ComponentName, int)} to have the user
- * enter a new password that meets the current requirements. You can use
- * {@link #isActivePasswordSufficient()} to determine whether you need to
- * have the user select a new password in order to meet the current
- * constraints. Upon being resumed from this activity, you can check the new
+ * Activity action: have the user enter a new password.
+ *
+ * <p>For admin apps, this activity should be launched after using {@link
+ * #setPasswordQuality(ComponentName, int)}, or {@link
+ * #setPasswordMinimumLength(ComponentName, int)} to have the user enter a new password that
+ * meets the current requirements. You can use {@link #isActivePasswordSufficient()} to
+ * determine whether you need to have the user select a new password in order to meet the
+ * current constraints. Upon being resumed from this activity, you can check the new
* password characteristics to see if they are sufficient.
*
- * If the intent is launched from within a managed profile with a profile
+ * <p>Non-admin apps can use {@link #getPasswordComplexity()} to check the current screen lock
+ * complexity, and use this activity with extra {@link #EXTRA_PASSWORD_COMPLEXITY} to suggest
+ * to users how complex the app wants the new screen lock to be. Note that both {@link
+ * #getPasswordComplexity()} and the extra {@link #EXTRA_PASSWORD_COMPLEXITY} require the
+ * calling app to have the permission {@link permission#GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY}.
+ *
+ * <p>If the intent is launched from within a managed profile with a profile
* owner built against {@link android.os.Build.VERSION_CODES#M} or before,
* this will trigger entering a new password for the parent of the profile.
* For all other cases it will trigger entering a new password for the user
@@ -1384,6 +1391,24 @@
= "android.app.action.SET_NEW_PASSWORD";
/**
+ * An integer indicating the complexity level of the new password an app would like the user to
+ * set when launching the action {@link #ACTION_SET_NEW_PASSWORD}.
+ *
+ * <p>Must be one of
+ * <ul>
+ * <li>{@link #PASSWORD_COMPLEXITY_HIGH}
+ * <li>{@link #PASSWORD_COMPLEXITY_MEDIUM}
+ * <li>{@link #PASSWORD_COMPLEXITY_LOW}
+ * <li>{@link #PASSWORD_COMPLEXITY_NONE}
+ * </ul>
+ *
+ * <p>If an invalid value is used, it will be treated as {@link #PASSWORD_COMPLEXITY_NONE}.
+ */
+ @RequiresPermission(android.Manifest.permission.GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY)
+ public static final String EXTRA_PASSWORD_COMPLEXITY =
+ "android.app.extra.PASSWORD_COMPLEXITY";
+
+ /**
* Constant for {@link #getPasswordComplexity()}: no password.
*
* <p>Note that these complexity constants are ordered so that higher values are more complex.
@@ -2513,6 +2538,9 @@
* requested quality constant (between the policy set here, the user's preference, and any other
* considerations) is the one that is in effect.
* <p>
+ * On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ * <p>
* The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
* not, a security exception will be thrown.
@@ -2548,6 +2576,9 @@
* returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
* restrictions on the parent profile.
*
+ * <p>Note: on devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature,
+ * the password is always treated as empty.
+ *
* @param admin The name of the admin component to check, or {@code null} to aggregate
* all admins.
*/
@@ -2580,6 +2611,9 @@
* {@link #PASSWORD_QUALITY_ALPHANUMERIC}, or {@link #PASSWORD_QUALITY_COMPLEX} with
* {@link #setPasswordQuality}.
* <p>
+ * On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ * <p>
* The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
* not, a security exception will be thrown.
@@ -2609,11 +2643,13 @@
* restrictions on this user and its participating profiles. Restrictions on profiles that have
* a separate challenge are not taken into account.
*
+ * <p>On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ *
* <p>This method can be called on the {@link DevicePolicyManager} instance
* returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
* restrictions on the parent profile.
*
- * user and its profiles or a particular one.
* @param admin The name of the admin component to check, or {@code null} to aggregate
* all admins.
*/
@@ -2644,6 +2680,9 @@
* setting this value. This constraint is only imposed if the administrator has also requested
* {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The default value is 0.
* <p>
+ * On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ * <p>
* The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
* not, a security exception will be thrown.
@@ -2678,6 +2717,9 @@
* and only applies when the password quality is
* {@link #PASSWORD_QUALITY_COMPLEX}.
*
+ * <p>On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ *
* <p>This method can be called on the {@link DevicePolicyManager} instance
* returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
* restrictions on the parent profile.
@@ -2714,6 +2756,9 @@
* setting this value. This constraint is only imposed if the administrator has also requested
* {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The default value is 0.
* <p>
+ * On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ * <p>
* The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
* not, a security exception will be thrown.
@@ -2748,6 +2793,9 @@
* and only applies when the password quality is
* {@link #PASSWORD_QUALITY_COMPLEX}.
*
+ * <p>On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ *
* <p>This method can be called on the {@link DevicePolicyManager} instance
* returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
* restrictions on the parent profile.
@@ -2784,6 +2832,9 @@
* only imposed if the administrator has also requested {@link #PASSWORD_QUALITY_COMPLEX} with
* {@link #setPasswordQuality}. The default value is 1.
* <p>
+ * On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ * <p>
* The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
* not, a security exception will be thrown.
@@ -2818,6 +2869,9 @@
* and only applies when the password quality is
* {@link #PASSWORD_QUALITY_COMPLEX}.
*
+ * <p>On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ *
* <p>This method can be called on the {@link DevicePolicyManager} instance
* returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
* restrictions on the parent profile.
@@ -2853,6 +2907,9 @@
* setting this value. This constraint is only imposed if the administrator has also requested
* {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The default value is 1.
* <p>
+ * On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ * <p>
* The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
* not, a security exception will be thrown.
@@ -2887,6 +2944,9 @@
* and only applies when the password quality is
* {@link #PASSWORD_QUALITY_COMPLEX}.
*
+ * <p>On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ *
* <p>This method can be called on the {@link DevicePolicyManager} instance
* returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
* restrictions on the parent profile.
@@ -2922,6 +2982,9 @@
* only imposed if the administrator has also requested {@link #PASSWORD_QUALITY_COMPLEX} with
* {@link #setPasswordQuality}. The default value is 1.
* <p>
+ * On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ * <p>
* The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
* not, a security exception will be thrown.
@@ -2955,6 +3018,9 @@
* and only applies when the password quality is
* {@link #PASSWORD_QUALITY_COMPLEX}.
*
+ * <p>On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ *
* <p>This method can be called on the {@link DevicePolicyManager} instance
* returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
* restrictions on the parent profile.
@@ -2990,6 +3056,9 @@
* setting this value. This constraint is only imposed if the administrator has also requested
* {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The default value is 0.
* <p>
+ * On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ * <p>
* The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
* not, a security exception will be thrown.
@@ -3024,6 +3093,9 @@
* and only applies when the password quality is
* {@link #PASSWORD_QUALITY_COMPLEX}.
*
+ * <p>On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ *
* <p>This method can be called on the {@link DevicePolicyManager} instance
* returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
* restrictions on the parent profile.
@@ -3060,6 +3132,9 @@
* , {@link #PASSWORD_QUALITY_NUMERIC_COMPLEX} {@link #PASSWORD_QUALITY_ALPHABETIC}, or
* {@link #PASSWORD_QUALITY_ALPHANUMERIC} with {@link #setPasswordQuality}.
* <p>
+ * On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ * <p>
* The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
* not, a security exception will be thrown.
@@ -3112,6 +3187,7 @@
* @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
* does not use {@link DeviceAdminInfo#USES_POLICY_EXPIRE_PASSWORD}
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public void setPasswordExpirationTimeout(@NonNull ComponentName admin, long timeout) {
if (mService != null) {
try {
@@ -3136,6 +3212,7 @@
* @param admin The name of the admin component to check, or {@code null} to aggregate all admins.
* @return The timeout for the given admin or the minimum of all timeouts
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public long getPasswordExpirationTimeout(@Nullable ComponentName admin) {
if (mService != null) {
try {
@@ -3160,6 +3237,7 @@
* @param admin The name of the admin component to check, or {@code null} to aggregate all admins.
* @return The password expiration time, in milliseconds since epoch.
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public long getPasswordExpiration(@Nullable ComponentName admin) {
if (mService != null) {
try {
@@ -3184,12 +3262,14 @@
* all admins.
* @return The length of the password history
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public int getPasswordHistoryLength(@Nullable ComponentName admin) {
return getPasswordHistoryLength(admin, myUserId());
}
/** @hide per-user version */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public int getPasswordHistoryLength(@Nullable ComponentName admin, int userHandle) {
if (mService != null) {
try {
@@ -3204,10 +3284,16 @@
/**
* Return the maximum password length that the device supports for a
* particular password quality.
+ * <p>On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always empty.
* @param quality The quality being interrogated.
* @return Returns the maximum length that the user can enter.
*/
public int getPasswordMaximumLength(int quality) {
+ PackageManager pm = mContext.getPackageManager();
+ if (!pm.hasSystemFeature(PackageManager.FEATURE_DEVICE_ID_ATTESTATION)) {
+ return 0;
+ }
// Kind-of arbitrary.
return 16;
}
@@ -3218,6 +3304,10 @@
* user and its participating profiles. Restrictions on profiles that have a separate challenge
* are not taken into account. The user must be unlocked in order to perform the check.
* <p>
+ * On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty - i.e. this method will always return false on such
+ * devices, provided any password requirements were set.
+ * <p>
* The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
* not, a security exception will be thrown.
@@ -3250,6 +3340,9 @@
* explicitly querying the parent profile screen lock complexity via {@link
* #getParentProfileInstance}.
*
+ * <p>On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
+ * password is always treated as empty.
+ *
* @throws IllegalStateException if the user is not unlocked.
* @throws SecurityException if the calling application does not have the permission
* {@link permission#GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY}
@@ -3329,6 +3422,7 @@
* @throws SecurityException if the calling application does not own an active administrator
* that uses {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN}
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public int getCurrentFailedPasswordAttempts() {
return getCurrentFailedPasswordAttempts(myUserId());
}
@@ -3396,6 +3490,7 @@
* both {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} and
* {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA}.
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public void setMaximumFailedPasswordsForWipe(@NonNull ComponentName admin, int num) {
if (mService != null) {
try {
@@ -3419,12 +3514,14 @@
* @param admin The name of the admin component to check, or {@code null} to aggregate
* all admins.
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public int getMaximumFailedPasswordsForWipe(@Nullable ComponentName admin) {
return getMaximumFailedPasswordsForWipe(admin, myUserId());
}
/** @hide per-user version */
@UnsupportedAppUsage
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public int getMaximumFailedPasswordsForWipe(@Nullable ComponentName admin, int userHandle) {
if (mService != null) {
try {
@@ -3444,6 +3541,7 @@
* user passed in.
* @hide Used only by Keyguard
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public int getProfileWithMinimumFailedPasswordsForWipe(int userHandle) {
if (mService != null) {
try {
@@ -3514,6 +3612,7 @@
* that uses {@link DeviceAdminInfo#USES_POLICY_RESET_PASSWORD}
* @throws IllegalStateException if the calling user is locked or has a managed profile.
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public boolean resetPassword(String password, int flags) {
throwIfParentInstance("resetPassword");
if (mService != null) {
@@ -3557,6 +3656,7 @@
* @throws SecurityException if admin is not a device or profile owner.
* @throws IllegalArgumentException if the supplied token is invalid.
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public boolean setResetPasswordToken(ComponentName admin, byte[] token) {
throwIfParentInstance("setResetPasswordToken");
if (mService != null) {
@@ -3576,6 +3676,7 @@
* @return true if the operation is successful, false otherwise.
* @throws SecurityException if admin is not a device or profile owner.
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public boolean clearResetPasswordToken(ComponentName admin) {
throwIfParentInstance("clearResetPasswordToken");
if (mService != null) {
@@ -3596,6 +3697,7 @@
* @throws SecurityException if admin is not a device or profile owner.
* @throws IllegalStateException if no token has been set.
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public boolean isResetPasswordTokenActive(ComponentName admin) {
throwIfParentInstance("isResetPasswordTokenActive");
if (mService != null) {
@@ -3637,6 +3739,7 @@
* @throws SecurityException if admin is not a device or profile owner.
* @throws IllegalStateException if the provided token is not valid.
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public boolean resetPasswordWithToken(@NonNull ComponentName admin, String password,
byte[] token, int flags) {
throwIfParentInstance("resetPassword");
@@ -3742,6 +3845,7 @@
*
* @throws SecurityException if {@code admin} is not a device or profile owner.
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public void setRequiredStrongAuthTimeout(@NonNull ComponentName admin,
long timeoutMs) {
if (mService != null) {
@@ -3766,12 +3870,14 @@
* across all participating admins.
* @return The timeout in milliseconds or 0 if not configured for the provided admin.
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public long getRequiredStrongAuthTimeout(@Nullable ComponentName admin) {
return getRequiredStrongAuthTimeout(admin, myUserId());
}
/** @hide per-user version */
@UnsupportedAppUsage
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public long getRequiredStrongAuthTimeout(@Nullable ComponentName admin, @UserIdInt int userId) {
if (mService != null) {
try {
@@ -5350,6 +5456,7 @@
* @hide
*/
@UnsupportedAppUsage
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public void setActivePasswordState(PasswordMetrics metrics, int userHandle) {
if (mService != null) {
try {
@@ -5363,6 +5470,7 @@
/**
* @hide
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public void reportPasswordChanged(@UserIdInt int userId) {
if (mService != null) {
try {
@@ -5377,6 +5485,7 @@
* @hide
*/
@UnsupportedAppUsage
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public void reportFailedPasswordAttempt(int userHandle) {
if (mService != null) {
try {
@@ -5391,6 +5500,7 @@
* @hide
*/
@UnsupportedAppUsage
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public void reportSuccessfulPasswordAttempt(int userHandle) {
if (mService != null) {
try {
@@ -5404,6 +5514,7 @@
/**
* @hide
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public void reportFailedBiometricAttempt(int userHandle) {
if (mService != null) {
try {
@@ -5417,6 +5528,7 @@
/**
* @hide
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public void reportSuccessfulBiometricAttempt(int userHandle) {
if (mService != null) {
try {
@@ -6383,6 +6495,7 @@
* @throws SecurityException if {@code admin} is not an active administrator or does not use
* {@link DeviceAdminInfo#USES_POLICY_DISABLE_KEYGUARD_FEATURES}
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public void setTrustAgentConfiguration(@NonNull ComponentName admin,
@NonNull ComponentName target, PersistableBundle configuration) {
if (mService != null) {
@@ -6412,6 +6525,7 @@
* @param agent Which component to get enabled features for.
* @return configuration for the given trust agent.
*/
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public @Nullable List<PersistableBundle> getTrustAgentConfiguration(
@Nullable ComponentName admin, @NonNull ComponentName agent) {
return getTrustAgentConfiguration(admin, agent, myUserId());
@@ -6419,6 +6533,7 @@
/** @hide per-user version */
@UnsupportedAppUsage
+ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)
public @Nullable List<PersistableBundle> getTrustAgentConfiguration(
@Nullable ComponentName admin, @NonNull ComponentName agent, int userHandle) {
if (mService != null) {
diff --git a/core/java/android/app/admin/PasswordMetrics.java b/core/java/android/app/admin/PasswordMetrics.java
index 8b41755..e5df2c7 100644
--- a/core/java/android/app/admin/PasswordMetrics.java
+++ b/core/java/android/app/admin/PasswordMetrics.java
@@ -20,6 +20,11 @@
import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW;
import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM;
import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -27,6 +32,8 @@
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -85,6 +92,101 @@
nonLetter = in.readInt();
}
+ /** Returns the min quality allowed by {@code complexityLevel}. */
+ public static int complexityLevelToMinQuality(@PasswordComplexity int complexityLevel) {
+ // this would be the quality of the first metrics since mMetrics is sorted in ascending
+ // order of quality
+ return PasswordComplexityBucket
+ .complexityLevelToBucket(complexityLevel).mMetrics[0].quality;
+ }
+
+ /**
+ * Returns a merged minimum {@link PasswordMetrics} requirements that a new password must meet
+ * to fulfil {@code requestedQuality}, {@code requiresNumeric} and {@code
+ * requiresLettersOrSymbols}, which are derived from {@link DevicePolicyManager} requirements,
+ * and {@code complexityLevel}.
+ *
+ * <p>Note that we are taking {@code userEnteredPasswordQuality} into account because there are
+ * more than one set of metrics to meet the minimum complexity requirement and inspecting what
+ * the user has entered can help determine whether the alphabetic or alphanumeric set of metrics
+ * should be used. For example, suppose minimum complexity requires either ALPHABETIC(8+), or
+ * ALPHANUMERIC(6+). If the user has entered "a", the length requirement displayed on the UI
+ * would be 8. Then the user appends "1" to make it "a1". We now know the user is entering
+ * an alphanumeric password so we would update the min complexity required min length to 6.
+ */
+ public static PasswordMetrics getMinimumMetrics(@PasswordComplexity int complexityLevel,
+ int userEnteredPasswordQuality, int requestedQuality, boolean requiresNumeric,
+ boolean requiresLettersOrSymbols) {
+ int targetQuality = Math.max(
+ userEnteredPasswordQuality,
+ getActualRequiredQuality(
+ requestedQuality, requiresNumeric, requiresLettersOrSymbols));
+ return getTargetQualityMetrics(complexityLevel, targetQuality);
+ }
+
+ /**
+ * Returns the {@link PasswordMetrics} at {@code complexityLevel} which the metrics quality
+ * is the same as {@code targetQuality}.
+ *
+ * <p>If {@code complexityLevel} does not allow {@code targetQuality}, returns the metrics
+ * with the min quality at {@code complexityLevel}.
+ */
+ // TODO(bernardchau): update tests to test getMinimumMetrics and change this to be private
+ @VisibleForTesting
+ public static PasswordMetrics getTargetQualityMetrics(
+ @PasswordComplexity int complexityLevel, int targetQuality) {
+ PasswordComplexityBucket targetBucket =
+ PasswordComplexityBucket.complexityLevelToBucket(complexityLevel);
+ for (PasswordMetrics metrics : targetBucket.mMetrics) {
+ if (targetQuality == metrics.quality) {
+ return metrics;
+ }
+ }
+ // none of the metrics at complexityLevel has targetQuality, return metrics with min quality
+ // see test case testGetMinimumMetrics_actualRequiredQualityStricter for an example, where
+ // min complexity allows at least NUMERIC_COMPLEX, user has not entered anything yet, and
+ // requested quality is NUMERIC
+ return targetBucket.mMetrics[0];
+ }
+
+ /**
+ * Finds out the actual quality requirement based on whether quality is {@link
+ * DevicePolicyManager#PASSWORD_QUALITY_COMPLEX} and whether digits, letters or symbols are
+ * required.
+ */
+ @VisibleForTesting
+ // TODO(bernardchau): update tests to test getMinimumMetrics and change this to be private
+ public static int getActualRequiredQuality(
+ int requestedQuality, boolean requiresNumeric, boolean requiresLettersOrSymbols) {
+ if (requestedQuality != PASSWORD_QUALITY_COMPLEX) {
+ return requestedQuality;
+ }
+
+ // find out actual password quality from complex requirements
+ if (requiresNumeric && requiresLettersOrSymbols) {
+ return PASSWORD_QUALITY_ALPHANUMERIC;
+ }
+ if (requiresLettersOrSymbols) {
+ return PASSWORD_QUALITY_ALPHABETIC;
+ }
+ if (requiresNumeric) {
+ // cannot specify numeric complex using complex quality so this must be numeric
+ return PASSWORD_QUALITY_NUMERIC;
+ }
+
+ // reaching here means dpm sets quality to complex without specifying any requirements
+ return PASSWORD_QUALITY_UNSPECIFIED;
+ }
+
+ /**
+ * Returns {@code complexityLevel} or {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE}
+ * if {@code complexityLevel} is not valid.
+ */
+ @PasswordComplexity
+ public static int sanitizeComplexityLevel(@PasswordComplexity int complexityLevel) {
+ return PasswordComplexityBucket.complexityLevelToBucket(complexityLevel).mComplexityLevel;
+ }
+
public boolean isDefault() {
return quality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED
&& length == 0 && letters == 0 && upperCase == 0 && lowerCase == 0
@@ -280,7 +382,7 @@
@PasswordComplexity
public int determineComplexity() {
for (PasswordComplexityBucket bucket : PasswordComplexityBucket.BUCKETS) {
- if (satisfiesBucket(bucket.getMetrics())) {
+ if (satisfiesBucket(bucket.mMetrics)) {
return bucket.mComplexityLevel;
}
}
@@ -290,7 +392,7 @@
/**
* Requirements in terms of {@link PasswordMetrics} for each {@link PasswordComplexity}.
*/
- public static class PasswordComplexityBucket {
+ private static class PasswordComplexityBucket {
/**
* Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_HIGH} in terms of
* {@link PasswordMetrics}.
@@ -299,12 +401,13 @@
new PasswordComplexityBucket(
PASSWORD_COMPLEXITY_HIGH,
new PasswordMetrics(
- DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */ 6),
+ DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */
+ 8),
new PasswordMetrics(
DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 6),
new PasswordMetrics(
- DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */
- 8));
+ DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */
+ 6));
/**
* Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_MEDIUM} in terms of
@@ -314,11 +417,12 @@
new PasswordComplexityBucket(
PASSWORD_COMPLEXITY_MEDIUM,
new PasswordMetrics(
- DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */ 4),
+ DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */
+ 4),
new PasswordMetrics(
DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 4),
new PasswordMetrics(
- DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */
+ DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */
4));
/**
@@ -328,11 +432,11 @@
private static final PasswordComplexityBucket LOW =
new PasswordComplexityBucket(
PASSWORD_COMPLEXITY_LOW,
- new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC),
- new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC),
- new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX),
+ new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING),
new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC),
- new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING));
+ new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX),
+ new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC),
+ new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC));
/**
* A special bucket to represent {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE}.
@@ -348,19 +452,27 @@
private final int mComplexityLevel;
private final PasswordMetrics[] mMetrics;
+ /**
+ * @param metricsArray must be sorted in ascending order of {@link #quality}.
+ */
private PasswordComplexityBucket(@PasswordComplexity int complexityLevel,
- PasswordMetrics... metrics) {
- this.mComplexityLevel = complexityLevel;
- this.mMetrics = metrics;
- }
+ PasswordMetrics... metricsArray) {
+ int previousQuality = PASSWORD_QUALITY_UNSPECIFIED;
+ for (PasswordMetrics metrics : metricsArray) {
+ if (metrics.quality < previousQuality) {
+ throw new IllegalArgumentException("metricsArray must be sorted in ascending"
+ + " order of quality");
+ }
+ previousQuality = metrics.quality;
+ }
- /** Returns the {@link PasswordMetrics} that meet the min requirements of this bucket. */
- public PasswordMetrics[] getMetrics() {
- return mMetrics;
+ this.mMetrics = metricsArray;
+ this.mComplexityLevel = complexityLevel;
+
}
/** Returns the bucket that {@code complexityLevel} represents. */
- public static PasswordComplexityBucket complexityLevelToBucket(
+ private static PasswordComplexityBucket complexityLevelToBucket(
@PasswordComplexity int complexityLevel) {
for (PasswordComplexityBucket bucket : BUCKETS) {
if (bucket.mComplexityLevel == complexityLevel) {
diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java
index 1945b2f..97bc079 100644
--- a/core/java/android/bluetooth/BluetoothAdapter.java
+++ b/core/java/android/bluetooth/BluetoothAdapter.java
@@ -36,6 +36,7 @@
import android.content.Context;
import android.os.BatteryStats;
import android.os.Binder;
+import android.os.Handler;
import android.os.IBinder;
import android.os.ParcelUuid;
import android.os.RemoteException;
@@ -648,6 +649,32 @@
private final Object mLock = new Object();
private final Map<LeScanCallback, ScanCallback> mLeScanClients;
+ private static final Map<BluetoothDevice, List<Pair<MetadataListener, Handler>>>
+ sMetadataListeners = new HashMap<>();
+
+ /**
+ * Bluetooth metadata listener. Overrides the default BluetoothMetadataListener
+ * implementation.
+ */
+ private static final IBluetoothMetadataListener sBluetoothMetadataListener =
+ new IBluetoothMetadataListener.Stub() {
+ @Override
+ public void onMetadataChanged(BluetoothDevice device, int key, String value) {
+ synchronized (sMetadataListeners) {
+ if (sMetadataListeners.containsKey(device)) {
+ List<Pair<MetadataListener, Handler>> list = sMetadataListeners.get(device);
+ for (Pair<MetadataListener, Handler> pair : list) {
+ MetadataListener listener = pair.first;
+ Handler handler = pair.second;
+ handler.post(() -> {
+ listener.onMetadataChanged(device, key, value);
+ });
+ }
+ }
+ }
+ return;
+ }
+ };
/**
* Get a handle to the default local Bluetooth adapter.
@@ -2607,6 +2634,16 @@
}
}
}
+ synchronized (sMetadataListeners) {
+ sMetadataListeners.forEach((device, pair) -> {
+ try {
+ mService.registerMetadataListener(sBluetoothMetadataListener,
+ device);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to register metadata listener", e);
+ }
+ });
+ }
}
public void onBluetoothServiceDown() {
@@ -3090,4 +3127,142 @@
+ "listenUsingInsecureL2capChannel");
return listenUsingInsecureL2capChannel();
}
+
+ /**
+ * Register a {@link #MetadataListener} to receive update about metadata
+ * changes for this {@link BluetoothDevice}.
+ * Registration must be done when Bluetooth is ON and will last until
+ * {@link #unregisterMetadataListener(BluetoothDevice)} is called, even when Bluetooth
+ * restarted in the middle.
+ * All input parameters should not be null or {@link NullPointerException} will be triggered.
+ * The same {@link BluetoothDevice} and {@link #MetadataListener} pair can only be registered
+ * once, double registration would cause {@link IllegalArgumentException}.
+ *
+ * @param device {@link BluetoothDevice} that will be registered
+ * @param listener {@link #MetadataListener} that will receive asynchronous callbacks
+ * @param handler the handler for listener callback
+ * @return true on success, false on error
+ * @throws NullPointerException If one of {@code listener}, {@code device} or {@code handler}
+ * is null.
+ * @throws IllegalArgumentException The same {@link #MetadataListener} and
+ * {@link BluetoothDevice} are registered twice.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean registerMetadataListener(BluetoothDevice device, MetadataListener listener,
+ Handler handler) {
+ if (DBG) Log.d(TAG, "registerMetdataListener()");
+
+ final IBluetooth service = mService;
+ if (service == null) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot register metadata listener");
+ return false;
+ }
+ if (listener == null) {
+ throw new NullPointerException("listener is null");
+ }
+ if (device == null) {
+ throw new NullPointerException("device is null");
+ }
+ if (handler == null) {
+ throw new NullPointerException("handler is null");
+ }
+
+ synchronized (sMetadataListeners) {
+ List<Pair<MetadataListener, Handler>> listenerList = sMetadataListeners.get(device);
+ if (listenerList == null) {
+ // Create new listener/handler list for registeration
+ listenerList = new ArrayList<>();
+ sMetadataListeners.put(device, listenerList);
+ } else {
+ // Check whether this device was already registed by the lisenter
+ if (listenerList.stream().anyMatch((pair) -> (pair.first.equals(listener)))) {
+ throw new IllegalArgumentException("listener was already regestered"
+ + " for the device");
+ }
+ }
+
+ Pair<MetadataListener, Handler> listenerPair = new Pair(listener, handler);
+ listenerList.add(listenerPair);
+
+ boolean ret = false;
+ try {
+ ret = service.registerMetadataListener(sBluetoothMetadataListener, device);
+ } catch (RemoteException e) {
+ Log.e(TAG, "registerMetadataListener fail", e);
+ } finally {
+ if (!ret) {
+ // Remove listener registered earlier when fail.
+ listenerList.remove(listenerPair);
+ if (listenerList.isEmpty()) {
+ // Remove the device if its listener list is empty
+ sMetadataListeners.remove(device);
+ }
+ }
+ }
+ return ret;
+ }
+ }
+
+ /**
+ * Unregister all {@link MetadataListener} from this {@link BluetoothDevice}.
+ * Unregistration can be done when Bluetooth is either ON or OFF.
+ * {@link #registerMetadataListener(MetadataListener, BluetoothDevice, Handler)} must
+ * be called before unregisteration.
+ * Unregistering a device that is not regestered would cause {@link IllegalArgumentException}.
+ *
+ * @param device {@link BluetoothDevice} that will be unregistered. it
+ * should not be null or {@link NullPointerException} will be triggered.
+ * @return true on success, false on error
+ * @throws NullPointerException If {@code device} is null.
+ * @throws IllegalArgumentException If {@code device} has not been registered before.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean unregisterMetadataListener(BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "unregisterMetdataListener()");
+ if (device == null) {
+ throw new NullPointerException("device is null");
+ }
+
+ synchronized (sMetadataListeners) {
+ if (sMetadataListeners.containsKey(device)) {
+ sMetadataListeners.remove(device);
+ } else {
+ throw new IllegalArgumentException("device was not registered");
+ }
+
+ final IBluetooth service = mService;
+ if (service == null) {
+ // Bluetooth is OFF, do nothing to Bluetooth service.
+ return true;
+ }
+ try {
+ return service.unregisterMetadataListener(device);
+ } catch (RemoteException e) {
+ Log.e(TAG, "unregisterMetadataListener fail", e);
+ return false;
+ }
+ }
+ }
+
+ /**
+ * This abstract class is used to implement {@link BluetoothAdapter} metadata listener.
+ * @hide
+ */
+ @SystemApi
+ public abstract class MetadataListener {
+ /**
+ * Callback triggered if the metadata of {@link BluetoothDevice} registered in
+ * {@link #registerMetadataListener}.
+ *
+ * @param device changed {@link BluetoothDevice}.
+ * @param key changed metadata key, one of BluetoothDevice.METADATA_*.
+ * @param value the new value of metadata.
+ */
+ public void onMetadataChanged(BluetoothDevice device, int key, String value) {
+ }
+ }
}
diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java
index 235dc5c..17cf702 100644
--- a/core/java/android/bluetooth/BluetoothDevice.java
+++ b/core/java/android/bluetooth/BluetoothDevice.java
@@ -341,6 +341,137 @@
"android.bluetooth.device.action.SDP_RECORD";
/**
+ * Maximum length of a metadata entry, this is to avoid exploding Bluetooth
+ * disk usage
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MAX_LENGTH = 2048;
+
+ /**
+ * Manufacturer name of this Bluetooth device
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MANUFACTURER_NAME = 0;
+
+ /**
+ * Model name of this Bluetooth device
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MODEL_NAME = 1;
+
+ /**
+ * Software version of this Bluetooth device
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_SOFTWARE_VERSION = 2;
+
+ /**
+ * Hardware version of this Bluetooth device
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_HARDWARE_VERSION = 3;
+
+ /**
+ * Package name of the companion app, if any
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_COMPANION_APP = 4;
+
+ /**
+ * URI to the main icon shown on the settings UI
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MAIN_ICON = 5;
+
+ /**
+ * Whether this device is an untethered headset with left, right and case
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_IS_UNTHETHERED_HEADSET = 6;
+
+ /**
+ * URI to icon of the left headset
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTHETHERED_LEFT_ICON = 7;
+
+ /**
+ * URI to icon of the right headset
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTHETHERED_RIGHT_ICON = 8;
+
+ /**
+ * URI to icon of the headset charging case
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTHETHERED_CASE_ICON = 9;
+
+ /**
+ * Battery level (0-100), {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
+ * is invalid, of the left headset
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTHETHERED_LEFT_BATTERY = 10;
+
+ /**
+ * Battery level (0-100), {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
+ * is invalid, of the right headset
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTHETHERED_RIGHT_BATTERY = 11;
+
+ /**
+ * Battery level (0-100), {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
+ * is invalid, of the headset charging case
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTHETHERED_CASE_BATTERY = 12;
+
+ /**
+ * Whether the left headset is charging
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTHETHERED_LEFT_CHARGING = 13;
+
+ /**
+ * Whether the right headset is charging
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTHETHERED_RIGHT_CHARGING = 14;
+
+ /**
+ * Whether the headset charging case is charging
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTHETHERED_CASE_CHARGING = 15;
+
+ /**
+ * URI to the enhanced settings UI slice, null or empty String means
+ * the UI does not exist
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_ENHANCED_SETTINGS_UI_URI = 16;
+
+ /**
* Broadcast Action: This intent is used to broadcast the {@link UUID}
* wrapped as a {@link android.os.ParcelUuid} of the remote device after it
* has been fetched. This intent is sent only when the UUIDs of the remote
@@ -2026,4 +2157,61 @@
Log.e(TAG, "createL2capCocSocket: PLEASE USE THE OFFICIAL API, createInsecureL2capChannel");
return createInsecureL2capChannel(psm);
}
+
+ /**
+ * Set a keyed metadata of this {@link BluetoothDevice} to a
+ * {@link String} value.
+ * Only bonded devices's metadata will be persisted across Bluetooth
+ * restart.
+ * Metadata will be removed when the device's bond state is moved to
+ * {@link #BOND_NONE}.
+ *
+ * @param key must be within the list of BluetoothDevice.METADATA_*
+ * @param value the string data to set for key. Must be less than
+ * {@link BluetoothAdapter#METADATA_MAX_LENGTH} characters in length
+ * @return true on success, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean setMetadata(int key, String value) {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot set metadata");
+ return false;
+ }
+ if (value.length() > METADATA_MAX_LENGTH) {
+ throw new IllegalArgumentException("value length is " + value.length()
+ + ", should not over " + METADATA_MAX_LENGTH);
+ }
+ try {
+ return service.setMetadata(this, key, value);
+ } catch (RemoteException e) {
+ Log.e(TAG, "setMetadata fail", e);
+ return false;
+ }
+ }
+
+ /**
+ * Get a keyed metadata for this {@link BluetoothDevice} as {@link String}
+ *
+ * @param key must be within the list of BluetoothDevice.METADATA_*
+ * @return Metadata of the key as string, null on error or not found
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public String getMetadata(int key) {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot get metadata");
+ return null;
+ }
+ try {
+ return service.getMetadata(this, key);
+ } catch (RemoteException e) {
+ Log.e(TAG, "getMetadata fail", e);
+ return null;
+ }
+ }
}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 3c487a1..b46203c 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2008,6 +2008,15 @@
"android.intent.extra.PERMISSION_GROUP_NAME";
/**
+ * Intent extra: The number of milliseconds.
+ * <p>
+ * Type: long
+ * </p>
+ */
+ public static final String EXTRA_DURATION_MILLIS =
+ "android.intent.extra.DURATION_MILLIS";
+
+ /**
* Activity action: Launch UI to review app uses of permissions.
* <p>
* Input: {@link #EXTRA_PERMISSION_NAME} specifies the permission name
@@ -2020,11 +2029,16 @@
* {@link #EXTRA_PERMISSION_NAME}.
* </p>
* <p>
+ * Input: {@link #EXTRA_DURATION_MILLIS} specifies the minimum number of milliseconds of recent
+ * activity to show (optional). Must be non-negative.
+ * </p>
+ * <p>
* Output: Nothing.
* </p>
*
* @see #EXTRA_PERMISSION_NAME
* @see #EXTRA_PERMISSION_GROUP_NAME
+ * @see #EXTRA_DURATION_MILLIS
*
* @hide
*/
@@ -2363,7 +2377,6 @@
/**
* Broadcast Action: An existing version of an application package has been
* rolled back to a previous version.
- * The data contains the name of the package.
*
* <p class="note">This is a protected intent that can only be sent
* by the system.
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index b845673..9e2f316 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2060,6 +2060,14 @@
/**
* Feature for {@link #getSystemAvailableFeatures} and
+ * {@link #hasSystemFeature}: The device has a secure implementation of keyguard, meaning the
+ * device supports PIN, pattern and password as defined in Android CDD
+ */
+ @SdkConstant(SdkConstantType.FEATURE)
+ public static final String FEATURE_SECURE_LOCK_SCREEN = "android.software.secure_lock_screen";
+
+ /**
+ * Feature for {@link #getSystemAvailableFeatures} and
* {@link #hasSystemFeature}: The device includes an accelerometer.
*/
@SdkConstant(SdkConstantType.FEATURE)
@@ -5764,7 +5772,7 @@
*/
@RequiresPermission(value = android.Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE,
conditional = true)
- public abstract void setComponentEnabledSetting(ComponentName componentName,
+ public abstract void setComponentEnabledSetting(@NonNull ComponentName componentName,
@EnabledState int newState, @EnabledFlags int flags);
/**
@@ -5778,7 +5786,7 @@
* @return Returns the current enabled state for the component.
*/
public abstract @EnabledState int getComponentEnabledSetting(
- ComponentName componentName);
+ @NonNull ComponentName componentName);
/**
* Set the enabled setting for an application
@@ -5793,7 +5801,7 @@
*/
@RequiresPermission(value = android.Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE,
conditional = true)
- public abstract void setApplicationEnabledSetting(String packageName,
+ public abstract void setApplicationEnabledSetting(@NonNull String packageName,
@EnabledState int newState, @EnabledFlags int flags);
/**
@@ -5807,7 +5815,7 @@
* @return Returns the current enabled state for the application.
* @throws IllegalArgumentException if the named package does not exist.
*/
- public abstract @EnabledState int getApplicationEnabledSetting(String packageName);
+ public abstract @EnabledState int getApplicationEnabledSetting(@NonNull String packageName);
/**
* Flush the package restrictions for a given user to disk. This forces the package restrictions
diff --git a/core/java/android/content/pm/PackageManagerInternal.java b/core/java/android/content/pm/PackageManagerInternal.java
index c7320b0..c9a4c82 100644
--- a/core/java/android/content/pm/PackageManagerInternal.java
+++ b/core/java/android/content/pm/PackageManagerInternal.java
@@ -794,6 +794,12 @@
"android.content.pm.extra.ENABLE_ROLLBACK_INSTALL_FLAGS";
/**
+ * Extra field name for the set of installed users for a given rollback package.
+ */
+ public static final String EXTRA_ENABLE_ROLLBACK_INSTALLED_USERS =
+ "android.content.pm.extra.ENABLE_ROLLBACK_INSTALLED_USERS";
+
+ /**
* Used as the {@code enableRollbackCode} argument for
* {@link PackageManagerInternal#setEnableRollbackCode} to indicate that
* enabling rollback succeeded.
@@ -827,4 +833,10 @@
* Ask the package manager to compile layouts in the given package.
*/
public abstract boolean compileLayouts(String packageName);
+
+ /*
+ * Inform the package manager that the pending package install identified by
+ * {@code token} can be completed.
+ */
+ public abstract void finishPackageInstall(int token, boolean didLaunch);
}
diff --git a/core/java/android/content/rollback/IRollbackManager.aidl b/core/java/android/content/rollback/IRollbackManager.aidl
index 7f557cd..420bcb6 100644
--- a/core/java/android/content/rollback/IRollbackManager.aidl
+++ b/core/java/android/content/rollback/IRollbackManager.aidl
@@ -33,6 +33,12 @@
void executeRollback(in RollbackInfo rollback, String callerPackageName,
in IntentSender statusReceiver);
+ // Exposed for use from the system server only. Callback from the package
+ // manager during the install flow when user data can be restored for a given
+ // package.
+ void restoreUserData(String packageName, int userId, int appId, long ceDataInode,
+ String seInfo, int token);
+
// Exposed for test purposes only.
void reloadPersistedData();
diff --git a/core/java/android/content/rollback/PackageRollbackInfo.java b/core/java/android/content/rollback/PackageRollbackInfo.java
index 2040024..4644a83 100644
--- a/core/java/android/content/rollback/PackageRollbackInfo.java
+++ b/core/java/android/content/rollback/PackageRollbackInfo.java
@@ -17,11 +17,10 @@
package android.content.rollback;
import android.annotation.SystemApi;
+import android.content.pm.VersionedPackage;
import android.os.Parcel;
import android.os.Parcelable;
-import java.util.Objects;
-
/**
* Information about a rollback available for a particular package.
*
@@ -29,59 +28,41 @@
*/
@SystemApi
public final class PackageRollbackInfo implements Parcelable {
- /**
- * The name of a package being rolled back.
- */
- public final String packageName;
+
+ private final VersionedPackage mVersionRolledBackFrom;
+ private final VersionedPackage mVersionRolledBackTo;
/**
- * The version the package was rolled back from.
+ * Returns the name of the package to roll back from.
*/
- public final PackageVersion higherVersion;
-
- /**
- * The version the package was rolled back to.
- */
- public final PackageVersion lowerVersion;
-
- /**
- * Represents a version of a package.
- */
- public static class PackageVersion {
- public final long versionCode;
-
- // TODO(b/120200473): Include apk sha or some other way to distinguish
- // between two different apks with the same version code.
- public PackageVersion(long versionCode) {
- this.versionCode = versionCode;
- }
-
- @Override
- public boolean equals(Object other) {
- if (other instanceof PackageVersion) {
- PackageVersion otherVersion = (PackageVersion) other;
- return versionCode == otherVersion.versionCode;
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(versionCode);
- }
+ public String getPackageName() {
+ return mVersionRolledBackFrom.getPackageName();
}
- public PackageRollbackInfo(String packageName,
- PackageVersion higherVersion, PackageVersion lowerVersion) {
- this.packageName = packageName;
- this.higherVersion = higherVersion;
- this.lowerVersion = lowerVersion;
+ /**
+ * Returns the version of the package rolled back from.
+ */
+ public VersionedPackage getVersionRolledBackFrom() {
+ return mVersionRolledBackFrom;
+ }
+
+ /**
+ * Returns the version of the package rolled back to.
+ */
+ public VersionedPackage getVersionRolledBackTo() {
+ return mVersionRolledBackTo;
+ }
+
+ /** @hide */
+ public PackageRollbackInfo(VersionedPackage packageRolledBackFrom,
+ VersionedPackage packageRolledBackTo) {
+ this.mVersionRolledBackFrom = packageRolledBackFrom;
+ this.mVersionRolledBackTo = packageRolledBackTo;
}
private PackageRollbackInfo(Parcel in) {
- this.packageName = in.readString();
- this.higherVersion = new PackageVersion(in.readLong());
- this.lowerVersion = new PackageVersion(in.readLong());
+ this.mVersionRolledBackFrom = VersionedPackage.CREATOR.createFromParcel(in);
+ this.mVersionRolledBackTo = VersionedPackage.CREATOR.createFromParcel(in);
}
@Override
@@ -91,9 +72,8 @@
@Override
public void writeToParcel(Parcel out, int flags) {
- out.writeString(packageName);
- out.writeLong(higherVersion.versionCode);
- out.writeLong(lowerVersion.versionCode);
+ mVersionRolledBackFrom.writeToParcel(out, flags);
+ mVersionRolledBackTo.writeToParcel(out, flags);
}
public static final Parcelable.Creator<PackageRollbackInfo> CREATOR =
diff --git a/core/java/android/content/rollback/RollbackInfo.java b/core/java/android/content/rollback/RollbackInfo.java
index 66df4fe..0803a7c 100644
--- a/core/java/android/content/rollback/RollbackInfo.java
+++ b/core/java/android/content/rollback/RollbackInfo.java
@@ -30,6 +30,11 @@
public final class RollbackInfo implements Parcelable {
/**
+ * A unique identifier for the rollback.
+ */
+ private final int mRollbackId;
+
+ /**
* The package that needs to be rolled back.
*/
public final PackageRollbackInfo targetPackage;
@@ -40,12 +45,21 @@
// staged installs is supported.
/** @hide */
- public RollbackInfo(PackageRollbackInfo targetPackage) {
+ public RollbackInfo(int rollbackId, PackageRollbackInfo targetPackage) {
+ this.mRollbackId = rollbackId;
this.targetPackage = targetPackage;
}
private RollbackInfo(Parcel in) {
- this.targetPackage = PackageRollbackInfo.CREATOR.createFromParcel(in);
+ mRollbackId = in.readInt();
+ targetPackage = PackageRollbackInfo.CREATOR.createFromParcel(in);
+ }
+
+ /**
+ * Returns a unique identifier for this rollback.
+ */
+ public int getRollbackId() {
+ return mRollbackId;
}
@Override
@@ -55,6 +69,7 @@
@Override
public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mRollbackId);
targetPackage.writeToParcel(out, flags);
}
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index d3509d5..38ddc16 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -2087,7 +2087,14 @@
* protocol, so applications with custom text editing written before this method appeared will
* not call to inform the IME of this interaction.
* @param focusChanged true if the user changed the focused view by this click.
+ * @see InputMethodManager#viewClicked(View)
+ * @deprecated The method may not be called for composite {@link View} that works as a giant
+ * "Canvas", which can host its own UI hierarchy and sub focus state.
+ * {@link android.webkit.WebView} is a good example. Application / IME developers
+ * should not rely on this method. If your goal is just being notified when an
+ * on-going input is interrupted, simply monitor {@link #onFinishInput()}.
*/
+ @Deprecated
public void onViewClicked(boolean focusChanged) {
// Intentionally empty
}
diff --git a/core/java/android/net/DnsPacket.java b/core/java/android/net/DnsPacket.java
new file mode 100644
index 0000000..458fb34
--- /dev/null
+++ b/core/java/android/net/DnsPacket.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.internal.util.BitUtils;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringJoiner;
+
+/**
+ * Defines basic data for DNS protocol based on RFC 1035.
+ * Subclasses create the specific format used in DNS packet.
+ *
+ * @hide
+ */
+public abstract class DnsPacket {
+ public class DnsHeader {
+ private static final String TAG = "DnsHeader";
+ public final int id;
+ public final int flags;
+ public final int rcode;
+ private final int[] mSectionCount;
+
+ /**
+ * Create a new DnsHeader from a positioned ByteBuffer.
+ *
+ * The ByteBuffer must be in network byte order (which is the default).
+ * Reads the passed ByteBuffer from its current position and decodes a DNS header.
+ * When this constructor returns, the reading position of the ByteBuffer has been
+ * advanced to the end of the DNS header record.
+ * This is meant to chain with other methods reading a DNS response in sequence.
+ *
+ */
+ DnsHeader(@NonNull ByteBuffer buf) throws BufferUnderflowException {
+ id = BitUtils.uint16(buf.getShort());
+ flags = BitUtils.uint16(buf.getShort());
+ rcode = flags & 0xF;
+ mSectionCount = new int[NUM_SECTIONS];
+ for (int i = 0; i < NUM_SECTIONS; ++i) {
+ mSectionCount[i] = BitUtils.uint16(buf.getShort());
+ }
+ }
+
+ /**
+ * Get section count by section type.
+ */
+ public int getSectionCount(int sectionType) {
+ return mSectionCount[sectionType];
+ }
+ }
+
+ public class DnsSection {
+ private static final int MAXNAMESIZE = 255;
+ private static final int MAXLABELSIZE = 63;
+ private static final int MAXLABELCOUNT = 128;
+ private static final int NAME_NORMAL = 0;
+ private static final int NAME_COMPRESSION = 0xC0;
+ private final DecimalFormat byteFormat = new DecimalFormat();
+ private final FieldPosition pos = new FieldPosition(0);
+
+ private static final String TAG = "DnsSection";
+
+ public final String dName;
+ public final int nsType;
+ public final int nsClass;
+ public final long ttl;
+ private final byte[] mRR;
+
+ /**
+ * Create a new DnsSection from a positioned ByteBuffer.
+ *
+ * The ByteBuffer must be in network byte order (which is the default).
+ * Reads the passed ByteBuffer from its current position and decodes a DNS section.
+ * When this constructor returns, the reading position of the ByteBuffer has been
+ * advanced to the end of the DNS header record.
+ * This is meant to chain with other methods reading a DNS response in sequence.
+ *
+ */
+ DnsSection(int sectionType, @NonNull ByteBuffer buf)
+ throws BufferUnderflowException, ParseException {
+ dName = parseName(buf, 0 /* Parse depth */);
+ if (dName.length() > MAXNAMESIZE) {
+ throw new ParseException("Parse name fail, name size is too long");
+ }
+ nsType = BitUtils.uint16(buf.getShort());
+ nsClass = BitUtils.uint16(buf.getShort());
+
+ if (sectionType != QDSECTION) {
+ ttl = BitUtils.uint32(buf.getInt());
+ final int length = BitUtils.uint16(buf.getShort());
+ mRR = new byte[length];
+ buf.get(mRR);
+ } else {
+ ttl = 0;
+ mRR = null;
+ }
+ }
+
+ /**
+ * Get a copy of rr.
+ */
+ @Nullable public byte[] getRR() {
+ return (mRR == null) ? null : mRR.clone();
+ }
+
+ /**
+ * Convert label from {@code byte[]} to {@code String}
+ *
+ * It follows the same converting rule as native layer.
+ * (See ns_name.c in libc)
+ *
+ */
+ private String labelToString(@NonNull byte[] label) {
+ final StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < label.length; ++i) {
+ int b = BitUtils.uint8(label[i]);
+ // Control characters and non-ASCII characters.
+ if (b <= 0x20 || b >= 0x7f) {
+ sb.append('\\');
+ byteFormat.format(b, sb, pos);
+ } else if (b == '"' || b == '.' || b == ';' || b == '\\'
+ || b == '(' || b == ')' || b == '@' || b == '$') {
+ sb.append('\\');
+ sb.append((char) b);
+ } else {
+ sb.append((char) b);
+ }
+ }
+ return sb.toString();
+ }
+
+ private String parseName(@NonNull ByteBuffer buf, int depth) throws
+ BufferUnderflowException, ParseException {
+ if (depth > MAXLABELCOUNT) throw new ParseException("Parse name fails, too many labels");
+ final int len = BitUtils.uint8(buf.get());
+ final int mask = len & NAME_COMPRESSION;
+ if (0 == len) {
+ return "";
+ } else if (mask != NAME_NORMAL && mask != NAME_COMPRESSION) {
+ throw new ParseException("Parse name fail, bad label type");
+ } else if (mask == NAME_COMPRESSION) {
+ // Name compression based on RFC 1035 - 4.1.4 Message compression
+ final int offset = ((len & ~NAME_COMPRESSION) << 8) + BitUtils.uint8(buf.get());
+ final int oldPos = buf.position();
+ if (offset >= oldPos - 2) {
+ throw new ParseException("Parse compression name fail, invalid compression");
+ }
+ buf.position(offset);
+ final String pointed = parseName(buf, depth + 1);
+ buf.position(oldPos);
+ return pointed;
+ } else {
+ final byte[] label = new byte[len];
+ buf.get(label);
+ final String head = labelToString(label);
+ if (head.length() > MAXLABELSIZE) {
+ throw new ParseException("Parse name fail, invalid label length");
+ }
+ final String tail = parseName(buf, depth + 1);
+ return TextUtils.isEmpty(tail) ? head : head + "." + tail;
+ }
+ }
+ }
+
+ public static final int QDSECTION = 0;
+ public static final int ANSECTION = 1;
+ public static final int NSSECTION = 2;
+ public static final int ARSECTION = 3;
+ private static final int NUM_SECTIONS = ARSECTION + 1;
+
+ private static final String TAG = DnsPacket.class.getSimpleName();
+
+ protected final DnsHeader mHeader;
+ protected final List<DnsSection>[] mSections;
+
+ public static class ParseException extends Exception {
+ public ParseException(String msg) {
+ super(msg);
+ }
+
+ public ParseException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+ }
+
+ protected DnsPacket(@NonNull byte[] data) throws ParseException {
+ if (null == data) throw new ParseException("Parse header failed, null input data");
+ final ByteBuffer buffer;
+ try {
+ buffer = ByteBuffer.wrap(data);
+ mHeader = new DnsHeader(buffer);
+ } catch (BufferUnderflowException e) {
+ throw new ParseException("Parse Header fail, bad input data", e);
+ }
+
+ mSections = new ArrayList[NUM_SECTIONS];
+
+ for (int i = 0; i < NUM_SECTIONS; ++i) {
+ final int count = mHeader.getSectionCount(i);
+ if (count > 0) {
+ mSections[i] = new ArrayList(count);
+ }
+ for (int j = 0; j < count; ++j) {
+ try {
+ mSections[i].add(new DnsSection(i, buffer));
+ } catch (BufferUnderflowException e) {
+ throw new ParseException("Parse section fail", e);
+ }
+ }
+ }
+ }
+}
diff --git a/core/java/android/net/DnsResolver.java b/core/java/android/net/DnsResolver.java
new file mode 100644
index 0000000..6d54264
--- /dev/null
+++ b/core/java/android/net/DnsResolver.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.net.NetworkUtils.resNetworkQuery;
+import static android.net.NetworkUtils.resNetworkResult;
+import static android.net.NetworkUtils.resNetworkSend;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.MessageQueue;
+import android.system.ErrnoException;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+
+/**
+ * Dns resolver class for asynchronous dns querying
+ *
+ */
+public final class DnsResolver {
+ private static final String TAG = "DnsResolver";
+ private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+ private static final int MAXPACKET = 8 * 1024;
+
+ @IntDef(prefix = { "CLASS_" }, value = {
+ CLASS_IN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface QueryClass {}
+ public static final int CLASS_IN = 1;
+
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_A,
+ TYPE_AAAA
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface QueryType {}
+ public static final int TYPE_A = 1;
+ public static final int TYPE_AAAA = 28;
+
+ @IntDef(prefix = { "FLAG_" }, value = {
+ FLAG_EMPTY,
+ FLAG_NO_RETRY,
+ FLAG_NO_CACHE_STORE,
+ FLAG_NO_CACHE_LOOKUP
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface QueryFlag {}
+ public static final int FLAG_EMPTY = 0;
+ public static final int FLAG_NO_RETRY = 1 << 0;
+ public static final int FLAG_NO_CACHE_STORE = 1 << 1;
+ public static final int FLAG_NO_CACHE_LOOKUP = 1 << 2;
+
+ private static final int DNS_RAW_RESPONSE = 1;
+
+ private static final int NETID_UNSET = 0;
+
+ private static final DnsResolver sInstance = new DnsResolver();
+
+ /**
+ * listener for receiving raw answers
+ */
+ public interface RawAnswerListener {
+ /**
+ * {@code byte[]} is {@code null} if query timed out
+ */
+ void onAnswer(@Nullable byte[] answer);
+ }
+
+ /**
+ * listener for receiving parsed answers
+ */
+ public interface InetAddressAnswerListener {
+ /**
+ * Will be called exactly once with all the answers to the query.
+ * size of addresses will be zero if no available answer could be parsed.
+ */
+ void onAnswer(@NonNull List<InetAddress> addresses);
+ }
+
+ /**
+ * Get instance for DnsResolver
+ */
+ public static DnsResolver getInstance() {
+ return sInstance;
+ }
+
+ private DnsResolver() {}
+
+ /**
+ * Pass in a blob and corresponding setting,
+ * get a blob back asynchronously with the entire raw answer.
+ *
+ * @param network {@link Network} specifying which network for querying.
+ * {@code null} for query on default network.
+ * @param query blob message
+ * @param flags flags as a combination of the FLAGS_* constants
+ * @param handler {@link Handler} to specify the thread
+ * upon which the {@link RawAnswerListener} will be invoked.
+ * @param listener a {@link RawAnswerListener} which will be called to notify the caller
+ * of the result of dns query.
+ */
+ public void query(@Nullable Network network, @NonNull byte[] query, @QueryFlag int flags,
+ @NonNull Handler handler, @NonNull RawAnswerListener listener) throws ErrnoException {
+ final FileDescriptor queryfd = resNetworkSend((network != null
+ ? network.netId : NETID_UNSET), query, query.length, flags);
+ registerFDListener(handler.getLooper().getQueue(), queryfd,
+ answerbuf -> listener.onAnswer(answerbuf));
+ }
+
+ /**
+ * Pass in a domain name and corresponding setting,
+ * get a blob back asynchronously with the entire raw answer.
+ *
+ * @param network {@link Network} specifying which network for querying.
+ * {@code null} for query on default network.
+ * @param domain domain name for querying
+ * @param nsClass dns class as one of the CLASS_* constants
+ * @param nsType dns resource record (RR) type as one of the TYPE_* constants
+ * @param flags flags as a combination of the FLAGS_* constants
+ * @param handler {@link Handler} to specify the thread
+ * upon which the {@link RawAnswerListener} will be invoked.
+ * @param listener a {@link RawAnswerListener} which will be called to notify the caller
+ * of the result of dns query.
+ */
+ public void query(@Nullable Network network, @NonNull String domain, @QueryClass int nsClass,
+ @QueryType int nsType, @QueryFlag int flags,
+ @NonNull Handler handler, @NonNull RawAnswerListener listener) throws ErrnoException {
+ final FileDescriptor queryfd = resNetworkQuery((network != null
+ ? network.netId : NETID_UNSET), domain, nsClass, nsType, flags);
+ registerFDListener(handler.getLooper().getQueue(), queryfd,
+ answerbuf -> listener.onAnswer(answerbuf));
+ }
+
+ /**
+ * Pass in a domain name and corresponding setting,
+ * get back a set of InetAddresses asynchronously.
+ *
+ * @param network {@link Network} specifying which network for querying.
+ * {@code null} for query on default network.
+ * @param domain domain name for querying
+ * @param flags flags as a combination of the FLAGS_* constants
+ * @param handler {@link Handler} to specify the thread
+ * upon which the {@link InetAddressAnswerListener} will be invoked.
+ * @param listener an {@link InetAddressAnswerListener} which will be called to
+ * notify the caller of the result of dns query.
+ *
+ */
+ public void query(@Nullable Network network, @NonNull String domain, @QueryFlag int flags,
+ @NonNull Handler handler, @NonNull InetAddressAnswerListener listener)
+ throws ErrnoException {
+ final FileDescriptor v4fd = resNetworkQuery((network != null
+ ? network.netId : NETID_UNSET), domain, CLASS_IN, TYPE_A, flags);
+ final FileDescriptor v6fd = resNetworkQuery((network != null
+ ? network.netId : NETID_UNSET), domain, CLASS_IN, TYPE_AAAA, flags);
+
+ final InetAddressAnswerAccumulator accmulator =
+ new InetAddressAnswerAccumulator(2, listener);
+ final Consumer<byte[]> consumer = answerbuf ->
+ accmulator.accumulate(parseAnswers(answerbuf));
+
+ registerFDListener(handler.getLooper().getQueue(), v4fd, consumer);
+ registerFDListener(handler.getLooper().getQueue(), v6fd, consumer);
+ }
+
+ private void registerFDListener(@NonNull MessageQueue queue,
+ @NonNull FileDescriptor queryfd, @NonNull Consumer<byte[]> answerConsumer) {
+ queue.addOnFileDescriptorEventListener(
+ queryfd,
+ FD_EVENTS,
+ (fd, events) -> {
+ byte[] answerbuf = null;
+ try {
+ // TODO: Implement result function in Java side instead of using JNI
+ // Because JNI method close fd prior than unregistering fd on
+ // event listener.
+ answerbuf = resNetworkResult(fd);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "resNetworkResult:" + e.toString());
+ }
+ answerConsumer.accept(answerbuf);
+
+ // Unregister this fd listener
+ return 0;
+ });
+ }
+
+ private class DnsAddressAnswer extends DnsPacket {
+ private static final String TAG = "DnsResolver.DnsAddressAnswer";
+ private static final boolean DBG = false;
+
+ private final int mQueryType;
+
+ DnsAddressAnswer(@NonNull byte[] data) throws ParseException {
+ super(data);
+ if ((mHeader.flags & (1 << 15)) == 0) {
+ throw new ParseException("Not an answer packet");
+ }
+ if (mHeader.rcode != 0) {
+ throw new ParseException("Response error, rcode:" + mHeader.rcode);
+ }
+ if (mHeader.getSectionCount(ANSECTION) == 0) {
+ throw new ParseException("No available answer");
+ }
+ if (mHeader.getSectionCount(QDSECTION) == 0) {
+ throw new ParseException("No question found");
+ }
+ // Assume only one question per answer packet. (RFC1035)
+ mQueryType = mSections[QDSECTION].get(0).nsType;
+ }
+
+ public @NonNull List<InetAddress> getAddresses() {
+ final List<InetAddress> results = new ArrayList<InetAddress>();
+ for (final DnsSection ansSec : mSections[ANSECTION]) {
+ // Only support A and AAAA, also ignore answers if query type != answer type.
+ int nsType = ansSec.nsType;
+ if (nsType != mQueryType || (nsType != TYPE_A && nsType != TYPE_AAAA)) {
+ continue;
+ }
+ try {
+ results.add(InetAddress.getByAddress(ansSec.getRR()));
+ } catch (UnknownHostException e) {
+ if (DBG) {
+ Log.w(TAG, "rr to address fail");
+ }
+ }
+ }
+ return results;
+ }
+ }
+
+ private @Nullable List<InetAddress> parseAnswers(@Nullable byte[] data) {
+ try {
+ return (data == null) ? null : new DnsAddressAnswer(data).getAddresses();
+ } catch (DnsPacket.ParseException e) {
+ Log.e(TAG, "Parse answer fail " + e.getMessage());
+ return null;
+ }
+ }
+
+ private class InetAddressAnswerAccumulator {
+ private final List<InetAddress> mAllAnswers;
+ private final InetAddressAnswerListener mAnswerListener;
+ private final int mTargetAnswerCount;
+ private int mReceivedAnswerCount = 0;
+
+ InetAddressAnswerAccumulator(int size, @NonNull InetAddressAnswerListener listener) {
+ mTargetAnswerCount = size;
+ mAllAnswers = new ArrayList<>();
+ mAnswerListener = listener;
+ }
+
+ public void accumulate(@Nullable List<InetAddress> answer) {
+ if (null != answer) {
+ mAllAnswers.addAll(answer);
+ }
+ if (++mReceivedAnswerCount == mTargetAnswerCount) {
+ mAnswerListener.onAnswer(mAllAnswers);
+ }
+ }
+ }
+}
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index 4eab49c..c996d01 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -39,6 +39,8 @@
import java.util.Locale;
import java.util.TreeSet;
+import android.system.ErrnoException;
+
/**
* Native methods for managing network interfaces.
*
@@ -138,6 +140,32 @@
public native static boolean queryUserAccess(int uid, int netId);
/**
+ * DNS resolver series jni method.
+ * Issue the query {@code msg} on the network designated by {@code netId}.
+ * {@code flags} is an additional config to control actual querying behavior.
+ * @return a file descriptor to watch for read events
+ */
+ public static native FileDescriptor resNetworkSend(
+ int netId, byte[] msg, int msglen, int flags) throws ErrnoException;
+
+ /**
+ * DNS resolver series jni method.
+ * Look up the {@code nsClass} {@code nsType} Resource Record (RR) associated
+ * with Domain Name {@code dname} on the network designated by {@code netId}.
+ * {@code flags} is an additional config to control actual querying behavior.
+ * @return a file descriptor to watch for read events
+ */
+ public static native FileDescriptor resNetworkQuery(
+ int netId, String dname, int nsClass, int nsType, int flags) throws ErrnoException;
+
+ /**
+ * DNS resolver series jni method.
+ * Read a result for the query associated with the {@code fd}.
+ * @return a byte array containing blob answer
+ */
+ public static native byte[] resNetworkResult(FileDescriptor fd) throws ErrnoException;
+
+ /**
* Add an entry into the ARP cache.
*/
public static void addArpEntry(Inet4Address ipv4Addr, MacAddress ethAddr, String ifname,
diff --git a/core/java/android/net/ipmemorystore/NetworkAttributes.java b/core/java/android/net/ipmemorystore/NetworkAttributes.java
index 5397b57..6a9eae0 100644
--- a/core/java/android/net/ipmemorystore/NetworkAttributes.java
+++ b/core/java/android/net/ipmemorystore/NetworkAttributes.java
@@ -252,6 +252,12 @@
}
}
+ /** @hide */
+ public boolean isEmpty() {
+ return (null == assignedV4Address) && (null == groupHint)
+ && (null == dnsAddresses) && (null == mtu);
+ }
+
@Override
public boolean equals(@Nullable final Object o) {
if (!(o instanceof NetworkAttributes)) return false;
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index ca39051..51c3c4c 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -72,6 +72,7 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Comparator;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -831,6 +832,16 @@
return false;
}
+ /** {@hide} */
+ public static boolean contains(Collection<File> dirs, File file) {
+ for (File dir : dirs) {
+ if (contains(dir, file)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Test if a file lives under the given directory, either as a direct child
* or a distant grandchild.
diff --git a/core/java/android/os/IDeviceIdleController.aidl b/core/java/android/os/IDeviceIdleController.aidl
index 8271701..fe17c6b 100644
--- a/core/java/android/os/IDeviceIdleController.aidl
+++ b/core/java/android/os/IDeviceIdleController.aidl
@@ -45,4 +45,6 @@
void exitIdle(String reason);
boolean registerMaintenanceActivityListener(IMaintenanceActivityListener listener);
void unregisterMaintenanceActivityListener(IMaintenanceActivityListener listener);
+ int setPreIdleTimeoutMode(int Mode);
+ void resetPreIdleTimeoutMode();
}
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 0942d97..4ce760f 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -1724,6 +1724,25 @@
= "android.os.action.SCREEN_BRIGHTNESS_BOOST_CHANGED";
/**
+ * Constant for PreIdleTimeout normal mode (default mode, not short nor extend timeout) .
+ * @hide
+ */
+ public static final int PRE_IDLE_TIMEOUT_MODE_NORMAL = 0;
+
+ /**
+ * Constant for PreIdleTimeout long mode (extend timeout to keep in inactive mode
+ * longer).
+ * @hide
+ */
+ public static final int PRE_IDLE_TIMEOUT_MODE_LONG = 1;
+
+ /**
+ * Constant for PreIdleTimeout short mode (short timeout to go to doze mode quickly)
+ * @hide
+ */
+ public static final int PRE_IDLE_TIMEOUT_MODE_SHORT = 2;
+
+ /**
* A wake lock is a mechanism to indicate that your application needs
* to have the device stay on.
* <p>
diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java
index df1a713..714a061 100644
--- a/core/java/android/os/storage/StorageVolume.java
+++ b/core/java/android/os/storage/StorageVolume.java
@@ -35,6 +35,7 @@
import java.io.CharArrayWriter;
import java.io.File;
+import java.util.Locale;
/**
* Information about a shared/external storage volume for a specific user.
@@ -263,6 +264,11 @@
return mFsUuid;
}
+ /** {@hide} */
+ public @Nullable String getNormalizedUuid() {
+ return mFsUuid != null ? mFsUuid.toLowerCase(Locale.US) : null;
+ }
+
/**
* Parse and return volume UUID as FAT volume ID, or return -1 if unable to
* parse or UUID is unknown.
diff --git a/core/java/android/os/storage/VolumeInfo.java b/core/java/android/os/storage/VolumeInfo.java
index 8c3aa17..5d310e1 100644
--- a/core/java/android/os/storage/VolumeInfo.java
+++ b/core/java/android/os/storage/VolumeInfo.java
@@ -42,6 +42,7 @@
import java.io.CharArrayWriter;
import java.io.File;
import java.util.Comparator;
+import java.util.Locale;
import java.util.Objects;
/**
@@ -254,6 +255,10 @@
return fsUuid;
}
+ public @Nullable String getNormalizedFsUuid() {
+ return fsUuid != null ? fsUuid.toLowerCase(Locale.US) : null;
+ }
+
@UnsupportedAppUsage
public int getMountUserId() {
return mountUserId;
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index cd3efb4..87efbf3 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -103,6 +103,24 @@
@SystemApi
public static final String NAMESPACE_NOTIFICATION_ASSISTANT = "notification_assistant";
+ /**
+ * Telephony related properties definitions.
+ *
+ * @hide
+ */
+ @SystemApi
+ public interface Telephony {
+ String NAMESPACE = "telephony";
+ /**
+ * Whether to apply ramping ringer on incoming phone calls.
+ */
+ String PROPERTY_ENABLE_RAMPING_RINGER = "enable_ramping_ringer";
+ /**
+ * Ringer ramping time in milliseconds.
+ */
+ String PROPERTY_RAMPING_RINGER_DURATION = "ramping_duration";
+ }
+
private static final Object sLock = new Object();
@GuardedBy("sLock")
private static Map<OnPropertyChangedListener, Pair<String, Executor>> sListeners =
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
index 3a49986..487198b 100644
--- a/core/java/android/provider/MediaStore.java
+++ b/core/java/android/provider/MediaStore.java
@@ -70,6 +70,8 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -1224,7 +1226,7 @@
if (sv.isPrimary()) {
return VOLUME_EXTERNAL;
} else {
- return checkArgumentVolumeName(sv.getUuid());
+ return checkArgumentVolumeName(sv.getNormalizedUuid());
}
}
throw new IllegalStateException("Unknown volume at " + path);
@@ -2919,7 +2921,7 @@
if (vi.isPrimary()) {
volumeNames.add(VOLUME_EXTERNAL);
} else {
- volumeNames.add(vi.getFsUuid());
+ volumeNames.add(vi.getNormalizedFsUuid());
}
}
}
@@ -2953,8 +2955,7 @@
// When not one of the well-known values above, it must be a hex UUID
for (int i = 0; i < volumeName.length(); i++) {
final char c = volumeName.charAt(i);
- if (('a' <= c && c <= 'f') || ('A' <= c && c <= 'F')
- || ('0' <= c && c <= '9') || (c == '-')) {
+ if (('a' <= c && c <= 'f') || ('0' <= c && c <= '9') || (c == '-')) {
continue;
} else {
throw new IllegalArgumentException("Invalid volume name: " + volumeName);
@@ -2963,23 +2964,26 @@
return volumeName;
}
- /** {@hide} */
+ /**
+ * Return path where the given volume is mounted. Not valid for
+ * {@link #VOLUME_INTERNAL}.
+ *
+ * @hide
+ */
public static @NonNull File getVolumePath(@NonNull String volumeName)
throws FileNotFoundException {
if (TextUtils.isEmpty(volumeName)) {
throw new IllegalArgumentException();
}
- if (VOLUME_INTERNAL.equals(volumeName)) {
- return Environment.getDataDirectory();
- } else if (VOLUME_EXTERNAL.equals(volumeName)) {
+ if (VOLUME_EXTERNAL.equals(volumeName)) {
return Environment.getExternalStorageDirectory();
}
final StorageManager sm = AppGlobals.getInitialApplication()
.getSystemService(StorageManager.class);
for (VolumeInfo vi : sm.getVolumes()) {
- if (Objects.equals(vi.getFsUuid(), volumeName)) {
+ if (Objects.equals(vi.getNormalizedFsUuid(), volumeName)) {
final File path = vi.getPathForUser(UserHandle.myUserId());
if (path != null) {
return path;
@@ -2992,6 +2996,33 @@
}
/**
+ * Return paths that should be scanned for the given volume.
+ *
+ * @hide
+ */
+ public static @NonNull Collection<File> getVolumeScanPaths(@NonNull String volumeName)
+ throws FileNotFoundException {
+ if (TextUtils.isEmpty(volumeName)) {
+ throw new IllegalArgumentException();
+ }
+
+ final ArrayList<File> res = new ArrayList<>();
+ if (VOLUME_INTERNAL.equals(volumeName)) {
+ res.add(new File(Environment.getRootDirectory(), "media"));
+ res.add(new File(Environment.getOemDirectory(), "media"));
+ res.add(new File(Environment.getProductDirectory(), "media"));
+ } else {
+ res.add(getVolumePath(volumeName));
+ final UserManager um = AppGlobals.getInitialApplication()
+ .getSystemService(UserManager.class);
+ if (VOLUME_EXTERNAL.equals(volumeName) && um.isDemoUser()) {
+ res.add(Environment.getDataPreloadsMediaDirectory());
+ }
+ }
+ return res;
+ }
+
+ /**
* Uri for querying the state of the media scanner.
*/
public static Uri getMediaScannerUri() {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index e54c2bf..c9bb3cf 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -11244,8 +11244,6 @@
* service_max_inactivity (long)
* service_bg_start_timeout (long)
* process_start_async (boolean)
- * use_mem_aware_service_restarts (boolean)
- * service_restart_delay_duration (long)
* </pre>
*
* <p>
diff --git a/core/java/android/service/euicc/EuiccService.java b/core/java/android/service/euicc/EuiccService.java
index 4dc10cd..ffb524d 100644
--- a/core/java/android/service/euicc/EuiccService.java
+++ b/core/java/android/service/euicc/EuiccService.java
@@ -103,10 +103,23 @@
*/
public static final String ACTION_MANAGE_EMBEDDED_SUBSCRIPTIONS =
"android.service.euicc.action.MANAGE_EMBEDDED_SUBSCRIPTIONS";
+
/** @see android.telephony.euicc.EuiccManager#ACTION_PROVISION_EMBEDDED_SUBSCRIPTION */
public static final String ACTION_PROVISION_EMBEDDED_SUBSCRIPTION =
"android.service.euicc.action.PROVISION_EMBEDDED_SUBSCRIPTION";
+ /** @see android.telephony.euicc.EuiccManager#ACTION_TOGGLE_SUBSCRIPTION_PRIVILEGED */
+ public static final String ACTION_TOGGLE_SUBSCRIPTION_PRIVILEGED =
+ "android.service.euicc.action.TOGGLE_SUBSCRIPTION_PRIVILEGED";
+
+ /** @see android.telephony.euicc.EuiccManager#ACTION_DELETE_SUBSCRIPTION_PRIVILEGED */
+ public static final String ACTION_DELETE_SUBSCRIPTION_PRIVILEGED =
+ "android.service.euicc.action.DELETE_SUBSCRIPTION_PRIVILEGED";
+
+ /** @see android.telephony.euicc.EuiccManager#ACTION_RENAME_SUBSCRIPTION_PRIVILEGED */
+ public static final String ACTION_RENAME_SUBSCRIPTION_PRIVILEGED =
+ "android.service.euicc.action.RENAME_SUBSCRIPTION_PRIVILEGED";
+
// LUI resolution actions. These are called by the platform to resolve errors in situations that
// require user interaction.
// TODO(b/33075886): Define extras for any input parameters to these dialogs once they are
diff --git a/core/java/android/service/vr/IVrManager.aidl b/core/java/android/service/vr/IVrManager.aidl
index f7acfc5..b0269e3 100644
--- a/core/java/android/service/vr/IVrManager.aidl
+++ b/core/java/android/service/vr/IVrManager.aidl
@@ -110,13 +110,5 @@
* @param standy True if the device is entering standby, false if it's exiting standby.
*/
void setStandbyEnabled(boolean standby);
-
- /**
- * Start VR Input method for the given packageName in {@param componentName}.
- * This method notifies InputMethodManagerService to use VR IME instead of
- * regular phone IME.
- */
- void setVrInputMethod(in ComponentName componentName);
-
}
diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java
index 33b3ff4f..7d9ec70 100644
--- a/core/java/android/view/GestureDetector.java
+++ b/core/java/android/view/GestureDetector.java
@@ -237,6 +237,16 @@
private static final int LONG_PRESS = 2;
private static final int TAP = 3;
+ /**
+ * If a MotionEvent has CLASSIFICATION_AMBIGUOUS_GESTURE set, then certain actions, such as
+ * scrolling, will be inhibited. However, to account for the possibility of incorrect
+ * classification, the default scrolling will only be inhibited if the gesture moves beyond
+ * (default touch slop * AMBIGUOUS_GESTURE_MULTIPLIER). Likewise, the default long press
+ * timeout will be increased for some situations where the default behaviour
+ * is to cancel it.
+ */
+ private static final int AMBIGUOUS_GESTURE_MULTIPLIER = 2;
+
private final Handler mHandler;
@UnsupportedAppUsage
private final OnGestureListener mListener;
@@ -292,27 +302,27 @@
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
- case SHOW_PRESS:
- mListener.onShowPress(mCurrentDownEvent);
- break;
-
- case LONG_PRESS:
- dispatchLongPress();
- break;
-
- case TAP:
- // If the user's finger is still down, do not count it as a tap
- if (mDoubleTapListener != null) {
- if (!mStillDown) {
- mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
- } else {
- mDeferConfirmSingleTap = true;
- }
- }
- break;
+ case SHOW_PRESS:
+ mListener.onShowPress(mCurrentDownEvent);
+ break;
- default:
- throw new RuntimeException("Unknown message " + msg); //never
+ case LONG_PRESS:
+ dispatchLongPress();
+ break;
+
+ case TAP:
+ // If the user's finger is still down, do not count it as a tap
+ if (mDoubleTapListener != null) {
+ if (!mStillDown) {
+ mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
+ } else {
+ mDeferConfirmSingleTap = true;
+ }
+ }
+ break;
+
+ default:
+ throw new RuntimeException("Unknown message " + msg); //never
}
}
}
@@ -427,7 +437,7 @@
if (context == null) {
//noinspection deprecation
touchSlop = ViewConfiguration.getTouchSlop();
- doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this
+ doubleTapTouchSlop = touchSlop; // Hack rather than adding a hidden method for this
doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
//noinspection deprecation
mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
@@ -605,6 +615,10 @@
if (mInLongPress || mInContextClick) {
break;
}
+
+ final int motionClassification = ev.getClassification();
+ final boolean hasPendingLongPress = mHandler.hasMessages(LONG_PRESS);
+
final float scrollX = mLastFocusX - focusX;
final float scrollY = mLastFocusY - focusY;
if (mIsDoubleTapping) {
@@ -615,6 +629,31 @@
final int deltaY = (int) (focusY - mDownFocusY);
int distance = (deltaX * deltaX) + (deltaY * deltaY);
int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
+
+ final boolean ambiguousGesture =
+ motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
+ final boolean shouldInhibitDefaultAction =
+ hasPendingLongPress && ambiguousGesture;
+ if (shouldInhibitDefaultAction) {
+ // Inhibit default long press
+ if (distance > slopSquare) {
+ // The default action here is to remove long press. But if the touch
+ // slop below gets increased, and we never exceed the modified touch
+ // slop while still receiving AMBIGUOUS_GESTURE, we risk that *nothing*
+ // will happen in response to user input. To prevent this,
+ // reschedule long press with a modified timeout.
+ mHandler.removeMessages(LONG_PRESS);
+ final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
+ mHandler.sendEmptyMessageAtTime(LONG_PRESS, ev.getDownTime()
+ + longPressTimeout * AMBIGUOUS_GESTURE_MULTIPLIER);
+ }
+ // Inhibit default scroll. If a gesture is ambiguous, we prevent scroll
+ // until the gesture is resolved.
+ // However, for safety, simply increase the touch slop in case the
+ // classification is erroneous. Since the value is squared, multiply twice.
+ slopSquare *= AMBIGUOUS_GESTURE_MULTIPLIER * AMBIGUOUS_GESTURE_MULTIPLIER;
+ }
+
if (distance > slopSquare) {
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
@@ -633,6 +672,12 @@
mLastFocusX = focusX;
mLastFocusY = focusY;
}
+ final boolean deepPress =
+ motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
+ if (deepPress && hasPendingLongPress) {
+ mHandler.removeMessages(LONG_PRESS);
+ mHandler.sendEmptyMessage(LONG_PRESS);
+ }
break;
case MotionEvent.ACTION_UP:
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index 330d72f..42ac880 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -51,6 +51,7 @@
import android.view.AppTransitionAnimationSpec;
import android.view.WindowContentFrameStats;
import android.view.WindowManager;
+import android.view.SurfaceControl;
/**
* System private interface to the window manager.
@@ -555,8 +556,8 @@
* display content info to any SurfaceControl, as this would be a security issue.
*
* @param displayId The id of the display.
- * @param surfaceControlHandle The SurfaceControl handle that the top level layers for the
+ * @param surfaceControlHandle The SurfaceControl that the top level layers for the
* display should be re-parented to.
*/
- void reparentDisplayContent(int displayId, in IBinder surfaceControlHandle);
+ void reparentDisplayContent(int displayId, in SurfaceControl sc);
}
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 7c1465b..7291d0b 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -122,7 +122,8 @@
mController.scheduleApplyChangeInsets();
}
- void applyChangeInsets(InsetsState state) {
+ @VisibleForTesting
+ public void applyChangeInsets(InsetsState state) {
final Insets offset = Insets.subtract(mShownInsets, mPendingInsets);
ArrayList<SurfaceParams> params = new ArrayList<>();
if (offset.left != 0) {
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 4b1d1ec..dd6231d 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -231,15 +231,18 @@
mState.dump(prefix + " ", pw);
}
- void dispatchAnimationStarted(WindowInsetsAnimationListener.InsetsAnimation animation) {
+ @VisibleForTesting
+ public void dispatchAnimationStarted(WindowInsetsAnimationListener.InsetsAnimation animation) {
mViewRoot.mView.dispatchWindowInsetsAnimationStarted(animation);
}
- void dispatchAnimationFinished(WindowInsetsAnimationListener.InsetsAnimation animation) {
+ @VisibleForTesting
+ public void dispatchAnimationFinished(WindowInsetsAnimationListener.InsetsAnimation animation) {
mViewRoot.mView.dispatchWindowInsetsAnimationFinished(animation);
}
- void scheduleApplyChangeInsets() {
+ @VisibleForTesting
+ public void scheduleApplyChangeInsets() {
if (!mAnimCallbackScheduled) {
mViewRoot.mChoreographer.postCallback(Choreographer.CALLBACK_INSETS_ANIMATION,
mAnimCallback, null /* token*/);
diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java
index f3cb376..7fcb2af 100644
--- a/core/java/android/view/Surface.java
+++ b/core/java/android/view/Surface.java
@@ -185,6 +185,18 @@
}
/**
+ * Create a Surface assosciated with a given {@link SurfaceControl}. Buffers submitted to this
+ * surface will be displayed by the system compositor according to the parameters
+ * specified by the control. Multiple surfaces may be constructed from one SurfaceControl,
+ * but only one can be connected (e.g. have an active EGL context) at a time.
+ *
+ * @param from The SurfaceControl to assosciate this Surface with
+ */
+ public Surface(SurfaceControl from) {
+ copyFrom(from);
+ }
+
+ /**
* Create Surface from a {@link SurfaceTexture}.
*
* Images drawn to the Surface will be made available to the {@link
@@ -494,7 +506,6 @@
* in to it.
*
* @param other {@link SurfaceControl} to copy from.
- *
* @hide
*/
@UnsupportedAppUsage
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 5e98236..863b717 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -27,6 +27,10 @@
import static android.view.SurfaceControlProto.HASH_CODE;
import static android.view.SurfaceControlProto.NAME;
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.Size;
import android.annotation.UnsupportedAppUsage;
import android.graphics.Bitmap;
@@ -58,10 +62,16 @@
import java.io.Closeable;
/**
- * SurfaceControl
- * @hide
+ * Handle to an on-screen Surface managed by the system compositor. The SurfaceControl is
+ * a combination of a buffer source, and metadata about how to display the buffers.
+ * By constructing a {@link Surface} from this SurfaceControl you can submit buffers to be
+ * composited. Using {@link SurfaceControl.Transaction} you can manipulate various
+ * properties of how the buffer will be displayed on-screen. SurfaceControl's are
+ * arranged into a scene-graph like hierarchy, and as such any SurfaceControl may have
+ * a parent. Geometric properties like transform, crop, and Z-ordering will be inherited
+ * from the parent, as if the child were content in the parents buffer stream.
*/
-public class SurfaceControl implements Parcelable {
+public final class SurfaceControl implements Parcelable {
private static final String TAG = "SurfaceControl";
private static native long nativeCreate(SurfaceSession session, String name,
@@ -103,6 +113,8 @@
float dtdy, float dsdy);
private static native void nativeSetColorTransform(long transactionObj, long nativeObject,
float[] matrix, float[] translation);
+ private static native void nativeSetGeometry(long transactionObj, long nativeObject,
+ Rect sourceCrop, Rect dest, long orientation);
private static native void nativeSetColor(long transactionObj, long nativeObject, float[] color);
private static native void nativeSetFlags(long transactionObj, long nativeObject,
int flags, int mask);
@@ -156,7 +168,7 @@
private static native void nativeReparentChildren(long transactionObj, long nativeObject,
IBinder handle);
private static native void nativeReparent(long transactionObj, long nativeObject,
- IBinder parentHandle);
+ long newParentNativeObject);
private static native void nativeSeverChildren(long transactionObj, long nativeObject);
private static native void nativeSetOverrideScalingMode(long transactionObj, long nativeObject,
int scalingMode);
@@ -331,8 +343,7 @@
*/
public static final int BUILT_IN_DISPLAY_ID_HDMI = 1;
- /* Display power modes * /
-
+ // Display power modes.
/**
* Display power mode off: used while blanking the screen.
* Use only with {@link SurfaceControl#setDisplayPowerMode}.
@@ -403,7 +414,6 @@
/**
* Builder class for {@link SurfaceControl} objects.
- * @hide
*/
public static class Builder {
private SurfaceSession mSession;
@@ -427,8 +437,14 @@
}
/**
- * Construct a new {@link SurfaceControl} with the set parameters.
- * @hide
+ * Begin building a SurfaceControl.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Construct a new {@link SurfaceControl} with the set parameters. The builder
+ * remains valid.
*/
public SurfaceControl build() {
if (mWidth < 0 || mHeight < 0) {
@@ -447,7 +463,6 @@
* Set a debugging-name for the SurfaceControl.
*
* @param name A name to identify the Surface in debugging.
- * @hide
*/
public Builder setName(String name) {
mName = name;
@@ -459,9 +474,9 @@
*
* @param width The buffer width in pixels.
* @param height The buffer height in pixels.
- * @hide
*/
- public Builder setBufferSize(int width, int height) {
+ public Builder setBufferSize(@IntRange(from = 0) int width,
+ @IntRange(from = 0) int height) {
if (width < 0 || height < 0) {
throw new IllegalArgumentException(
"width and height must be positive");
@@ -474,8 +489,8 @@
/**
* Set the pixel format of the controlled surface's buffers, using constants from
* {@link android.graphics.PixelFormat}.
- * @hide
*/
+ @NonNull
public Builder setFormat(@PixelFormat.Format int format) {
mFormat = format;
return this;
@@ -490,6 +505,7 @@
* @param protectedContent Whether to require a protected sink.
* @hide
*/
+ @NonNull
public Builder setProtected(boolean protectedContent) {
if (protectedContent) {
mFlags |= PROTECTED_APP;
@@ -506,6 +522,7 @@
* not a complete prevention of readback as {@link #setProtected}.
* @hide
*/
+ @NonNull
public Builder setSecure(boolean secure) {
if (secure) {
mFlags |= SECURE;
@@ -537,8 +554,8 @@
* If the underlying buffer lacks an alpha channel, it is as if setOpaque(true)
* were set automatically.
* @param opaque Whether the Surface is OPAQUE.
- * @hide
*/
+ @NonNull
public Builder setOpaque(boolean opaque) {
if (opaque) {
mFlags |= OPAQUE;
@@ -556,9 +573,9 @@
* of the parent.
*
* @param parent The parent control.
- * @hide
*/
- public Builder setParent(SurfaceControl parent) {
+ @NonNull
+ public Builder setParent(@Nullable SurfaceControl parent) {
mParent = parent;
return this;
}
@@ -673,9 +690,6 @@
private SurfaceControl(SurfaceSession session, String name, int w, int h, int format, int flags,
SurfaceControl parent, int windowType, int ownerUid)
throws OutOfResourcesException, IllegalArgumentException {
- if (session == null) {
- throw new IllegalArgumentException("session must not be null");
- }
if (name == null) {
throw new IllegalArgumentException("name must not be null");
}
@@ -729,9 +743,6 @@
mCloseGuard.open("release");
}
- /**
- * @hide
- */
public void readFromParcel(Parcel in) {
if (in == null) {
throw new IllegalArgumentException("source must not be null");
@@ -748,17 +759,11 @@
assignNativeObject(object);
}
- /**
- * @hide
- */
@Override
public int describeContents() {
return 0;
}
- /**
- * @hide
- */
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mName);
@@ -791,9 +796,6 @@
proto.end(token);
}
- /**
- * @hide
- */
public static final Creator<SurfaceControl> CREATOR
= new Creator<SurfaceControl>() {
public SurfaceControl createFromParcel(Parcel in) {
@@ -823,10 +825,12 @@
}
/**
- * Release the local reference to the server-side surface.
- * Always call release() when you're done with a Surface.
- * This will make the surface invalid.
- * @hide
+ * Release the local reference to the server-side surface. The surface
+ * may continue to exist on-screen as long as its parent continues
+ * to exist. To explicitly remove a surface from the screen use
+ * {@link Transaction#reparent} with a null-parent.
+ *
+ * Always call release() when you're done with a SurfaceControl.
*/
public void release() {
if (mNativeObject != 0) {
@@ -866,7 +870,10 @@
}
/**
- * @hide
+ * Check whether this instance points to a valid layer with the system-compositor. For
+ * example this may be false if construction failed, or the layer was released.
+ *
+ * @return Whether this SurfaceControl is valid.
*/
public boolean isValid() {
return mNativeObject != 0;
@@ -962,9 +969,9 @@
/**
* @hide
*/
- public void reparent(IBinder newParentHandle) {
+ public void reparent(SurfaceControl newParent) {
synchronized(SurfaceControl.class) {
- sGlobalTransaction.reparent(this, newParentHandle);
+ sGlobalTransaction.reparent(this, newParent);
}
}
@@ -1270,9 +1277,6 @@
}
}
- /**
- * @hide
- */
@Override
public String toString() {
return "Surface(name=" + mName + ")/@0x" +
@@ -1286,6 +1290,7 @@
/**
* Describes the properties of a physical display known to surface flinger.
+ * @hide
*/
public static final class PhysicalDisplayInfo {
/**
@@ -1777,9 +1782,12 @@
}
/**
- * @hide
+ * An atomic set of changes to a set of SurfaceControl.
*/
public static class Transaction implements Closeable {
+ /**
+ * @hide
+ */
public static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
Transaction.class.getClassLoader(),
nativeGetNativeTransactionFinalizer(), 512);
@@ -1789,9 +1797,12 @@
Runnable mFreeNativeResources;
/**
- * @hide
+ * Open a new transaction object. The transaction may be filed with commands to
+ * manipulate {@link SurfaceControl} instances, and then applied atomically with
+ * {@link #apply}. Eventually the user should invoke {@link #close}, when the object
+ * is no longer required. Note however that re-using a transaction after a call to apply
+ * is allowed as a convenience.
*/
- @UnsupportedAppUsage
public Transaction() {
mNativeObject = nativeCreateTransaction();
mFreeNativeResources
@@ -1801,9 +1812,7 @@
/**
* Apply the transaction, clearing it's state, and making it usable
* as a new transaction.
- * @hide
*/
- @UnsupportedAppUsage
public void apply() {
apply(false);
}
@@ -1811,7 +1820,6 @@
/**
* Close the transaction, if the transaction was not already applied this will cancel the
* transaction.
- * @hide
*/
@Override
public void close() {
@@ -1841,6 +1849,27 @@
}
/**
+ * Toggle the visibility of a given Layer and it's sub-tree.
+ *
+ * @param sc The SurfaceControl for which to set the visibility
+ * @param visible The new visibility
+ * @return This transaction object.
+ */
+ @NonNull
+ public Transaction setVisibility(@NonNull SurfaceControl sc, boolean visible) {
+ sc.checkNotReleased();
+ if (visible) {
+ return show(sc);
+ } else {
+ return hide(sc);
+ }
+ }
+
+ /**
+ * Request that a given surface and it's sub-tree be shown.
+ *
+ * @param sc The surface to show.
+ * @return This transaction.
* @hide
*/
@UnsupportedAppUsage
@@ -1851,6 +1880,10 @@
}
/**
+ * Request that a given surface and it's sub-tree be hidden.
+ *
+ * @param sc The surface to hidden.
+ * @return This transaction.
* @hide
*/
@UnsupportedAppUsage
@@ -1871,10 +1904,17 @@
}
/**
- * @hide
+ * Set the default buffer size for the SurfaceControl, if there is an
+ * {@link Surface} assosciated with the control, then
+ * this will be the default size for buffers dequeued from it.
+ * @param sc The surface to set the buffer size for.
+ * @param w The default width
+ * @param h The default height
+ * @return This Transaction
*/
- @UnsupportedAppUsage
- public Transaction setBufferSize(SurfaceControl sc, int w, int h) {
+ @NonNull
+ public Transaction setBufferSize(@NonNull SurfaceControl sc,
+ @IntRange(from = 0) int w, @IntRange(from = 0) int h) {
sc.checkNotReleased();
mResizedSurfaces.put(sc, new Point(w, h));
nativeSetSize(mNativeObject, sc.mNativeObject, w, h);
@@ -1882,10 +1922,17 @@
}
/**
- * @hide
+ * Set the Z-order for a given SurfaceControl, relative to it's siblings.
+ * If two siblings share the same Z order the ordering is undefined. Surfaces
+ * with a negative Z will be placed below the parent surface.
+ *
+ * @param sc The SurfaceControl to set the Z order on
+ * @param z The Z-order
+ * @return This Transaction.
*/
- @UnsupportedAppUsage
- public Transaction setLayer(SurfaceControl sc, int z) {
+ @NonNull
+ public Transaction setLayer(@NonNull SurfaceControl sc,
+ @IntRange(from = Integer.MIN_VALUE, to = Integer.MAX_VALUE) int z) {
sc.checkNotReleased();
nativeSetLayer(mNativeObject, sc.mNativeObject, z);
return this;
@@ -1912,10 +1959,15 @@
}
/**
- * @hide
+ * Set the alpha for a given surface. If the alpha is non-zero the SurfaceControl
+ * will be blended with the Surfaces under it according to the specified ratio.
+ *
+ * @param sc The given SurfaceControl.
+ * @param alpha The alpha to set.
*/
- @UnsupportedAppUsage
- public Transaction setAlpha(SurfaceControl sc, float alpha) {
+ @NonNull
+ public Transaction setAlpha(@NonNull SurfaceControl sc,
+ @FloatRange(from = 0.0, to = 1.0) float alpha) {
sc.checkNotReleased();
nativeSetAlpha(mNativeObject, sc.mNativeObject, alpha);
return this;
@@ -1947,6 +1999,25 @@
}
/**
+ * Specify how the buffer assosciated with this Surface is mapped in to the
+ * parent coordinate space. The source frame will be scaled to fit the destination
+ * frame, after being rotated according to the orientation parameter.
+ *
+ * @param sc The SurfaceControl to specify the geometry of
+ * @param sourceCrop The source rectangle in buffer space. Or null for the entire buffer.
+ * @param destFrame The destination rectangle in parent space. Or null for the source frame.
+ * @param orientation The buffer rotation
+ * @return This transaction object.
+ */
+ @NonNull
+ public Transaction setGeometry(@NonNull SurfaceControl sc, @Nullable Rect sourceCrop,
+ @Nullable Rect destFrame, @Surface.Rotation int orientation) {
+ sc.checkNotReleased();
+ nativeSetGeometry(mNativeObject, sc.mNativeObject, sourceCrop, destFrame, orientation);
+ return this;
+ }
+
+ /**
* @hide
*/
@UnsupportedAppUsage
@@ -2023,20 +2094,20 @@
return this;
}
- @UnsupportedAppUsage
/**
* @hide
*/
+ @UnsupportedAppUsage
public Transaction setLayerStack(SurfaceControl sc, int layerStack) {
sc.checkNotReleased();
nativeSetLayerStack(mNativeObject, sc.mNativeObject, layerStack);
return this;
}
- @UnsupportedAppUsage
/**
* @hide
*/
+ @UnsupportedAppUsage
public Transaction deferTransactionUntil(SurfaceControl sc, IBinder handle,
long frameNumber) {
if (frameNumber < 0) {
@@ -2047,10 +2118,10 @@
return this;
}
- @UnsupportedAppUsage
/**
* @hide
*/
+ @UnsupportedAppUsage
public Transaction deferTransactionUntilSurface(SurfaceControl sc, Surface barrierSurface,
long frameNumber) {
if (frameNumber < 0) {
@@ -2071,13 +2142,25 @@
return this;
}
- /** Re-parents a specific child layer to a new parent
- * @hide
+ /**
+ * Re-parents a given layer to a new parent. Children inherit transform (position, scaling)
+ * crop, visibility, and Z-ordering from their parents, as if the children were pixels within the
+ * parent Surface.
+ *
+ * @param sc The SurfaceControl to reparent
+ * @param newParent The new parent for the given control.
+ * @return This Transaction
*/
- public Transaction reparent(SurfaceControl sc, IBinder newParentHandle) {
+ @NonNull
+ public Transaction reparent(@NonNull SurfaceControl sc,
+ @Nullable SurfaceControl newParent) {
sc.checkNotReleased();
- nativeReparent(mNativeObject, sc.mNativeObject,
- newParentHandle);
+ long otherObject = 0;
+ if (newParent != null) {
+ newParent.checkNotReleased();
+ otherObject = newParent.mNativeObject;
+ }
+ nativeReparent(mNativeObject, sc.mNativeObject, otherObject);
return this;
}
@@ -2245,9 +2328,12 @@
/**
* Merge the other transaction into this transaction, clearing the
* other transaction as if it had been applied.
- * @hide
+ *
+ * @param other The transaction to merge in to this one.
+ * @return This transaction.
*/
- public Transaction merge(Transaction other) {
+ @NonNull
+ public Transaction merge(@NonNull Transaction other) {
mResizedSurfaces.putAll(other.mResizedSurfaces);
other.mResizedSurfaces.clear();
nativeMergeTransaction(mNativeObject, other.mNativeObject);
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 61fb00d..45e6c50 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -120,10 +120,11 @@
final Rect mScreenRect = new Rect();
SurfaceSession mSurfaceSession;
- SurfaceControlWithBackground mSurfaceControl;
+ SurfaceControl mSurfaceControl;
// In the case of format changes we switch out the surface in-place
// we need to preserve the old one until the new one has drawn.
SurfaceControl mDeferredDestroySurfaceControl;
+ SurfaceControl mBackgroundControl;
final Rect mTmpRect = new Rect();
final Configuration mConfiguration = new Configuration();
@@ -487,6 +488,29 @@
}
}
+ private void updateBackgroundVisibilityInTransaction() {
+ if (mBackgroundControl == null) {
+ return;
+ }
+ if ((mSurfaceFlags & PixelFormat.OPAQUE) == 0) {
+ mBackgroundControl.show();
+ mBackgroundControl.setLayer(Integer.MIN_VALUE);
+ } else {
+ mBackgroundControl.hide();
+ }
+ }
+
+ private void releaseSurfaces() {
+ if (mSurfaceControl != null) {
+ mSurfaceControl.destroy();
+ mSurfaceControl = null;
+ }
+ if (mBackgroundControl != null) {
+ mBackgroundControl.destroy();
+ mBackgroundControl = null;
+ }
+ }
+
/** @hide */
protected void updateSurface() {
if (!mHaveFrame) {
@@ -553,14 +577,21 @@
updateOpaqueFlag();
final String name = "SurfaceView - " + viewRoot.getTitle().toString();
- mSurfaceControl = new SurfaceControlWithBackground(
- name,
- (mSurfaceFlags & SurfaceControl.OPAQUE) != 0,
- new SurfaceControl.Builder(mSurfaceSession)
- .setBufferSize(mSurfaceWidth, mSurfaceHeight)
- .setFormat(mFormat)
- .setParent(viewRoot.getSurfaceControl())
- .setFlags(mSurfaceFlags));
+ mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
+ .setName(name)
+ .setOpaque((mSurfaceFlags & SurfaceControl.OPAQUE) != 0)
+ .setBufferSize(mSurfaceWidth, mSurfaceHeight)
+ .setFormat(mFormat)
+ .setParent(viewRoot.getSurfaceControl())
+ .setFlags(mSurfaceFlags)
+ .build();
+ mBackgroundControl = new SurfaceControl.Builder(mSurfaceSession)
+ .setName("Background for -" + name)
+ .setOpaque(true)
+ .setColorLayer(true)
+ .setParent(mSurfaceControl)
+ .build();
+
} else if (mSurfaceControl == null) {
return;
}
@@ -577,11 +608,13 @@
SurfaceControl.openTransaction();
try {
mSurfaceControl.setLayer(mSubLayer);
+
if (mViewVisibility) {
mSurfaceControl.show();
} else {
mSurfaceControl.hide();
}
+ updateBackgroundVisibilityInTransaction();
// While creating the surface, we will set it's initial
// geometry. Outside of that though, we should generally
@@ -724,8 +757,7 @@
if (mSurfaceControl != null && !mSurfaceCreated) {
mSurface.release();
- mSurfaceControl.destroy();
- mSurfaceControl = null;
+ releaseSurfaces();
}
}
} catch (Exception ex) {
@@ -823,7 +855,6 @@
final ViewRootImpl viewRoot = getViewRootImpl();
applySurfaceTransforms(mSurfaceControl, position, frameNumber);
- applySurfaceTransforms(mSurfaceControl.mBackgroundControl, position, frameNumber);
applyChildSurfaceTransaction_renderWorker(mRtTransaction, viewRoot.mSurface,
frameNumber);
@@ -950,7 +981,19 @@
* @hide
*/
public void setResizeBackgroundColor(int bgColor) {
- mSurfaceControl.setBackgroundColor(bgColor);
+ if (mBackgroundControl == null) {
+ return;
+ }
+
+ final float[] colorComponents = new float[] { Color.red(bgColor) / 255.f,
+ Color.green(bgColor) / 255.f, Color.blue(bgColor) / 255.f };
+
+ SurfaceControl.openTransaction();
+ try {
+ mBackgroundControl.setColor(colorComponents);
+ } finally {
+ SurfaceControl.closeTransaction();
+ }
}
@UnsupportedAppUsage
@@ -1128,154 +1171,12 @@
};
/**
- * @hide
+ * Return a SurfaceControl which can be used for parenting Surfaces to
+ * this SurfaceView.
+ *
+ * @return The SurfaceControl for this SurfaceView.
*/
public SurfaceControl getSurfaceControl() {
return mSurfaceControl;
}
-
- class SurfaceControlWithBackground extends SurfaceControl {
- SurfaceControl mBackgroundControl;
- private boolean mOpaque = true;
- public boolean mVisible = false;
-
- public SurfaceControlWithBackground(String name, boolean opaque, SurfaceControl.Builder b)
- throws Exception {
- super(b.setName(name).build());
-
- mBackgroundControl = b.setName("Background for -" + name)
- .setFormat(OPAQUE)
- // Unset the buffer size of the background color layer.
- .setBufferSize(0, 0)
- .setColorLayer(true)
- .build();
- mOpaque = opaque;
- }
-
- @Override
- public void setAlpha(float alpha) {
- super.setAlpha(alpha);
- mBackgroundControl.setAlpha(alpha);
- }
-
- @Override
- public void setLayer(int zorder) {
- super.setLayer(zorder);
- // -3 is below all other child layers as SurfaceView never goes below -2
- mBackgroundControl.setLayer(-3);
- }
-
- @Override
- public void setPosition(float x, float y) {
- super.setPosition(x, y);
- mBackgroundControl.setPosition(x, y);
- }
-
- @Override
- public void setBufferSize(int w, int h) {
- super.setBufferSize(w, h);
- // The background surface is a color layer so we do not set a size.
- }
-
- @Override
- public void setWindowCrop(Rect crop) {
- super.setWindowCrop(crop);
- mBackgroundControl.setWindowCrop(crop);
- }
-
- @Override
- public void setWindowCrop(int width, int height) {
- super.setWindowCrop(width, height);
- mBackgroundControl.setWindowCrop(width, height);
- }
-
- @Override
- public void setLayerStack(int layerStack) {
- super.setLayerStack(layerStack);
- mBackgroundControl.setLayerStack(layerStack);
- }
-
- @Override
- public void setOpaque(boolean isOpaque) {
- super.setOpaque(isOpaque);
- mOpaque = isOpaque;
- updateBackgroundVisibility();
- }
-
- @Override
- public void setSecure(boolean isSecure) {
- super.setSecure(isSecure);
- }
-
- @Override
- public void setMatrix(float dsdx, float dtdx, float dsdy, float dtdy) {
- super.setMatrix(dsdx, dtdx, dsdy, dtdy);
- mBackgroundControl.setMatrix(dsdx, dtdx, dsdy, dtdy);
- }
-
- @Override
- public void hide() {
- super.hide();
- mVisible = false;
- updateBackgroundVisibility();
- }
-
- @Override
- public void show() {
- super.show();
- mVisible = true;
- updateBackgroundVisibility();
- }
-
- @Override
- public void destroy() {
- super.destroy();
- mBackgroundControl.destroy();
- }
-
- @Override
- public void release() {
- super.release();
- mBackgroundControl.release();
- }
-
- @Override
- public void setTransparentRegionHint(Region region) {
- super.setTransparentRegionHint(region);
- mBackgroundControl.setTransparentRegionHint(region);
- }
-
- @Override
- public void deferTransactionUntil(IBinder handle, long frame) {
- super.deferTransactionUntil(handle, frame);
- mBackgroundControl.deferTransactionUntil(handle, frame);
- }
-
- @Override
- public void deferTransactionUntil(Surface barrier, long frame) {
- super.deferTransactionUntil(barrier, frame);
- mBackgroundControl.deferTransactionUntil(barrier, frame);
- }
-
- /** Set the color to fill the background with. */
- private void setBackgroundColor(int bgColor) {
- final float[] colorComponents = new float[] { Color.red(bgColor) / 255.f,
- Color.green(bgColor) / 255.f, Color.blue(bgColor) / 255.f };
-
- SurfaceControl.openTransaction();
- try {
- mBackgroundControl.setColor(colorComponents);
- } finally {
- SurfaceControl.closeTransaction();
- }
- }
-
- void updateBackgroundVisibility() {
- if (mOpaque && mVisible) {
- mBackgroundControl.show();
- } else {
- mBackgroundControl.hide();
- }
- }
- }
}
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 86c5f18..14a5e3b 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -922,19 +922,6 @@
}
}
- /**
- * Returns a list of VR InputMethod currently installed.
- * @hide
- */
- @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS)
- public List<InputMethodInfo> getVrInputMethodList() {
- try {
- return mService.getVrInputMethodList();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
public List<InputMethodInfo> getEnabledInputMethodList() {
try {
return mService.getEnabledInputMethodList();
@@ -1894,7 +1881,15 @@
/**
* Notify the event when the user tapped or clicked the text view.
+ *
+ * @param view {@link View} which is being clicked.
+ * @see InputMethodService#onViewClicked(boolean)
+ * @deprecated The semantics of this method can never be defined well for composite {@link View}
+ * that works as a giant "Canvas", which can host its own UI hierarchy and sub focus
+ * state. {@link android.webkit.WebView} is a good example. Application / IME
+ * developers should not rely on this method.
*/
+ @Deprecated
public void viewClicked(View view) {
// Re-dispatch if there is a context mismatch.
final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 356d178..8194a92 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -35,7 +35,6 @@
// TODO: Use ParceledListSlice instead
List<InputMethodInfo> getInputMethodList();
- List<InputMethodInfo> getVrInputMethodList();
// TODO: Use ParceledListSlice instead
List<InputMethodInfo> getEnabledInputMethodList();
List<InputMethodSubtype> getEnabledInputMethodSubtypeList(in String imiId,
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index d5dc703..8d3c482 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -34,6 +34,7 @@
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.os.AsyncTask;
import android.os.Handler;
@@ -176,6 +177,7 @@
private UserManager mUserManager;
private final Handler mHandler;
private final SparseLongArray mLockoutDeadlines = new SparseLongArray();
+ private Boolean mHasSecureLockScreen;
/**
* Use {@link TrustManager#isTrustUsuallyManaged(int)}.
@@ -706,6 +708,10 @@
* @param userId the user whose pattern is to be saved.
*/
public void saveLockPattern(List<LockPatternView.Cell> pattern, String savedPattern, int userId) {
+ if (!hasSecureLockScreen()) {
+ throw new UnsupportedOperationException(
+ "This operation requires the lock screen feature.");
+ }
if (pattern == null || pattern.size() < MIN_LOCK_PATTERN_SIZE) {
throw new IllegalArgumentException("pattern must not be null and at least "
+ MIN_LOCK_PATTERN_SIZE + " dots long.");
@@ -801,6 +807,10 @@
/** Update the encryption password if it is enabled **/
private void updateEncryptionPassword(final int type, final String password) {
+ if (!hasSecureLockScreen()) {
+ throw new UnsupportedOperationException(
+ "This operation requires the lock screen feature.");
+ }
if (!isDeviceEncryptionEnabled()) {
return;
}
@@ -835,6 +845,10 @@
*/
public void saveLockPassword(String password, String savedPassword, int requestedQuality,
int userHandle) {
+ if (!hasSecureLockScreen()) {
+ throw new UnsupportedOperationException(
+ "This operation requires the lock screen feature.");
+ }
if (password == null || password.length() < MIN_LOCK_PASSWORD_SIZE) {
throw new IllegalArgumentException("password must not be null and at least "
+ "of length " + MIN_LOCK_PASSWORD_SIZE);
@@ -1621,6 +1635,10 @@
*/
public boolean setLockCredentialWithToken(String credential, int type, int requestedQuality,
long tokenHandle, byte[] token, int userId) {
+ if (!hasSecureLockScreen()) {
+ throw new UnsupportedOperationException(
+ "This operation requires the lock screen feature.");
+ }
LockSettingsInternal localService = getLockSettingsInternal();
if (type != CREDENTIAL_TYPE_NONE) {
if (TextUtils.isEmpty(credential) || credential.length() < MIN_LOCK_PASSWORD_SIZE) {
@@ -1854,6 +1872,17 @@
return getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM) != 0;
}
+ /**
+ * Return true if the device supports the lock screen feature, false otherwise.
+ */
+ public boolean hasSecureLockScreen() {
+ if (mHasSecureLockScreen == null) {
+ mHasSecureLockScreen = Boolean.valueOf(mContext.getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN));
+ }
+ return mHasSecureLockScreen.booleanValue();
+ }
+
public static boolean userOwnsFrpCredential(Context context, UserInfo info) {
return info != null && info.isPrimary() && info.isAdmin() && frpCredentialEnabled(context);
}
diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp
index 9b138eb..7eddcfe 100644
--- a/core/jni/android_net_NetUtils.cpp
+++ b/core/jni/android_net_NetUtils.cpp
@@ -16,8 +16,11 @@
#define LOG_TAG "NetUtils"
+#include <vector>
+
#include "jni.h"
#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
#include "NetdClient.h"
#include <utils/misc.h>
#include <android_runtime/AndroidRuntime.h>
@@ -55,6 +58,31 @@
static const uint32_t kUDPDstPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, dest);
static const uint16_t kDhcpClientPort = 68;
+constexpr int MAXPACKETSIZE = 8 * 1024;
+// FrameworkListener limits the size of commands to 1024 bytes. TODO: fix this.
+constexpr int MAXCMDSIZE = 1024;
+
+static void throwErrnoException(JNIEnv* env, const char* functionName, int error) {
+ ScopedLocalRef<jstring> detailMessage(env, env->NewStringUTF(functionName));
+ if (detailMessage.get() == NULL) {
+ // Not really much we can do here. We're probably dead in the water,
+ // but let's try to stumble on...
+ env->ExceptionClear();
+ }
+ static jclass errnoExceptionClass =
+ MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/system/ErrnoException"));
+
+ static jmethodID errnoExceptionCtor =
+ GetMethodIDOrDie(env, errnoExceptionClass,
+ "<init>", "(Ljava/lang/String;I)V");
+
+ jobject exception = env->NewObject(errnoExceptionClass,
+ errnoExceptionCtor,
+ detailMessage.get(),
+ error);
+ env->Throw(reinterpret_cast<jthrowable>(exception));
+}
+
static void android_net_utils_attachDhcpFilter(JNIEnv *env, jobject clazz, jobject javaFd)
{
struct sock_filter filter_code[] = {
@@ -372,6 +400,63 @@
}
}
+static jobject android_net_utils_resNetworkQuery(JNIEnv *env, jobject thiz, jint netId,
+ jstring dname, jint ns_class, jint ns_type, jint flags) {
+ const jsize javaCharsCount = env->GetStringLength(dname);
+ const jsize byteCountUTF8 = env->GetStringUTFLength(dname);
+
+ // Only allow dname which could be simply formatted to UTF8.
+ // In native layer, res_mkquery would re-format the input char array to packet.
+ std::vector<char> queryname(byteCountUTF8 + 1, 0);
+
+ env->GetStringUTFRegion(dname, 0, javaCharsCount, queryname.data());
+ int fd = resNetworkQuery(netId, queryname.data(), ns_class, ns_type, flags);
+
+ if (fd < 0) {
+ throwErrnoException(env, "resNetworkQuery", -fd);
+ return nullptr;
+ }
+
+ return jniCreateFileDescriptor(env, fd);
+}
+
+static jobject android_net_utils_resNetworkSend(JNIEnv *env, jobject thiz, jint netId,
+ jbyteArray msg, jint msgLen, jint flags) {
+ uint8_t data[MAXCMDSIZE];
+
+ checkLenAndCopy(env, msg, msgLen, data);
+ int fd = resNetworkSend(netId, data, msgLen, flags);
+
+ if (fd < 0) {
+ throwErrnoException(env, "resNetworkSend", -fd);
+ return nullptr;
+ }
+
+ return jniCreateFileDescriptor(env, fd);
+}
+
+static jbyteArray android_net_utils_resNetworkResult(JNIEnv *env, jobject thiz, jobject javaFd) {
+ int fd = jniGetFDFromFileDescriptor(env, javaFd);
+ int rcode;
+ std::vector<uint8_t> buf(MAXPACKETSIZE, 0);
+
+ int res = resNetworkResult(fd, &rcode, buf.data(), MAXPACKETSIZE);
+ if (res < 0) {
+ throwErrnoException(env, "resNetworkResult", -res);
+ return nullptr;
+ }
+
+ jbyteArray answer = env->NewByteArray(res);
+ if (answer == nullptr) {
+ throwErrnoException(env, "resNetworkResult", ENOMEM);
+ return nullptr;
+ } else {
+ env->SetByteArrayRegion(answer, 0, res,
+ reinterpret_cast<jbyte*>(buf.data()));
+ }
+
+ return answer;
+}
// ----------------------------------------------------------------------------
@@ -391,6 +476,9 @@
{ "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachRaFilter },
{ "attachControlPacketFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachControlPacketFilter },
{ "setupRaSocket", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_setupRaSocket },
+ { "resNetworkSend", "(I[BII)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkSend },
+ { "resNetworkQuery", "(ILjava/lang/String;III)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkQuery },
+ { "resNetworkResult", "(Ljava/io/FileDescriptor;)[B", (void*) android_net_utils_resNetworkResult },
};
int register_android_net_NetworkUtils(JNIEnv* env)
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 897427f..0453195 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -126,7 +126,12 @@
jstring nameStr, jint w, jint h, jint format, jint flags, jlong parentObject,
jint windowType, jint ownerUid) {
ScopedUtfChars name(env, nameStr);
- sp<SurfaceComposerClient> client(android_view_SurfaceSession_getClient(env, sessionObj));
+ sp<SurfaceComposerClient> client;
+ if (sessionObj != NULL) {
+ client = android_view_SurfaceSession_getClient(env, sessionObj);
+ } else {
+ client = SurfaceComposerClient::getDefault();
+ }
SurfaceControl *parent = reinterpret_cast<SurfaceControl*>(parentObject);
sp<SurfaceControl> surface;
status_t err = client->createSurfaceChecked(
@@ -277,6 +282,21 @@
transaction->setPosition(ctrl, x, y);
}
+static void nativeSetGeometry(JNIEnv* env, jclass clazz, jlong transactionObj, jlong nativeObject,
+ jobject sourceObj, jobject dstObj, jlong orientation) {
+ auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+ SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl *>(nativeObject);
+
+ Rect source, dst;
+ if (sourceObj != NULL) {
+ source = rectFromObj(env, sourceObj);
+ }
+ if (dstObj != NULL) {
+ dst = rectFromObj(env, dstObj);
+ }
+ transaction->setGeometry(ctrl, source, dst, orientation);
+}
+
static void nativeSetGeometryAppliesWithResize(JNIEnv* env, jclass clazz,
jlong transactionObj,
jlong nativeObject) {
@@ -868,13 +888,13 @@
static void nativeReparent(JNIEnv* env, jclass clazz, jlong transactionObj,
jlong nativeObject,
- jobject newParentObject) {
+ jlong newParentObject) {
auto ctrl = reinterpret_cast<SurfaceControl *>(nativeObject);
- sp<IBinder> parentHandle = ibinderForJavaObject(env, newParentObject);
+ auto newParent = reinterpret_cast<SurfaceControl *>(newParentObject);
{
auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
- transaction->reparent(ctrl, parentHandle);
+ transaction->reparent(ctrl, newParent != NULL ? newParent->getHandle() : NULL);
}
}
@@ -1063,7 +1083,7 @@
(void*)nativeDeferTransactionUntilSurface },
{"nativeReparentChildren", "(JJLandroid/os/IBinder;)V",
(void*)nativeReparentChildren } ,
- {"nativeReparent", "(JJLandroid/os/IBinder;)V",
+ {"nativeReparent", "(JJJ)V",
(void*)nativeReparent },
{"nativeSeverChildren", "(JJ)V",
(void*)nativeSeverChildren } ,
@@ -1087,6 +1107,8 @@
{"nativeGetDisplayedContentSample",
"(Landroid/os/IBinder;JJ)Landroid/hardware/display/DisplayedContentSample;",
(void*)nativeGetDisplayedContentSample },
+ {"nativeSetGeometry", "(JJLandroid/graphics/Rect;Landroid/graphics/Rect;J)V",
+ (void*)nativeSetGeometry }
};
int register_android_view_SurfaceControl(JNIEnv* env)
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 7028008..2e7184b 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -493,10 +493,7 @@
// Apply system or app filter based on uid.
if (uid >= AID_APP_START) {
if (is_child_zygote) {
- // set_app_zygote_seccomp_filter();
- // TODO(b/111434506) install the filter; for now, install the app filter
- // which is more restrictive.
- set_app_seccomp_filter();
+ set_app_zygote_seccomp_filter();
} else {
set_app_seccomp_filter();
}
@@ -1645,14 +1642,10 @@
return;
}
- // TODO(b/111434506) install the filter
-
- /*
bool installed = install_setuidgid_seccomp_filter(uidGidMin, uidGidMax);
if (!installed) {
RuntimeAbort(env, __LINE__, "Could not install setuid/setgid seccomp filter.");
}
- */
}
/**
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 7f3ea7a..e68f9db 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -221,6 +221,8 @@
optional bool use_heartbeats = 23;
message TimeController {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
// Whether or not TimeController should skip setting wakeup alarms for jobs that aren't
// ready now.
optional bool skip_not_ready_jobs = 1;
@@ -228,6 +230,8 @@
optional TimeController time_controller = 25;
message QuotaController {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
// How much time each app will have to run jobs within their standby bucket window.
optional int64 allowed_time_per_period_ms = 1;
// How much time the package should have before transitioning from out-of-quota to in-quota.
@@ -251,6 +255,21 @@
optional int64 rare_window_size_ms = 6;
// The maximum amount of time an app can have its jobs running within a 24 hour window.
optional int64 max_execution_time_ms = 7;
+ // The maximum number of jobs an app can run within this particular standby bucket's
+ // window size.
+ optional int32 max_job_count_active = 8;
+ // The maximum number of jobs an app can run within this particular standby bucket's
+ // window size.
+ optional int32 max_job_count_working = 9;
+ // The maximum number of jobs an app can run within this particular standby bucket's
+ // window size.
+ optional int32 max_job_count_frequent = 10;
+ // The maximum number of jobs an app can run within this particular standby bucket's
+ // window size.
+ optional int32 max_job_count_rare = 11;
+ // The maximum number of jobs that should be allowed to run in the past
+ // {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS}.
+ optional int32 max_job_count_per_allowed_time = 12;
}
optional QuotaController quota_controller = 24;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index ea0c8e2..96b8dc2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1748,6 +1748,10 @@
<permission android:name="android.permission.MANAGE_BLUETOOTH_WHEN_WIRELESS_CONSENT_REQUIRED"
android:protectionLevel="signature" />
+ <!-- @hide Allows the device to be reset, clearing all data and enables Test Harness Mode. -->
+ <permission android:name="android.permission.ENABLE_TEST_HARNESS_MODE"
+ android:protectionLevel="signature" />
+
<!-- ================================== -->
<!-- Permissions for accessing accounts -->
<!-- ================================== -->
diff --git a/core/tests/coretests/src/android/app/admin/PasswordMetricsTest.java b/core/tests/coretests/src/android/app/admin/PasswordMetricsTest.java
index 8d42c74..5731daa 100644
--- a/core/tests/coretests/src/android/app/admin/PasswordMetricsTest.java
+++ b/core/tests/coretests/src/android/app/admin/PasswordMetricsTest.java
@@ -20,12 +20,23 @@
import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW;
import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM;
import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+import static android.app.admin.PasswordMetrics.complexityLevelToMinQuality;
+import static android.app.admin.PasswordMetrics.getActualRequiredQuality;
+import static android.app.admin.PasswordMetrics.getMinimumMetrics;
+import static android.app.admin.PasswordMetrics.getTargetQualityMetrics;
+import static android.app.admin.PasswordMetrics.sanitizeComplexityLevel;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
-import android.app.admin.PasswordMetrics.PasswordComplexityBucket;
import android.os.Parcel;
import androidx.test.filters.SmallTest;
@@ -109,7 +120,7 @@
assertEquals(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX,
PasswordMetrics.computeForPassword("1").quality);
// contains a long sequence so isn't complex
- assertEquals(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC,
+ assertEquals(PASSWORD_QUALITY_NUMERIC,
PasswordMetrics.computeForPassword("1234").quality);
assertEquals(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED,
PasswordMetrics.computeForPassword("").quality);
@@ -145,7 +156,7 @@
new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, 5));
assertNotEquals(new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, 4),
- new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, 4));
+ new PasswordMetrics(PASSWORD_QUALITY_COMPLEX, 4));
metrics0 = PasswordMetrics.computeForPassword("1234abcd,./");
metrics1 = PasswordMetrics.computeForPassword("1234abcd,./");
@@ -176,9 +187,9 @@
@Test
public void testConstructQuality() {
PasswordMetrics expected = new PasswordMetrics();
- expected.quality = DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
+ expected.quality = PASSWORD_QUALITY_COMPLEX;
- PasswordMetrics actual = new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_COMPLEX);
+ PasswordMetrics actual = new PasswordMetrics(PASSWORD_QUALITY_COMPLEX);
assertEquals(expected, actual);
}
@@ -256,42 +267,178 @@
}
@Test
- public void testComplexityLevelToBucket_none() {
- PasswordMetrics[] bucket = PasswordComplexityBucket.complexityLevelToBucket(
- PASSWORD_COMPLEXITY_NONE).getMetrics();
+ public void testSanitizeComplexityLevel_none() {
+ assertEquals(PASSWORD_COMPLEXITY_NONE, sanitizeComplexityLevel(PASSWORD_COMPLEXITY_NONE));
- for (PasswordMetrics metrics : bucket) {
- assertEquals(PASSWORD_COMPLEXITY_NONE, metrics.determineComplexity());
- }
}
@Test
- public void testComplexityLevelToBucket_low() {
- PasswordMetrics[] bucket = PasswordComplexityBucket.complexityLevelToBucket(
- PASSWORD_COMPLEXITY_LOW).getMetrics();
-
- for (PasswordMetrics metrics : bucket) {
- assertEquals(PASSWORD_COMPLEXITY_LOW, metrics.determineComplexity());
- }
+ public void testSanitizeComplexityLevel_low() {
+ assertEquals(PASSWORD_COMPLEXITY_LOW, sanitizeComplexityLevel(PASSWORD_COMPLEXITY_LOW));
}
@Test
- public void testComplexityLevelToBucket_medium() {
- PasswordMetrics[] bucket = PasswordComplexityBucket.complexityLevelToBucket(
- PASSWORD_COMPLEXITY_MEDIUM).getMetrics();
-
- for (PasswordMetrics metrics : bucket) {
- assertEquals(PASSWORD_COMPLEXITY_MEDIUM, metrics.determineComplexity());
- }
+ public void testSanitizeComplexityLevel_medium() {
+ assertEquals(
+ PASSWORD_COMPLEXITY_MEDIUM, sanitizeComplexityLevel(PASSWORD_COMPLEXITY_MEDIUM));
}
@Test
- public void testComplexityLevelToBucket_high() {
- PasswordMetrics[] bucket = PasswordComplexityBucket.complexityLevelToBucket(
- PASSWORD_COMPLEXITY_HIGH).getMetrics();
+ public void testSanitizeComplexityLevel_high() {
+ assertEquals(PASSWORD_COMPLEXITY_HIGH, sanitizeComplexityLevel(PASSWORD_COMPLEXITY_HIGH));
+ }
- for (PasswordMetrics metrics : bucket) {
- assertEquals(PASSWORD_COMPLEXITY_HIGH, metrics.determineComplexity());
- }
+ @Test
+ public void testSanitizeComplexityLevel_invalid() {
+ assertEquals(PASSWORD_COMPLEXITY_NONE, sanitizeComplexityLevel(-1));
+ }
+
+ @Test
+ public void testComplexityLevelToMinQuality_none() {
+ assertEquals(PASSWORD_QUALITY_UNSPECIFIED,
+ complexityLevelToMinQuality(PASSWORD_COMPLEXITY_NONE));
+ }
+
+ @Test
+ public void testComplexityLevelToMinQuality_low() {
+ assertEquals(PASSWORD_QUALITY_SOMETHING,
+ complexityLevelToMinQuality(PASSWORD_COMPLEXITY_LOW));
+ }
+
+ @Test
+ public void testComplexityLevelToMinQuality_medium() {
+ assertEquals(PASSWORD_QUALITY_NUMERIC_COMPLEX,
+ complexityLevelToMinQuality(PASSWORD_COMPLEXITY_MEDIUM));
+ }
+
+ @Test
+ public void testComplexityLevelToMinQuality_high() {
+ assertEquals(PASSWORD_QUALITY_NUMERIC_COMPLEX,
+ complexityLevelToMinQuality(PASSWORD_COMPLEXITY_HIGH));
+ }
+
+ @Test
+ public void testComplexityLevelToMinQuality_invalid() {
+ assertEquals(PASSWORD_QUALITY_UNSPECIFIED, complexityLevelToMinQuality(-1));
+ }
+
+ @Test
+ public void testGetTargetQualityMetrics_noneComplexityReturnsDefaultMetrics() {
+ PasswordMetrics metrics =
+ getTargetQualityMetrics(PASSWORD_COMPLEXITY_NONE, PASSWORD_QUALITY_ALPHANUMERIC);
+
+ assertTrue(metrics.isDefault());
+ }
+
+ @Test
+ public void testGetTargetQualityMetrics_qualityNotAllowedReturnsMinQualityMetrics() {
+ PasswordMetrics metrics =
+ getTargetQualityMetrics(PASSWORD_COMPLEXITY_MEDIUM, PASSWORD_QUALITY_NUMERIC);
+
+ assertEquals(PASSWORD_QUALITY_NUMERIC_COMPLEX, metrics.quality);
+ assertEquals(/* expected= */ 4, metrics.length);
+ }
+
+ @Test
+ public void testGetTargetQualityMetrics_highComplexityNumericComplex() {
+ PasswordMetrics metrics = getTargetQualityMetrics(
+ PASSWORD_COMPLEXITY_HIGH, PASSWORD_QUALITY_NUMERIC_COMPLEX);
+
+ assertEquals(PASSWORD_QUALITY_NUMERIC_COMPLEX, metrics.quality);
+ assertEquals(/* expected= */ 8, metrics.length);
+ }
+
+ @Test
+ public void testGetTargetQualityMetrics_mediumComplexityAlphabetic() {
+ PasswordMetrics metrics = getTargetQualityMetrics(
+ PASSWORD_COMPLEXITY_MEDIUM, PASSWORD_QUALITY_ALPHABETIC);
+
+ assertEquals(PASSWORD_QUALITY_ALPHABETIC, metrics.quality);
+ assertEquals(/* expected= */ 4, metrics.length);
+ }
+
+ @Test
+ public void testGetTargetQualityMetrics_lowComplexityAlphanumeric() {
+ PasswordMetrics metrics = getTargetQualityMetrics(
+ PASSWORD_COMPLEXITY_MEDIUM, PASSWORD_QUALITY_ALPHANUMERIC);
+
+ assertEquals(PASSWORD_QUALITY_ALPHANUMERIC, metrics.quality);
+ assertEquals(/* expected= */ 4, metrics.length);
+ }
+
+ @Test
+ public void testGetActualRequiredQuality_nonComplex() {
+ int actual = getActualRequiredQuality(
+ PASSWORD_QUALITY_NUMERIC_COMPLEX,
+ /* requiresNumeric= */ false,
+ /* requiresLettersOrSymbols= */ false);
+
+ assertEquals(PASSWORD_QUALITY_NUMERIC_COMPLEX, actual);
+ }
+
+ @Test
+ public void testGetActualRequiredQuality_complexRequiresNone() {
+ int actual = getActualRequiredQuality(
+ PASSWORD_QUALITY_COMPLEX,
+ /* requiresNumeric= */ false,
+ /* requiresLettersOrSymbols= */ false);
+
+ assertEquals(PASSWORD_QUALITY_UNSPECIFIED, actual);
+ }
+
+ @Test
+ public void testGetActualRequiredQuality_complexRequiresNumeric() {
+ int actual = getActualRequiredQuality(
+ PASSWORD_QUALITY_COMPLEX,
+ /* requiresNumeric= */ true,
+ /* requiresLettersOrSymbols= */ false);
+
+ assertEquals(PASSWORD_QUALITY_NUMERIC, actual);
+ }
+
+ @Test
+ public void testGetActualRequiredQuality_complexRequiresLetters() {
+ int actual = getActualRequiredQuality(
+ PASSWORD_QUALITY_COMPLEX,
+ /* requiresNumeric= */ false,
+ /* requiresLettersOrSymbols= */ true);
+
+ assertEquals(PASSWORD_QUALITY_ALPHABETIC, actual);
+ }
+
+ @Test
+ public void testGetActualRequiredQuality_complexRequiresNumericAndLetters() {
+ int actual = getActualRequiredQuality(
+ PASSWORD_QUALITY_COMPLEX,
+ /* requiresNumeric= */ true,
+ /* requiresLettersOrSymbols= */ true);
+
+ assertEquals(PASSWORD_QUALITY_ALPHANUMERIC, actual);
+ }
+
+ @Test
+ public void testGetMinimumMetrics_userInputStricter() {
+ PasswordMetrics metrics = getMinimumMetrics(
+ PASSWORD_COMPLEXITY_HIGH,
+ PASSWORD_QUALITY_ALPHANUMERIC,
+ PASSWORD_QUALITY_NUMERIC,
+ /* requiresNumeric= */ false,
+ /* requiresLettersOrSymbols= */ false);
+
+ assertEquals(PASSWORD_QUALITY_ALPHANUMERIC, metrics.quality);
+ assertEquals(/* expected= */ 6, metrics.length);
+ }
+
+ @Test
+ public void testGetMinimumMetrics_actualRequiredQualityStricter() {
+ PasswordMetrics metrics = getMinimumMetrics(
+ PASSWORD_COMPLEXITY_HIGH,
+ PASSWORD_QUALITY_UNSPECIFIED,
+ PASSWORD_QUALITY_NUMERIC,
+ /* requiresNumeric= */ false,
+ /* requiresLettersOrSymbols= */ false);
+
+ assertEquals(PASSWORD_QUALITY_NUMERIC_COMPLEX, metrics.quality);
+ assertEquals(/* expected= */ 8, metrics.length);
}
}
diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
index 7f00ad9..7cd3c44 100644
--- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
+++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
@@ -101,6 +101,7 @@
@Test
public void testChangeInsets() {
mController.changeInsets(Insets.of(0, 30, 40, 0));
+ mController.applyChangeInsets(new InsetsState());
assertEquals(Insets.of(0, 30, 40, 0), mController.getCurrentInsets());
ArgumentCaptor<SurfaceParams> captor = ArgumentCaptor.forClass(SurfaceParams.class);
diff --git a/core/tests/coretests/src/com/android/server/wm/test/filters/CoreTestsFilter.java b/core/tests/coretests/src/com/android/server/wm/test/filters/CoreTestsFilter.java
new file mode 100644
index 0000000..1a81c2c
--- /dev/null
+++ b/core/tests/coretests/src/com/android/server/wm/test/filters/CoreTestsFilter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.test.filters;
+
+import android.os.Bundle;
+
+import com.android.test.filters.SelectTest;
+
+/**
+ * JUnit test filter that select Window Manager Service related tests from FrameworksCoreTests.
+ *
+ * <p>Use this filter when running FrameworksCoreTests as
+ * <pre>
+ * adb shell am instrument -w \
+ * -e filter com.android.server.wm.test.filters.CoreTestsFilter \
+ * -e selectTest_verbose true \
+ * com.android.frameworks.coretests/androidx.test.runner.AndroidJUnitRunner
+ * </pre>
+ */
+public final class CoreTestsFilter extends SelectTest {
+
+ private static final String[] SELECTED_CORE_TESTS = {
+ "android.app.servertransaction.", // all tests under the package.
+ "android.view.DisplayCutoutTest",
+ "android.view.InsetsControllerTest",
+ "android.view.InsetsSourceTest",
+ "android.view.InsetsSourceConsumerTest",
+ "android.view.InsetsStateTest",
+ };
+
+ public CoreTestsFilter(Bundle testArgs) {
+ super(addSelectTest(testArgs, SELECTED_CORE_TESTS));
+ }
+}
diff --git a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
index 03cf3eb..9913531 100644
--- a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@@ -67,6 +68,7 @@
// TODO(b/63758238): stop spying the class under test
mLockPatternUtils = spy(new LockPatternUtils(context));
when(mLockPatternUtils.getLockSettings()).thenReturn(ils);
+ doReturn(true).when(mLockPatternUtils).hasSecureLockScreen();
final UserInfo userInfo = Mockito.mock(UserInfo.class);
when(userInfo.isDemo()).thenReturn(isDemoUser);
diff --git a/media/java/android/media/AudioRecordingConfiguration.java b/media/java/android/media/AudioRecordingConfiguration.java
index 52771e4..1d763ce 100644
--- a/media/java/android/media/AudioRecordingConfiguration.java
+++ b/media/java/android/media/AudioRecordingConfiguration.java
@@ -356,11 +356,11 @@
dest.writeInt(mDeviceSource);
dest.writeInt(mClientEffects.length);
for (int i = 0; i < mClientEffects.length; i++) {
- mClientEffects[i].writeToParcel(dest, 0);
+ mClientEffects[i].writeToParcel(dest);
}
dest.writeInt(mDeviceEffects.length);
for (int i = 0; i < mDeviceEffects.length; i++) {
- mDeviceEffects[i].writeToParcel(dest, 0);
+ mDeviceEffects[i].writeToParcel(dest);
}
}
@@ -375,13 +375,13 @@
mClientPortId = in.readInt();
mClientSilenced = in.readBoolean();
mDeviceSource = in.readInt();
- mClientEffects = AudioEffect.Descriptor.CREATOR.newArray(in.readInt());
+ mClientEffects = new AudioEffect.Descriptor[in.readInt()];
for (int i = 0; i < mClientEffects.length; i++) {
- mClientEffects[i] = AudioEffect.Descriptor.CREATOR.createFromParcel(in);
+ mClientEffects[i] = new AudioEffect.Descriptor(in);
}
- mDeviceEffects = AudioEffect.Descriptor.CREATOR.newArray(in.readInt());
- for (int i = 0; i < mClientEffects.length; i++) {
- mDeviceEffects[i] = AudioEffect.Descriptor.CREATOR.createFromParcel(in);
+ mDeviceEffects = new AudioEffect.Descriptor[in.readInt()];
+ for (int i = 0; i < mDeviceEffects.length; i++) {
+ mDeviceEffects[i] = new AudioEffect.Descriptor(in);
}
}
diff --git a/media/java/android/media/MediaController2.java b/media/java/android/media/MediaController2.java
index dd97195..887b447 100644
--- a/media/java/android/media/MediaController2.java
+++ b/media/java/android/media/MediaController2.java
@@ -45,7 +45,7 @@
/**
* Allows an app to interact with an active {@link MediaSession2} or a
- * MediaSession2Service which would provide {@link MediaSession2}. Media buttons and other
+ * {@link MediaSession2Service} which would provide {@link MediaSession2}. Media buttons and other
* commands can be sent to the session.
* <p>
* This API is not generally intended for third party application developers.
@@ -53,7 +53,6 @@
* <a href="{@docRoot}reference/androidx/media2/package-summary.html">Media2 Library</a>
* for consistent behavior across all devices.
*/
-// TODO: use @link for MediaSession2Service
public class MediaController2 implements AutoCloseable {
static final String TAG = "MediaController2";
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -71,6 +70,8 @@
private final Object mLock = new Object();
//@GuardedBy("mLock")
+ private boolean mClosed;
+ //@GuardedBy("mLock")
private int mNextSeqNumber;
//@GuardedBy("mLock")
private Session2Link mSessionBinder;
@@ -141,7 +142,14 @@
@Override
public void close() {
synchronized (mLock) {
+ if (mClosed) {
+ // Already closed. Ignore rest of clean up code.
+ // Note: unbindService() throws IllegalArgumentException when it's called twice.
+ return;
+ }
+ mClosed = true;
if (mServiceConnection != null) {
+ // Note: This should be called even when the bindService() has returned false.
mContext.unbindService(mServiceConnection);
}
if (mSessionBinder != null) {
@@ -167,7 +175,7 @@
* If it is not connected yet, it returns {@code null}.
* <p>
* This may differ with the {@link Session2Token} from the constructor. For example, if the
- * controller is created with the token for MediaSession2Service, this would return
+ * controller is created with the token for {@link MediaSession2Service}, this would return
* token for the {@link MediaSession2} in the service.
*
* @return Session2Token of the connected session, or {@code null} if not connected
@@ -316,7 +324,7 @@
MediaController2.this, command, args);
if (resultReceiver != null) {
if (result == null) {
- throw new RuntimeException("onSessionCommand shouldn't return null");
+ resultReceiver.send(Session2Command.RESULT_INFO_SKIPPED, null);
} else {
resultReceiver.send(result.getResultCode(), result.getResultData());
}
@@ -433,8 +441,8 @@
* @param controller the controller for this event
* @param command the session command
* @param args optional arguments
- * @return the result for the session command. A runtime exception will be thrown if null
- * is returned.
+ * @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED
+ * will be sent to the session.
*/
@Nullable
public Session2Command.Result onSessionCommand(@NonNull MediaController2 controller,
diff --git a/media/java/android/media/MediaSession2.java b/media/java/android/media/MediaSession2.java
index 3adac72..fdd07fd 100644
--- a/media/java/android/media/MediaSession2.java
+++ b/media/java/android/media/MediaSession2.java
@@ -90,6 +90,8 @@
private boolean mClosed;
//@GuardedBy("mLock")
private boolean mPlaybackActive;
+ //@GuardedBy("mLock")
+ private ForegroundServiceEventCallback mForegroundServiceEventCallback;
MediaSession2(@NonNull Context context, @NonNull String id, PendingIntent sessionActivity,
@NonNull Executor callbackExecutor, @NonNull SessionCallback callback) {
@@ -119,6 +121,7 @@
public void close() {
try {
List<ControllerInfo> controllerInfos;
+ ForegroundServiceEventCallback callback;
synchronized (mLock) {
if (mClosed) {
return;
@@ -126,11 +129,15 @@
mClosed = true;
controllerInfos = getConnectedControllers();
mConnectedControllers.clear();
- mCallback.onSessionClosed(this);
+ callback = mForegroundServiceEventCallback;
+ mForegroundServiceEventCallback = null;
}
synchronized (MediaSession2.class) {
SESSION_ID_LIST.remove(mSessionId);
}
+ if (callback != null) {
+ callback.onSessionClosed(this);
+ }
for (ControllerInfo info : controllerInfos) {
info.notifyDisconnected();
}
@@ -224,11 +231,16 @@
* @param playbackActive {@code true} if the playback active, {@code false} otherwise.
**/
public void setPlaybackActive(boolean playbackActive) {
+ final ForegroundServiceEventCallback serviceCallback;
synchronized (mLock) {
if (mPlaybackActive == playbackActive) {
return;
}
mPlaybackActive = playbackActive;
+ serviceCallback = mForegroundServiceEventCallback;
+ }
+ if (serviceCallback != null) {
+ serviceCallback.onPlaybackActiveChanged(this, playbackActive);
}
List<ControllerInfo> controllerInfos = getConnectedControllers();
for (ControllerInfo controller : controllerInfos) {
@@ -257,6 +269,18 @@
return mCallback;
}
+ void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
+ synchronized (mLock) {
+ if (mForegroundServiceEventCallback == callback) {
+ return;
+ }
+ if (mForegroundServiceEventCallback != null && callback != null) {
+ throw new IllegalStateException("A session cannot be added to multiple services");
+ }
+ mForegroundServiceEventCallback = callback;
+ }
+ }
+
// Called by Session2Link.onConnect and MediaSession2Service.MediaSession2ServiceStub.connect
void onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq,
Bundle connectionRequest) {
@@ -373,7 +397,7 @@
MediaSession2.this, controllerInfo, command, args);
if (resultReceiver != null) {
if (result == null) {
- throw new RuntimeException("onSessionCommand shouldn't return null");
+ resultReceiver.send(Session2Command.RESULT_INFO_SKIPPED, null);
} else {
resultReceiver.send(result.getResultCode(), result.getResultData());
}
@@ -695,8 +719,6 @@
* This API is not generally intended for third party application developers.
*/
public abstract static class SessionCallback {
- ForegroundServiceEventCallback mForegroundServiceEventCallback;
-
/**
* Called when a controller is created for this session. Return allowed commands for
* controller. By default it returns {@code null}.
@@ -731,8 +753,8 @@
* @param controller controller information
* @param command the session command
* @param args optional arguments
- * @return the result for the session command. A runtime exception will be thrown if null
- * is returned.
+ * @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED
+ * will be sent to the session.
*/
@Nullable
public Session2Command.Result onSessionCommand(@NonNull MediaSession2 session,
@@ -753,19 +775,10 @@
public void onCommandResult(@NonNull MediaSession2 session,
@NonNull ControllerInfo controller, @NonNull Object token,
@NonNull Session2Command command, @NonNull Session2Command.Result result) {}
+ }
- final void onSessionClosed(MediaSession2 session) {
- if (mForegroundServiceEventCallback != null) {
- mForegroundServiceEventCallback.onSessionClosed(session);
- }
- }
-
- void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
- mForegroundServiceEventCallback = callback;
- }
-
- abstract static class ForegroundServiceEventCallback {
- public void onSessionClosed(MediaSession2 session) {}
- }
+ abstract static class ForegroundServiceEventCallback {
+ public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {}
+ public void onSessionClosed(MediaSession2 session) {}
}
}
diff --git a/media/java/android/media/MediaSession2Service.java b/media/java/android/media/MediaSession2Service.java
index 8fb00fe..5bb746a 100644
--- a/media/java/android/media/MediaSession2Service.java
+++ b/media/java/android/media/MediaSession2Service.java
@@ -19,7 +19,10 @@
import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationManager;
import android.app.Service;
+import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
@@ -28,8 +31,6 @@
import android.util.ArrayMap;
import android.util.Log;
-import com.android.internal.annotations.GuardedBy;
-
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -42,11 +43,7 @@
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
* <a href="{@docRoot}reference/androidx/media2/package-summary.html">Media2 Library</a>
* for consistent behavior across all devices.
- * @hide
*/
-// TODO: Unhide
-// TODO: Add onUpdateNotification(), and calls it to get Notification for startForegroundService()
-// when a session's player state becomes playing.
public abstract class MediaSession2Service extends Service {
/**
* The {@link Intent} that must be declared as handled by the service.
@@ -56,10 +53,29 @@
private static final String TAG = "MediaSession2Service";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- private final Object mLock = new Object();
- @GuardedBy("mLock")
- private Map<String, MediaSession2> mSessions = new ArrayMap<>();
+ private final MediaSession2.ForegroundServiceEventCallback mForegroundServiceEventCallback =
+ new MediaSession2.ForegroundServiceEventCallback() {
+ @Override
+ public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {
+ MediaSession2Service.this.onPlaybackActiveChanged(session, playbackActive);
+ }
+ @Override
+ public void onSessionClosed(MediaSession2 session) {
+ removeSession(session);
+ }
+ };
+
+ private final Object mLock = new Object();
+ //@GuardedBy("mLock")
+ private NotificationManager mNotificationManager;
+ //@GuardedBy("mLock")
+ private Intent mStartSelfIntent;
+ //@GuardedBy("mLock")
+ private Map<String, MediaSession2> mSessions = new ArrayMap<>();
+ //@GuardedBy("mLock")
+ private Map<MediaSession2, MediaNotification> mNotifications = new ArrayMap<>();
+ //@GuardedBy("mLock")
private MediaSession2ServiceStub mStub;
/**
@@ -72,7 +88,12 @@
@Override
public void onCreate() {
super.onCreate();
- mStub = new MediaSession2ServiceStub(this);
+ synchronized (mLock) {
+ mStub = new MediaSession2ServiceStub(this);
+ mStartSelfIntent = new Intent(this, this.getClass());
+ mNotificationManager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
}
@CallSuper
@@ -80,18 +101,13 @@
@Nullable
public IBinder onBind(@NonNull Intent intent) {
if (SERVICE_INTERFACE.equals(intent.getAction())) {
- return mStub;
+ synchronized (mLock) {
+ return mStub;
+ }
}
return null;
}
- @CallSuper
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- // TODO: Dispatch media key events to the primary session.
- return START_STICKY;
- }
-
/**
* Called by the system to notify that it is no longer used and is being removed. Do not call
* this method directly.
@@ -104,10 +120,12 @@
public void onDestroy() {
super.onDestroy();
synchronized (mLock) {
- for (MediaSession2 session : mSessions.values()) {
- session.getCallback().setForegroundServiceEventCallback(null);
+ List<MediaSession2> sessions = getSessions();
+ for (MediaSession2 session : sessions) {
+ removeSession(session);
}
mSessions.clear();
+ mNotifications.clear();
}
mStub.close();
}
@@ -144,6 +162,24 @@
public abstract MediaSession2 onGetPrimarySession();
/**
+ * Called when notification UI needs update. Override this method to show or cancel your own
+ * notification UI.
+ * <p>
+ * This would be called on {@link MediaSession2}'s callback executor when playback state is
+ * changed.
+ * <p>
+ * With the notification returned here, the service becomes foreground service when the playback
+ * is started. Apps must request the permission
+ * {@link android.Manifest.permission#FOREGROUND_SERVICE} in order to use this API. It becomes
+ * background service after the playback is stopped.
+ *
+ * @param session a session that needs notification update.
+ * @return a {@link MediaNotification}. Can be {@code null}.
+ */
+ @Nullable
+ public abstract MediaNotification onUpdateNotification(@NonNull MediaSession2 session);
+
+ /**
* Adds a session to this service.
* <p>
* Added session will be removed automatically when it's closed, or removed when
@@ -161,21 +197,15 @@
}
synchronized (mLock) {
MediaSession2 previousSession = mSessions.get(session.getSessionId());
- if (previousSession != session) {
- if (previousSession != null) {
+ if (previousSession != null) {
+ if (previousSession != session) {
Log.w(TAG, "Session ID should be unique, ID=" + session.getSessionId()
+ ", previous=" + previousSession + ", session=" + session);
}
return;
}
mSessions.put(session.getSessionId(), session);
- session.getCallback().setForegroundServiceEventCallback(
- new MediaSession2.SessionCallback.ForegroundServiceEventCallback() {
- @Override
- public void onSessionClosed(MediaSession2 session) {
- removeSession(session);
- }
- });
+ session.setForegroundServiceEventCallback(mForegroundServiceEventCallback);
}
}
@@ -189,8 +219,21 @@
if (session == null) {
throw new IllegalArgumentException("session shouldn't be null");
}
+ MediaNotification notification;
synchronized (mLock) {
+ if (mSessions.get(session.getSessionId()) != session) {
+ // Session isn't added or removed already.
+ return;
+ }
mSessions.remove(session.getSessionId());
+ notification = mNotifications.remove(session);
+ }
+ session.setForegroundServiceEventCallback(null);
+ if (notification != null) {
+ mNotificationManager.cancel(notification.getNotificationId());
+ }
+ if (getSessions().isEmpty()) {
+ stopForeground(false);
}
}
@@ -207,6 +250,78 @@
return list;
}
+ /**
+ * Called by registered {@link MediaSession2.ForegroundServiceEventCallback}
+ *
+ * @param session session with change
+ * @param playbackActive {@code true} if playback is active.
+ */
+ void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {
+ MediaNotification mediaNotification = onUpdateNotification(session);
+ if (mediaNotification == null) {
+ // The service implementation doesn't want to use the automatic start/stopForeground
+ // feature.
+ return;
+ }
+ synchronized (mLock) {
+ mNotifications.put(session, mediaNotification);
+ }
+ int id = mediaNotification.getNotificationId();
+ Notification notification = mediaNotification.getNotification();
+ if (!playbackActive) {
+ mNotificationManager.notify(id, notification);
+ return;
+ }
+ // playbackActive == true
+ startForegroundService(mStartSelfIntent);
+ startForeground(id, notification);
+ }
+
+ /**
+ * Returned by {@link #onUpdateNotification(MediaSession2)} for making session service
+ * foreground service to keep playback running in the background. It's highly recommended to
+ * show media style notification here.
+ */
+ public static class MediaNotification {
+ private final int mNotificationId;
+ private final Notification mNotification;
+
+ /**
+ * Default constructor
+ *
+ * @param notificationId notification id to be used for
+ * {@link NotificationManager#notify(int, Notification)}.
+ * @param notification a notification to make session service run in the foreground. Media
+ * style notification is recommended here.
+ */
+ public MediaNotification(int notificationId, @NonNull Notification notification) {
+ if (notification == null) {
+ throw new IllegalArgumentException("notification shouldn't be null");
+ }
+ mNotificationId = notificationId;
+ mNotification = notification;
+ }
+
+ /**
+ * Gets the id of the notification.
+ *
+ * @return the notification id
+ */
+ public int getNotificationId() {
+ return mNotificationId;
+ }
+
+ /**
+ * Gets the notification.
+ *
+ * @return the notification
+ */
+ @NonNull
+ public Notification getNotification() {
+ return mNotification;
+ }
+ }
+
private static final class MediaSession2ServiceStub extends IMediaSession2Service.Stub
implements AutoCloseable {
final WeakReference<MediaSession2Service> mService;
diff --git a/media/java/android/media/RemoteControlClient.java b/media/java/android/media/RemoteControlClient.java
index 3b51c82..325420b 100644
--- a/media/java/android/media/RemoteControlClient.java
+++ b/media/java/android/media/RemoteControlClient.java
@@ -21,18 +21,14 @@
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.Bitmap;
+import android.media.session.MediaSession;
import android.media.session.MediaSessionLegacyHelper;
import android.media.session.PlaybackState;
-import android.media.session.MediaSession;
import android.os.Bundle;
-import android.os.Handler;
import android.os.Looper;
-import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
-import java.lang.IllegalArgumentException;
-
/**
* RemoteControlClient enables exposing information meant to be consumed by remote controls
* capable of displaying metadata, artwork and media transport control buttons.
@@ -682,7 +678,7 @@
// USE_SESSIONS
if (mSession != null) {
- int pbState = PlaybackState.getStateFromRccState(state);
+ int pbState = getStateFromRccState(state);
long position = hasPosition ? mPlaybackPositionMs
: PlaybackState.PLAYBACK_POSITION_UNKNOWN;
@@ -718,8 +714,7 @@
// USE_SESSIONS
if (mSession != null) {
PlaybackState.Builder bob = new PlaybackState.Builder(mSessionPlaybackState);
- bob.setActions(
- PlaybackState.getActionsFromRccControlFlags(transportControlFlags));
+ bob.setActions(getActionsFromRccControlFlags(transportControlFlags));
mSessionPlaybackState = bob.build();
mSession.setPlaybackState(mSessionPlaybackState);
}
@@ -1001,16 +996,19 @@
* Period for playback position drift checks, 15s when playing at 1x or slower.
*/
private final static long POSITION_REFRESH_PERIOD_PLAYING_MS = 15000;
+
/**
* Minimum period for playback position drift checks, never more often when every 2s, when
* fast forwarding or rewinding.
*/
private final static long POSITION_REFRESH_PERIOD_MIN_MS = 2000;
+
/**
* The value above which the difference between client-reported playback position and
* estimated position is considered a drift.
*/
private final static long POSITION_DRIFT_MAX_MS = 500;
+
/**
* Compute the period at which the estimated playback position should be compared against the
* actual playback position. Is a funciton of playback speed.
@@ -1025,4 +1023,151 @@
POSITION_REFRESH_PERIOD_MIN_MS);
}
}
+
+ /**
+ * Get the {@link PlaybackState} state for the given
+ * {@link RemoteControlClient} state.
+ *
+ * @param rccState The state used by {@link RemoteControlClient}.
+ * @return The equivalent state used by {@link PlaybackState}.
+ */
+ private static int getStateFromRccState(int rccState) {
+ switch (rccState) {
+ case PLAYSTATE_BUFFERING:
+ return PlaybackState.STATE_BUFFERING;
+ case PLAYSTATE_ERROR:
+ return PlaybackState.STATE_ERROR;
+ case PLAYSTATE_FAST_FORWARDING:
+ return PlaybackState.STATE_FAST_FORWARDING;
+ case PLAYSTATE_NONE:
+ return PlaybackState.STATE_NONE;
+ case PLAYSTATE_PAUSED:
+ return PlaybackState.STATE_PAUSED;
+ case PLAYSTATE_PLAYING:
+ return PlaybackState.STATE_PLAYING;
+ case PLAYSTATE_REWINDING:
+ return PlaybackState.STATE_REWINDING;
+ case PLAYSTATE_SKIPPING_BACKWARDS:
+ return PlaybackState.STATE_SKIPPING_TO_PREVIOUS;
+ case PLAYSTATE_SKIPPING_FORWARDS:
+ return PlaybackState.STATE_SKIPPING_TO_NEXT;
+ case PLAYSTATE_STOPPED:
+ return PlaybackState.STATE_STOPPED;
+ default:
+ return -1;
+ }
+ }
+
+ /**
+ * Get the {@link RemoteControlClient} state for the given
+ * {@link PlaybackState} state.
+ *
+ * @param state The state used by {@link PlaybackState}.
+ * @return The equivalent state used by {@link RemoteControlClient}.
+ */
+ static int getRccStateFromState(int state) {
+ switch (state) {
+ case PlaybackState.STATE_BUFFERING:
+ return PLAYSTATE_BUFFERING;
+ case PlaybackState.STATE_ERROR:
+ return PLAYSTATE_ERROR;
+ case PlaybackState.STATE_FAST_FORWARDING:
+ return PLAYSTATE_FAST_FORWARDING;
+ case PlaybackState.STATE_NONE:
+ return PLAYSTATE_NONE;
+ case PlaybackState.STATE_PAUSED:
+ return PLAYSTATE_PAUSED;
+ case PlaybackState.STATE_PLAYING:
+ return PLAYSTATE_PLAYING;
+ case PlaybackState.STATE_REWINDING:
+ return PLAYSTATE_REWINDING;
+ case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
+ return PLAYSTATE_SKIPPING_BACKWARDS;
+ case PlaybackState.STATE_SKIPPING_TO_NEXT:
+ return PLAYSTATE_SKIPPING_FORWARDS;
+ case PlaybackState.STATE_STOPPED:
+ return PLAYSTATE_STOPPED;
+ default:
+ return -1;
+ }
+ }
+
+ private static long getActionsFromRccControlFlags(int rccFlags) {
+ long actions = 0;
+ long flag = 1;
+ while (flag <= rccFlags) {
+ if ((flag & rccFlags) != 0) {
+ actions |= getActionForRccFlag((int) flag);
+ }
+ flag = flag << 1;
+ }
+ return actions;
+ }
+
+ static int getRccControlFlagsFromActions(long actions) {
+ int rccFlags = 0;
+ long action = 1;
+ while (action <= actions && action < Integer.MAX_VALUE) {
+ if ((action & actions) != 0) {
+ rccFlags |= getRccFlagForAction(action);
+ }
+ action = action << 1;
+ }
+ return rccFlags;
+ }
+
+ private static long getActionForRccFlag(int flag) {
+ switch (flag) {
+ case FLAG_KEY_MEDIA_PREVIOUS:
+ return PlaybackState.ACTION_SKIP_TO_PREVIOUS;
+ case FLAG_KEY_MEDIA_REWIND:
+ return PlaybackState.ACTION_REWIND;
+ case FLAG_KEY_MEDIA_PLAY:
+ return PlaybackState.ACTION_PLAY;
+ case FLAG_KEY_MEDIA_PLAY_PAUSE:
+ return PlaybackState.ACTION_PLAY_PAUSE;
+ case FLAG_KEY_MEDIA_PAUSE:
+ return PlaybackState.ACTION_PAUSE;
+ case FLAG_KEY_MEDIA_STOP:
+ return PlaybackState.ACTION_STOP;
+ case FLAG_KEY_MEDIA_FAST_FORWARD:
+ return PlaybackState.ACTION_FAST_FORWARD;
+ case FLAG_KEY_MEDIA_NEXT:
+ return PlaybackState.ACTION_SKIP_TO_NEXT;
+ case FLAG_KEY_MEDIA_POSITION_UPDATE:
+ return PlaybackState.ACTION_SEEK_TO;
+ case FLAG_KEY_MEDIA_RATING:
+ return PlaybackState.ACTION_SET_RATING;
+ }
+ return 0;
+ }
+
+ private static int getRccFlagForAction(long action) {
+ // We only care about the lower set of actions that can map to rcc
+ // flags.
+ int testAction = action < Integer.MAX_VALUE ? (int) action : 0;
+ switch (testAction) {
+ case (int) PlaybackState.ACTION_SKIP_TO_PREVIOUS:
+ return FLAG_KEY_MEDIA_PREVIOUS;
+ case (int) PlaybackState.ACTION_REWIND:
+ return FLAG_KEY_MEDIA_REWIND;
+ case (int) PlaybackState.ACTION_PLAY:
+ return FLAG_KEY_MEDIA_PLAY;
+ case (int) PlaybackState.ACTION_PLAY_PAUSE:
+ return FLAG_KEY_MEDIA_PLAY_PAUSE;
+ case (int) PlaybackState.ACTION_PAUSE:
+ return FLAG_KEY_MEDIA_PAUSE;
+ case (int) PlaybackState.ACTION_STOP:
+ return FLAG_KEY_MEDIA_STOP;
+ case (int) PlaybackState.ACTION_FAST_FORWARD:
+ return FLAG_KEY_MEDIA_FAST_FORWARD;
+ case (int) PlaybackState.ACTION_SKIP_TO_NEXT:
+ return FLAG_KEY_MEDIA_NEXT;
+ case (int) PlaybackState.ACTION_SEEK_TO:
+ return FLAG_KEY_MEDIA_POSITION_UPDATE;
+ case (int) PlaybackState.ACTION_SET_RATING:
+ return FLAG_KEY_MEDIA_RATING;
+ }
+ return 0;
+ }
}
diff --git a/media/java/android/media/RemoteController.java b/media/java/android/media/RemoteController.java
index 5e9eed7..f70963a 100644
--- a/media/java/android/media/RemoteController.java
+++ b/media/java/android/media/RemoteController.java
@@ -632,8 +632,8 @@
l = this.mOnClientUpdateListener;
}
if (l != null) {
- int playstate = state == null ? RemoteControlClient.PLAYSTATE_NONE : PlaybackState
- .getRccStateFromState(state.getState());
+ int playstate = state == null ? RemoteControlClient.PLAYSTATE_NONE
+ : RemoteControlClient.getRccStateFromState(state.getState());
if (state == null || state.getPosition() == PlaybackState.PLAYBACK_POSITION_UNKNOWN) {
l.onClientPlaybackStateUpdate(playstate);
} else {
@@ -642,7 +642,7 @@
}
if (state != null) {
l.onClientTransportControlUpdate(
- PlaybackState.getRccControlFlagsFromActions(state.getActions()));
+ RemoteControlClient.getRccControlFlagsFromActions(state.getActions()));
}
}
}
diff --git a/media/java/android/media/Ringtone.java b/media/java/android/media/Ringtone.java
index 42597aa..73d3d88 100644
--- a/media/java/android/media/Ringtone.java
+++ b/media/java/android/media/Ringtone.java
@@ -138,7 +138,7 @@
mAudioAttributes = attributes;
// The audio attributes have to be set before the media player is prepared.
// Re-initialize it.
- setUri(mUri);
+ setUri(mUri, mVolumeShaperConfig);
}
/**
@@ -415,6 +415,7 @@
mLocalPlayer.reset();
mLocalPlayer.release();
mLocalPlayer = null;
+ mVolumeShaper = null;
synchronized (sActiveRingtones) {
sActiveRingtones.remove(this);
}
diff --git a/media/java/android/media/Session2Token.java b/media/java/android/media/Session2Token.java
index d8f74c5..238cc2b 100644
--- a/media/java/android/media/Session2Token.java
+++ b/media/java/android/media/Session2Token.java
@@ -35,7 +35,7 @@
import java.util.Objects;
/**
- * Represents an ongoing {@link MediaSession2} or a MediaSession2Service.
+ * Represents an ongoing {@link MediaSession2} or a {@link MediaSession2Service}.
* If it's representing a session service, it may not be ongoing.
* <p>
* This API is not generally intended for third party application developers.
@@ -48,14 +48,6 @@
* <p>
* It can be also obtained by {@link android.media.session.MediaSessionManager}.
*/
-// New version of MediaSession2.Token for following reasons
-// - Stop implementing Parcelable for updatable support
-// - Represent session and library service (formerly browser service) in one class.
-// Previously MediaSession2.Token was for session and ComponentName was for service.
-// This helps controller apps to keep target of dispatching media key events in uniform way.
-// For details about the reason, see following. (Android O+)
-// android.media.session.MediaSessionManager.Callback#onAddressedPlayerChanged
-// TODO: use @link for MediaSession2Service
public final class Session2Token implements Parcelable {
private static final String TAG = "Session2Token";
@@ -85,12 +77,13 @@
public static final int TYPE_SESSION = 0;
/**
- * Type for MediaSession2Service.
+ * Type for {@link MediaSession2Service}.
*/
public static final int TYPE_SESSION_SERVICE = 1;
private final int mUid;
- private final @TokenType int mType;
+ @TokenType
+ private final int mType;
private final String mPackageName;
private final String mServiceName;
private final Session2Link mSessionLink;
@@ -206,14 +199,6 @@
}
/**
- * @hide
- * @return component name of the session. Can be {@code null} for {@link #TYPE_SESSION}.
- */
- public ComponentName getComponentName() {
- return mComponentName;
- }
-
- /**
* @return type of the token
* @see #TYPE_SESSION
* @see #TYPE_SESSION_SERVICE
@@ -222,10 +207,7 @@
return mType;
}
- /**
- * @hide
- */
- public Session2Link getSessionLink() {
+ Session2Link getSessionLink() {
return mSessionLink;
}
diff --git a/media/java/android/media/audiofx/AudioEffect.java b/media/java/android/media/audiofx/AudioEffect.java
index 52e9ae1..5b4bbce 100644
--- a/media/java/android/media/audiofx/AudioEffect.java
+++ b/media/java/android/media/audiofx/AudioEffect.java
@@ -26,7 +26,6 @@
import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
-import android.os.Parcelable;
import android.util.Log;
import java.lang.ref.WeakReference;
@@ -229,7 +228,7 @@
* The method {@link #queryEffects()} returns an array of Descriptors to facilitate effects
* enumeration.
*/
- public static final class Descriptor implements Parcelable {
+ public static class Descriptor {
public Descriptor() {
}
@@ -294,7 +293,9 @@
this.implementor = implementor;
}
- private Descriptor(Parcel in) {
+ /** @hide */
+ @TestApi
+ public Descriptor(Parcel in) {
type = UUID.fromString(in.readString());
uuid = UUID.fromString(in.readString());
connectMode = in.readString();
@@ -302,33 +303,14 @@
implementor = in.readString();
}
- public static final Parcelable.Creator<Descriptor> CREATOR =
- new Parcelable.Creator<Descriptor>() {
- /**
- * Rebuilds a Descriptor previously stored with writeToParcel().
- * @param p Parcel object to read the Descriptor from
- * @return a new Descriptor created from the data in the parcel
- */
- public Descriptor createFromParcel(Parcel p) {
- return new Descriptor(p);
- }
- public Descriptor[] newArray(int size) {
- return new Descriptor[size];
- }
- };
-
@Override
public int hashCode() {
return Objects.hash(type, uuid, connectMode, name, implementor);
}
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
+ /** @hide */
+ @TestApi
+ public void writeToParcel(Parcel dest) {
dest.writeString(type.toString());
dest.writeString(uuid.toString());
dest.writeString(connectMode);
diff --git a/media/java/android/media/session/ControllerCallbackLink.java b/media/java/android/media/session/ControllerCallbackLink.java
index 28845e4..2d59e44 100644
--- a/media/java/android/media/session/ControllerCallbackLink.java
+++ b/media/java/android/media/session/ControllerCallbackLink.java
@@ -24,6 +24,7 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.MediaMetadata;
+import android.media.MediaParceledListSlice;
import android.media.session.MediaController.PlaybackInfo;
import android.media.session.MediaSession.QueueItem;
import android.os.Binder;
@@ -127,7 +128,8 @@
@RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
public void notifyQueueChanged(@Nullable List<QueueItem> queue) {
try {
- mIControllerCallback.notifyQueueChanged(queue);
+ mIControllerCallback.notifyQueueChanged(queue == null ? null :
+ new MediaParceledListSlice(queue));
} catch (RemoteException e) {
throw new RuntimeException(e);
}
@@ -275,11 +277,11 @@
}
@Override
- public void notifyQueueChanged(List<QueueItem> queue) {
+ public void notifyQueueChanged(MediaParceledListSlice queue) {
ensureMediaControlPermission();
final long token = Binder.clearCallingIdentity();
try {
- mCallbackStub.onQueueChanged(queue);
+ mCallbackStub.onQueueChanged(queue == null ? null : queue.getList());
} finally {
Binder.restoreCallingIdentity(token);
}
diff --git a/media/java/android/media/session/ISessionControllerCallback.aidl b/media/java/android/media/session/ISessionControllerCallback.aidl
index 5c02e7c..56ae852 100644
--- a/media/java/android/media/session/ISessionControllerCallback.aidl
+++ b/media/java/android/media/session/ISessionControllerCallback.aidl
@@ -16,8 +16,8 @@
package android.media.session;
import android.media.MediaMetadata;
+import android.media.MediaParceledListSlice;
import android.media.session.MediaController;
-import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Bundle;
@@ -31,7 +31,7 @@
// These callbacks are for the TransportController
void notifyPlaybackStateChanged(in PlaybackState state);
void notifyMetadataChanged(in MediaMetadata metadata);
- void notifyQueueChanged(in List<MediaSession.QueueItem> queue);
+ void notifyQueueChanged(in MediaParceledListSlice queue);
void notifyQueueTitleChanged(CharSequence title);
void notifyExtrasChanged(in Bundle extras);
void notifyVolumeInfoChanged(in MediaController.PlaybackInfo info);
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index eda913e..4896d08 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -116,7 +116,7 @@
private final MediaSessionEngine mImpl;
- // Do not change the name of mCallbackWrapper. Support lib accesses this by using reflection.
+ // Do not change the name of mCallback. Support lib accesses this by using reflection.
@UnsupportedAppUsage
private Object mCallback;
@@ -144,7 +144,7 @@
SessionLink sessionLink = manager.createSession(cbLink, tag);
mImpl = new MediaSessionEngine(context, sessionLink, cbLink, cbStub,
context.getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize));
+ android.R.dimen.config_mediaMetadataBitmapMaxSize));
} catch (RuntimeException e) {
throw new RuntimeException("Remote error creating session.", e);
}
@@ -160,6 +160,7 @@
* @param callback The callback object
*/
public void setCallback(@Nullable Callback callback) {
+ mCallback = callback == null ? null : new Object();
mImpl.setCallback(callback);
}
@@ -173,6 +174,7 @@
* @param handler The handler that events should be posted on.
*/
public void setCallback(@Nullable Callback callback, @Nullable Handler handler) {
+ mCallback = callback == null ? null : new Object();
mImpl.setCallback(callback, handler);
}
@@ -723,7 +725,7 @@
private QueueItem(Parcel in) {
mImpl = new MediaSessionEngine.QueueItem(in);
- mId = in.readLong();
+ mId = mImpl.getQueueId();
}
/**
diff --git a/media/java/android/media/session/MediaSessionEngine.java b/media/java/android/media/session/MediaSessionEngine.java
index c4634a9..f159a95 100644
--- a/media/java/android/media/session/MediaSessionEngine.java
+++ b/media/java/android/media/session/MediaSessionEngine.java
@@ -989,10 +989,8 @@
public static final class CallbackStub extends SessionCallbackLink.CallbackStub {
private WeakReference<MediaSessionEngine> mSessionImpl;
- private static RemoteUserInfo createRemoteUserInfo(String packageName, int pid, int uid,
- ControllerCallbackLink caller) {
- return new RemoteUserInfo(packageName, pid, uid,
- caller != null ? caller.getBinder() : null);
+ private static RemoteUserInfo createRemoteUserInfo(String packageName, int pid, int uid) {
+ return new RemoteUserInfo(packageName, pid, uid);
}
public CallbackStub() {
@@ -1003,7 +1001,7 @@
ControllerCallbackLink caller, String command, Bundle args, ResultReceiver cb) {
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
- sessionImpl.dispatchCommand(createRemoteUserInfo(packageName, pid, uid, caller),
+ sessionImpl.dispatchCommand(createRemoteUserInfo(packageName, pid, uid),
command, args, cb);
}
}
@@ -1015,7 +1013,7 @@
try {
if (sessionImpl != null) {
sessionImpl.dispatchMediaButton(
- createRemoteUserInfo(packageName, pid, uid, null), mediaButtonIntent);
+ createRemoteUserInfo(packageName, pid, uid), mediaButtonIntent);
}
} finally {
if (cb != null) {
@@ -1029,7 +1027,7 @@
ControllerCallbackLink caller, Intent mediaButtonIntent) {
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
- sessionImpl.dispatchMediaButton(createRemoteUserInfo(packageName, pid, uid, caller),
+ sessionImpl.dispatchMediaButton(createRemoteUserInfo(packageName, pid, uid),
mediaButtonIntent);
}
}
@@ -1039,7 +1037,7 @@
ControllerCallbackLink caller) {
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
- sessionImpl.dispatchPrepare(createRemoteUserInfo(packageName, pid, uid, caller));
+ sessionImpl.dispatchPrepare(createRemoteUserInfo(packageName, pid, uid));
}
}
@@ -1050,7 +1048,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchPrepareFromMediaId(
- createRemoteUserInfo(packageName, pid, uid, caller), mediaId, extras);
+ createRemoteUserInfo(packageName, pid, uid), mediaId, extras);
}
}
@@ -1061,7 +1059,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchPrepareFromSearch(
- createRemoteUserInfo(packageName, pid, uid, caller), query, extras);
+ createRemoteUserInfo(packageName, pid, uid), query, extras);
}
}
@@ -1071,7 +1069,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchPrepareFromUri(
- createRemoteUserInfo(packageName, pid, uid, caller), uri, extras);
+ createRemoteUserInfo(packageName, pid, uid), uri, extras);
}
}
@@ -1080,7 +1078,7 @@
ControllerCallbackLink caller) {
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
- sessionImpl.dispatchPlay(createRemoteUserInfo(packageName, pid, uid, caller));
+ sessionImpl.dispatchPlay(createRemoteUserInfo(packageName, pid, uid));
}
}
@@ -1091,7 +1089,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchPlayFromMediaId(
- createRemoteUserInfo(packageName, pid, uid, caller), mediaId, extras);
+ createRemoteUserInfo(packageName, pid, uid), mediaId, extras);
}
}
@@ -1102,7 +1100,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchPlayFromSearch(
- createRemoteUserInfo(packageName, pid, uid, caller), query, extras);
+ createRemoteUserInfo(packageName, pid, uid), query, extras);
}
}
@@ -1112,7 +1110,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchPlayFromUri(
- createRemoteUserInfo(packageName, pid, uid, caller), uri, extras);
+ createRemoteUserInfo(packageName, pid, uid), uri, extras);
}
}
@@ -1122,7 +1120,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchSkipToItem(
- createRemoteUserInfo(packageName, pid, uid, caller), id);
+ createRemoteUserInfo(packageName, pid, uid), id);
}
}
@@ -1131,7 +1129,7 @@
ControllerCallbackLink caller) {
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
- sessionImpl.dispatchPause(createRemoteUserInfo(packageName, pid, uid, caller));
+ sessionImpl.dispatchPause(createRemoteUserInfo(packageName, pid, uid));
}
}
@@ -1140,7 +1138,7 @@
ControllerCallbackLink caller) {
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
- sessionImpl.dispatchStop(createRemoteUserInfo(packageName, pid, uid, caller));
+ sessionImpl.dispatchStop(createRemoteUserInfo(packageName, pid, uid));
}
}
@@ -1149,7 +1147,7 @@
ControllerCallbackLink caller) {
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
- sessionImpl.dispatchNext(createRemoteUserInfo(packageName, pid, uid, caller));
+ sessionImpl.dispatchNext(createRemoteUserInfo(packageName, pid, uid));
}
}
@@ -1158,7 +1156,7 @@
ControllerCallbackLink caller) {
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
- sessionImpl.dispatchPrevious(createRemoteUserInfo(packageName, pid, uid, caller));
+ sessionImpl.dispatchPrevious(createRemoteUserInfo(packageName, pid, uid));
}
}
@@ -1168,7 +1166,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchFastForward(
- createRemoteUserInfo(packageName, pid, uid, caller));
+ createRemoteUserInfo(packageName, pid, uid));
}
}
@@ -1177,7 +1175,7 @@
ControllerCallbackLink caller) {
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
- sessionImpl.dispatchRewind(createRemoteUserInfo(packageName, pid, uid, caller));
+ sessionImpl.dispatchRewind(createRemoteUserInfo(packageName, pid, uid));
}
}
@@ -1187,7 +1185,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchSeekTo(
- createRemoteUserInfo(packageName, pid, uid, caller), pos);
+ createRemoteUserInfo(packageName, pid, uid), pos);
}
}
@@ -1197,7 +1195,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchRate(
- createRemoteUserInfo(packageName, pid, uid, caller), rating);
+ createRemoteUserInfo(packageName, pid, uid), rating);
}
}
@@ -1207,7 +1205,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchCustomAction(
- createRemoteUserInfo(packageName, pid, uid, caller), action, args);
+ createRemoteUserInfo(packageName, pid, uid), action, args);
}
}
@@ -1217,7 +1215,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchAdjustVolume(
- createRemoteUserInfo(packageName, pid, uid, caller), direction);
+ createRemoteUserInfo(packageName, pid, uid), direction);
}
}
@@ -1227,7 +1225,7 @@
MediaSessionEngine sessionImpl = mSessionImpl.get();
if (sessionImpl != null) {
sessionImpl.dispatchSetVolumeTo(
- createRemoteUserInfo(packageName, pid, uid, caller), value);
+ createRemoteUserInfo(packageName, pid, uid), value);
}
}
diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java
index 3f4fbb9..c64c452 100644
--- a/media/java/android/media/session/MediaSessionManager.java
+++ b/media/java/android/media/session/MediaSessionManager.java
@@ -28,7 +28,6 @@
import android.media.IRemoteVolumeController;
import android.media.MediaSession2;
import android.media.Session2Token;
-import android.media.browse.MediaBrowser;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
@@ -37,6 +36,7 @@
import android.os.UserHandle;
import android.service.media.MediaBrowserService;
import android.service.notification.NotificationListenerService;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.view.KeyEvent;
@@ -797,7 +797,6 @@
private final String mPackageName;
private final int mPid;
private final int mUid;
- private final IBinder mCallerBinder;
/**
* Create a new remote user information.
@@ -807,22 +806,9 @@
* @param uid The uid of the remote user
*/
public RemoteUserInfo(@NonNull String packageName, int pid, int uid) {
- this(packageName, pid, uid, null);
- }
-
- /**
- * Create a new remote user information.
- *
- * @param packageName The package name of the remote user
- * @param pid The pid of the remote user
- * @param uid The uid of the remote user
- * @param callerBinder The binder of the remote user. Can be {@code null}.
- */
- public RemoteUserInfo(String packageName, int pid, int uid, IBinder callerBinder) {
mPackageName = packageName;
mPid = pid;
mUid = uid;
- mCallerBinder = callerBinder;
}
/**
@@ -847,13 +833,8 @@
}
/**
- * Returns equality of two RemoteUserInfo. Two RemoteUserInfos are the same only if they're
- * sent to the same controller (either {@link MediaController} or
- * {@link MediaBrowser}. If it's not nor one of them is triggered by the key presses, they
- * would be considered as different one.
- * <p>
- * If you only want to compare the caller's package, compare them with the
- * {@link #getPackageName()}, {@link #getPid()}, and/or {@link #getUid()} directly.
+ * Returns equality of two RemoteUserInfo. Two RemoteUserInfo objects are equal
+ * if and only if they have the same package name, same pid, and same uid.
*
* @param obj the reference object with which to compare.
* @return {@code true} if equals, {@code false} otherwise
@@ -867,8 +848,9 @@
return true;
}
RemoteUserInfo otherUserInfo = (RemoteUserInfo) obj;
- return (mCallerBinder == null || otherUserInfo.mCallerBinder == null) ? false
- : mCallerBinder.equals(otherUserInfo.mCallerBinder);
+ return TextUtils.equals(mPackageName, otherUserInfo.mPackageName)
+ && mPid == otherUserInfo.mPid
+ && mUid == otherUserInfo.mUid;
}
@Override
diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java
index 2c57d1f..0d0ec4c 100644
--- a/media/java/android/media/session/PlaybackState.java
+++ b/media/java/android/media/session/PlaybackState.java
@@ -19,7 +19,6 @@
import android.annotation.IntDef;
import android.annotation.LongDef;
import android.annotation.Nullable;
-import android.media.RemoteControlClient;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -481,161 +480,6 @@
return mExtras;
}
- /**
- * Get the {@link PlaybackState} state for the given
- * {@link RemoteControlClient} state.
- *
- * @param rccState The state used by {@link RemoteControlClient}.
- * @return The equivalent state used by {@link PlaybackState}.
- * @hide
- */
- public static int getStateFromRccState(int rccState) {
- switch (rccState) {
- case RemoteControlClient.PLAYSTATE_BUFFERING:
- return STATE_BUFFERING;
- case RemoteControlClient.PLAYSTATE_ERROR:
- return STATE_ERROR;
- case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
- return STATE_FAST_FORWARDING;
- case RemoteControlClient.PLAYSTATE_NONE:
- return STATE_NONE;
- case RemoteControlClient.PLAYSTATE_PAUSED:
- return STATE_PAUSED;
- case RemoteControlClient.PLAYSTATE_PLAYING:
- return STATE_PLAYING;
- case RemoteControlClient.PLAYSTATE_REWINDING:
- return STATE_REWINDING;
- case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
- return STATE_SKIPPING_TO_PREVIOUS;
- case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS:
- return STATE_SKIPPING_TO_NEXT;
- case RemoteControlClient.PLAYSTATE_STOPPED:
- return STATE_STOPPED;
- default:
- return -1;
- }
- }
-
- /**
- * Get the {@link RemoteControlClient} state for the given
- * {@link PlaybackState} state.
- *
- * @param state The state used by {@link PlaybackState}.
- * @return The equivalent state used by {@link RemoteControlClient}.
- * @hide
- */
- public static int getRccStateFromState(int state) {
- switch (state) {
- case STATE_BUFFERING:
- return RemoteControlClient.PLAYSTATE_BUFFERING;
- case STATE_ERROR:
- return RemoteControlClient.PLAYSTATE_ERROR;
- case STATE_FAST_FORWARDING:
- return RemoteControlClient.PLAYSTATE_FAST_FORWARDING;
- case STATE_NONE:
- return RemoteControlClient.PLAYSTATE_NONE;
- case STATE_PAUSED:
- return RemoteControlClient.PLAYSTATE_PAUSED;
- case STATE_PLAYING:
- return RemoteControlClient.PLAYSTATE_PLAYING;
- case STATE_REWINDING:
- return RemoteControlClient.PLAYSTATE_REWINDING;
- case STATE_SKIPPING_TO_PREVIOUS:
- return RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS;
- case STATE_SKIPPING_TO_NEXT:
- return RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS;
- case STATE_STOPPED:
- return RemoteControlClient.PLAYSTATE_STOPPED;
- default:
- return -1;
- }
- }
-
- /**
- * @hide
- */
- public static long getActionsFromRccControlFlags(int rccFlags) {
- long actions = 0;
- long flag = 1;
- while (flag <= rccFlags) {
- if ((flag & rccFlags) != 0) {
- actions |= getActionForRccFlag((int) flag);
- }
- flag = flag << 1;
- }
- return actions;
- }
-
- /**
- * @hide
- */
- public static int getRccControlFlagsFromActions(long actions) {
- int rccFlags = 0;
- long action = 1;
- while (action <= actions && action < Integer.MAX_VALUE) {
- if ((action & actions) != 0) {
- rccFlags |= getRccFlagForAction(action);
- }
- action = action << 1;
- }
- return rccFlags;
- }
-
- private static long getActionForRccFlag(int flag) {
- switch (flag) {
- case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS:
- return ACTION_SKIP_TO_PREVIOUS;
- case RemoteControlClient.FLAG_KEY_MEDIA_REWIND:
- return ACTION_REWIND;
- case RemoteControlClient.FLAG_KEY_MEDIA_PLAY:
- return ACTION_PLAY;
- case RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE:
- return ACTION_PLAY_PAUSE;
- case RemoteControlClient.FLAG_KEY_MEDIA_PAUSE:
- return ACTION_PAUSE;
- case RemoteControlClient.FLAG_KEY_MEDIA_STOP:
- return ACTION_STOP;
- case RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD:
- return ACTION_FAST_FORWARD;
- case RemoteControlClient.FLAG_KEY_MEDIA_NEXT:
- return ACTION_SKIP_TO_NEXT;
- case RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE:
- return ACTION_SEEK_TO;
- case RemoteControlClient.FLAG_KEY_MEDIA_RATING:
- return ACTION_SET_RATING;
- }
- return 0;
- }
-
- private static int getRccFlagForAction(long action) {
- // We only care about the lower set of actions that can map to rcc
- // flags.
- int testAction = action < Integer.MAX_VALUE ? (int) action : 0;
- switch (testAction) {
- case (int) ACTION_SKIP_TO_PREVIOUS:
- return RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS;
- case (int) ACTION_REWIND:
- return RemoteControlClient.FLAG_KEY_MEDIA_REWIND;
- case (int) ACTION_PLAY:
- return RemoteControlClient.FLAG_KEY_MEDIA_PLAY;
- case (int) ACTION_PLAY_PAUSE:
- return RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE;
- case (int) ACTION_PAUSE:
- return RemoteControlClient.FLAG_KEY_MEDIA_PAUSE;
- case (int) ACTION_STOP:
- return RemoteControlClient.FLAG_KEY_MEDIA_STOP;
- case (int) ACTION_FAST_FORWARD:
- return RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD;
- case (int) ACTION_SKIP_TO_NEXT:
- return RemoteControlClient.FLAG_KEY_MEDIA_NEXT;
- case (int) ACTION_SEEK_TO:
- return RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE;
- case (int) ACTION_SET_RATING:
- return RemoteControlClient.FLAG_KEY_MEDIA_RATING;
- }
- return 0;
- }
-
public static final Parcelable.Creator<PlaybackState> CREATOR =
new Parcelable.Creator<PlaybackState>() {
@Override
diff --git a/media/java/android/service/media/MediaBrowserService.java b/media/java/android/service/media/MediaBrowserService.java
index d19d117..2fbc699 100644
--- a/media/java/android/service/media/MediaBrowserService.java
+++ b/media/java/android/service/media/MediaBrowserService.java
@@ -541,8 +541,7 @@
throw new IllegalStateException("This should be called inside of onGetRoot or"
+ " onLoadChildren or onLoadItem methods");
}
- return new RemoteUserInfo(mCurConnection.pkg, mCurConnection.pid, mCurConnection.uid,
- mCurConnection.callbacks.asBinder());
+ return new RemoteUserInfo(mCurConnection.pkg, mCurConnection.pid, mCurConnection.uid);
}
/**
diff --git a/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java b/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
index a3d7852..6b31b82 100644
--- a/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
@@ -247,7 +247,6 @@
private final TelephonyManager mTelephonyManager;
private final WifiManager mWifiManager;
private final ConnectivityManager mCm;
- private final NetworkRequest mDefaultRequest;
private final IpConnectivityLog mMetricsLog;
private final Dependencies mDependencies;
@@ -336,7 +335,6 @@
mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
mCm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- mDefaultRequest = defaultRequest;
// CHECKSTYLE:OFF IndentationCheck
addState(mDefaultState);
@@ -486,8 +484,7 @@
}
private boolean isValidationRequired() {
- return NetworkMonitorUtils.isValidationRequired(
- mDefaultRequest.networkCapabilities, mNetworkCapabilities);
+ return NetworkMonitorUtils.isValidationRequired(mNetworkCapabilities);
}
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index b903142..c3c3f25 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -166,6 +166,8 @@
<uses-permission android:name="android.permission.CONTROL_KEYGUARD" />
<uses-permission android:name="android.permission.SUSPEND_APPS" />
<uses-permission android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND" />
+ <!-- Permission needed to wipe the device for Test Harness Mode -->
+ <uses-permission android:name="android.permission.ENABLE_TEST_HARNESS_MODE" />
<uses-permission android:name="android.permission.MANAGE_APPOPS" />
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyDialog.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyDialog.kt
index 77e25e3..26c6d50 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyDialog.kt
@@ -30,6 +30,7 @@
import com.android.systemui.Dependency
import com.android.systemui.R
import com.android.systemui.plugins.ActivityStarter
+import java.util.concurrent.TimeUnit
class OngoingPrivacyDialog constructor(
val context: Context,
@@ -60,7 +61,8 @@
setNegativeButton(R.string.ongoing_privacy_dialog_cancel, null)
setPositiveButton(R.string.ongoing_privacy_dialog_open_settings,
object : DialogInterface.OnClickListener {
- val intent = Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE)
+ val intent = Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE).putExtra(
+ Intent.EXTRA_DURATION_MILLIS, TimeUnit.MINUTES.toMillis(1))
@Suppress("DEPRECATION")
override fun onClick(dialog: DialogInterface?, which: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 9ef9c94..546b2e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -538,6 +538,7 @@
- getIntrinsicHeight());
}
float viewEnd = viewStart + fullHeight;
+ // TODO: fix this check for anchor scrolling.
if (expandingAnimated && mAmbientState.getScrollY() == 0
&& !mAmbientState.isOnKeyguard() && !iconState.isLastExpandIcon) {
// We are expanding animated. Because we switch to a linear interpolation in this case,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 1b5013d..c246af5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -42,6 +42,8 @@
private ArrayList<ExpandableView> mDraggedViews = new ArrayList<>();
private int mScrollY;
+ private int mAnchorViewIndex;
+ private int mAnchorViewY;
private boolean mDimmed;
private ActivatableNotificationView mActivatedChild;
private float mOverScrollTopAmount;
@@ -130,6 +132,27 @@
this.mScrollY = scrollY;
}
+ /**
+ * Index of the child view whose Y position on screen is returned by {@link #getAnchorViewY()}.
+ * Other views are laid out outwards from this view in both directions.
+ */
+ public int getAnchorViewIndex() {
+ return mAnchorViewIndex;
+ }
+
+ public void setAnchorViewIndex(int anchorViewIndex) {
+ mAnchorViewIndex = anchorViewIndex;
+ }
+
+ /** Current Y position of the view at {@link #getAnchorViewIndex()}. */
+ public int getAnchorViewY() {
+ return mAnchorViewY;
+ }
+
+ public void setAnchorViewY(int anchorViewY) {
+ mAnchorViewY = anchorViewY;
+ }
+
/** Call when dragging begins. */
public void onBeginDrag(ExpandableView view) {
mDraggedViews.add(view);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 0c37666..2129b81 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -18,6 +18,7 @@
import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters;
+import static com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.ANCHOR_SCROLLING;
import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_SWIPE;
import static com.android.systemui.statusbar.phone.NotificationIconAreaController.LOW_PRIORITY;
import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
@@ -176,10 +177,18 @@
private float mExpandedHeight;
private int mOwnScrollY;
+ private View mScrollAnchorView;
+ private int mScrollAnchorViewY;
private int mMaxLayoutHeight;
private VelocityTracker mVelocityTracker;
private OverScroller mScroller;
+ /** Last Y position reported by {@link #mScroller}, used to calculate scroll delta. */
+ private int mLastScrollerY;
+ /**
+ * True if the max position was set to a known position on the last call to {@link #mScroller}.
+ */
+ private boolean mIsScrollerBoundSet;
private Runnable mFinishScrollingCallback;
private int mTouchSlop;
private int mMinimumVelocity;
@@ -417,7 +426,12 @@
private int mStatusBarState;
private int mCachedBackgroundColor;
private boolean mHeadsUpGoingAwayAnimationsAllowed = true;
- private Runnable mAnimateScroll = this::animateScroll;
+ private Runnable mReflingAndAnimateScroll = () -> {
+ if (ANCHOR_SCROLLING) {
+ maybeReflingScroller();
+ }
+ animateScroll();
+ };
private int mCornerRadius;
private int mSidePaddings;
private final Rect mBackgroundAnimationRect = new Rect();
@@ -511,6 +525,7 @@
mDebugPaint.setColor(0xffff0000);
mDebugPaint.setStrokeWidth(2);
mDebugPaint.setStyle(Paint.Style.STROKE);
+ mDebugPaint.setTextSize(25f);
}
mClearAllEnabled = res.getBoolean(R.bool.config_enableNotificationsClearAll);
@@ -674,6 +689,30 @@
}
}
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (DEBUG && ANCHOR_SCROLLING) {
+ if (mScrollAnchorView instanceof ExpandableNotificationRow) {
+ canvas.drawRect(0,
+ mScrollAnchorView.getTranslationY(),
+ getWidth(),
+ mScrollAnchorView.getTranslationY()
+ + ((ExpandableNotificationRow) mScrollAnchorView).getActualHeight(),
+ mDebugPaint);
+ canvas.drawText(Integer.toString(mScrollAnchorViewY), getWidth() - 200,
+ mScrollAnchorView.getTranslationY() + 30, mDebugPaint);
+ int y = (int) mShelf.getTranslationY();
+ canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
+ }
+ canvas.drawText(Integer.toString(getMaxNegativeScrollAmount()), getWidth() - 100,
+ getIntrinsicPadding() + 30, mDebugPaint);
+ canvas.drawText(Integer.toString(getMaxPositiveScrollAmount()), getWidth() - 100,
+ getHeight() - 30, mDebugPaint);
+ }
+ }
+
@ShadeViewRefactor(RefactorComponent.DECORATOR)
private void drawBackground(Canvas canvas) {
int lockScreenLeft = mSidePaddings;
@@ -970,7 +1009,12 @@
mAmbientState.setCurrentScrollVelocity(mScroller.isFinished()
? 0
: mScroller.getCurrVelocity());
- mAmbientState.setScrollY(mOwnScrollY);
+ if (ANCHOR_SCROLLING) {
+ mAmbientState.setAnchorViewIndex(indexOfChild(mScrollAnchorView));
+ mAmbientState.setAnchorViewY(mScrollAnchorViewY);
+ } else {
+ mAmbientState.setScrollY(mOwnScrollY);
+ }
mStackScrollAlgorithm.resetViewStates(mAmbientState);
if (!isCurrentlyAnimating() && !mNeedsAnimation) {
applyCurrentState();
@@ -1004,7 +1048,7 @@
float end = start + child.getActualHeight();
boolean clip = clipStart > start && clipStart < end
|| clipEnd >= start && clipEnd <= end;
- clip &= !(first && mOwnScrollY == 0);
+ clip &= !(first && isScrolledToTop());
child.setDistanceToTopRoundness(clip ? Math.max(start - clipStart, 0)
: ExpandableView.NO_ROUNDNESS);
first = false;
@@ -1016,19 +1060,21 @@
if (mChildrenToAddAnimated.isEmpty()) {
return;
}
- for (int i = 0; i < getChildCount(); i++) {
- ExpandableView child = (ExpandableView) getChildAt(i);
- if (mChildrenToAddAnimated.contains(child)) {
- int startingPosition = getPositionInLinearLayout(child);
- float increasedPaddingAmount = child.getIncreasedPaddingAmount();
- int padding = increasedPaddingAmount == 1.0f ? mIncreasedPaddingBetweenElements
- : increasedPaddingAmount == -1.0f ? 0 : mPaddingBetweenElements;
- int childHeight = getIntrinsicHeight(child) + padding;
- if (startingPosition < mOwnScrollY) {
- // This child starts off screen, so let's keep it offscreen to keep the
- // others visible
+ if (!ANCHOR_SCROLLING) {
+ for (int i = 0; i < getChildCount(); i++) {
+ ExpandableView child = (ExpandableView) getChildAt(i);
+ if (mChildrenToAddAnimated.contains(child)) {
+ int startingPosition = getPositionInLinearLayout(child);
+ float increasedPaddingAmount = child.getIncreasedPaddingAmount();
+ int padding = increasedPaddingAmount == 1.0f ? mIncreasedPaddingBetweenElements
+ : increasedPaddingAmount == -1.0f ? 0 : mPaddingBetweenElements;
+ int childHeight = getIntrinsicHeight(child) + padding;
+ if (startingPosition < mOwnScrollY) {
+ // This child starts off screen, so let's keep it offscreen to keep the
+ // others visible
- setOwnScrollY(mOwnScrollY + childHeight);
+ setOwnScrollY(mOwnScrollY + childHeight);
+ }
}
}
}
@@ -1047,12 +1093,16 @@
int targetScroll = targetScrollForView(expandableView, positionInLinearLayout);
int outOfViewScroll = positionInLinearLayout + expandableView.getIntrinsicHeight();
- targetScroll = Math.max(0, Math.min(targetScroll, getScrollRange()));
+ if (ANCHOR_SCROLLING) {
+ // TODO
+ } else {
+ targetScroll = Math.max(0, Math.min(targetScroll, getScrollRange()));
- // Only apply the scroll if we're scrolling the view upwards, or the view is so far up
- // that it is not visible anymore.
- if (mOwnScrollY < targetScroll || outOfViewScroll < mOwnScrollY) {
- setOwnScrollY(targetScroll);
+ // Only apply the scroll if we're scrolling the view upwards, or the view is so
+ // far up that it is not visible anymore.
+ if (mOwnScrollY < targetScroll || outOfViewScroll < mOwnScrollY) {
+ setOwnScrollY(targetScroll);
+ }
}
}
}
@@ -1073,9 +1123,13 @@
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
private void clampScrollPosition() {
- int scrollRange = getScrollRange();
- if (scrollRange < mOwnScrollY) {
- setOwnScrollY(scrollRange);
+ if (ANCHOR_SCROLLING) {
+ // TODO
+ } else {
+ int scrollRange = getScrollRange();
+ if (scrollRange < mOwnScrollY) {
+ setOwnScrollY(scrollRange);
+ }
}
}
@@ -1453,17 +1507,21 @@
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public boolean scrollTo(View v) {
ExpandableView expandableView = (ExpandableView) v;
- int positionInLinearLayout = getPositionInLinearLayout(v);
- int targetScroll = targetScrollForView(expandableView, positionInLinearLayout);
- int outOfViewScroll = positionInLinearLayout + expandableView.getIntrinsicHeight();
+ if (ANCHOR_SCROLLING) {
+ // TODO
+ } else {
+ int positionInLinearLayout = getPositionInLinearLayout(v);
+ int targetScroll = targetScrollForView(expandableView, positionInLinearLayout);
+ int outOfViewScroll = positionInLinearLayout + expandableView.getIntrinsicHeight();
- // Only apply the scroll if we're scrolling the view upwards, or the view is so far up
- // that it is not visible anymore.
- if (mOwnScrollY < targetScroll || outOfViewScroll < mOwnScrollY) {
- mScroller.startScroll(mScrollX, mOwnScrollY, 0, targetScroll - mOwnScrollY);
- mDontReportNextOverScroll = true;
- animateScroll();
- return true;
+ // Only apply the scroll if we're scrolling the view upwards, or the view is so far up
+ // that it is not visible anymore.
+ if (mOwnScrollY < targetScroll || outOfViewScroll < mOwnScrollY) {
+ mScroller.startScroll(mScrollX, mOwnScrollY, 0, targetScroll - mOwnScrollY);
+ mDontReportNextOverScroll = true;
+ animateScroll();
+ return true;
+ }
}
return false;
}
@@ -1484,16 +1542,20 @@
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
mBottomInset = insets.getSystemWindowInsetBottom();
- int range = getScrollRange();
- if (mOwnScrollY > range) {
- // HACK: We're repeatedly getting staggered insets here while the IME is
- // animating away. To work around that we'll wait until things have settled.
- removeCallbacks(mReclamp);
- postDelayed(mReclamp, 50);
- } else if (mForcedScroll != null) {
- // The scroll was requested before we got the actual inset - in case we need
- // to scroll up some more do so now.
- scrollTo(mForcedScroll);
+ if (ANCHOR_SCROLLING) {
+ // TODO
+ } else {
+ int range = getScrollRange();
+ if (mOwnScrollY > range) {
+ // HACK: We're repeatedly getting staggered insets here while the IME is
+ // animating away. To work around that we'll wait until things have settled.
+ removeCallbacks(mReclamp);
+ postDelayed(mReclamp, 50);
+ } else if (mForcedScroll != null) {
+ // The scroll was requested before we got the actual inset - in case we need
+ // to scroll up some more do so now.
+ scrollTo(mForcedScroll);
+ }
}
return insets;
}
@@ -1502,8 +1564,12 @@
private Runnable mReclamp = new Runnable() {
@Override
public void run() {
- int range = getScrollRange();
- mScroller.startScroll(mScrollX, mOwnScrollY, 0, range - mOwnScrollY);
+ if (ANCHOR_SCROLLING) {
+ // TODO
+ } else {
+ int range = getScrollRange();
+ mScroller.startScroll(mScrollX, mOwnScrollY, 0, range - mOwnScrollY);
+ }
mDontReportNextOverScroll = true;
mDontClampNextScroll = true;
animateScroll();
@@ -1581,20 +1647,39 @@
}
// Top overScroll might not grab all scrolling motion,
// we have to scroll as well.
- float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
- float newScrollY = mOwnScrollY + scrollAmount;
- if (newScrollY > range) {
- if (!mExpandedInThisMotion) {
- float currentBottomPixels = getCurrentOverScrolledPixels(false);
- // We overScroll on the top
- setOverScrolledPixels(currentBottomPixels + newScrollY - range,
- false /* onTop */,
- false /* animate */);
+ if (ANCHOR_SCROLLING) {
+ float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
+ // TODO: once we're recycling this will need to check the adapter position of the child
+ ExpandableView lastRow = getLastRowNotGone();
+ if (lastRow != null && !lastRow.isInShelf()) {
+ float distanceToMax = Math.max(0, getMaxPositiveScrollAmount());
+ if (scrollAmount > distanceToMax) {
+ float currentBottomPixels = getCurrentOverScrolledPixels(false);
+ // We overScroll on the bottom
+ setOverScrolledPixels(currentBottomPixels + (scrollAmount - distanceToMax),
+ false /* onTop */,
+ false /* animate */);
+ mScrollAnchorViewY -= distanceToMax;
+ scrollAmount = 0f;
+ }
}
- setOwnScrollY(range);
- scrollAmount = 0.0f;
+ return scrollAmount;
+ } else {
+ float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
+ float newScrollY = mOwnScrollY + scrollAmount;
+ if (newScrollY > range) {
+ if (!mExpandedInThisMotion) {
+ float currentBottomPixels = getCurrentOverScrolledPixels(false);
+ // We overScroll on the bottom
+ setOverScrolledPixels(currentBottomPixels + newScrollY - range,
+ false /* onTop */,
+ false /* animate */);
+ }
+ setOwnScrollY(range);
+ scrollAmount = 0.0f;
+ }
+ return scrollAmount;
}
- return scrollAmount;
}
/**
@@ -1615,18 +1700,37 @@
}
// Bottom overScroll might not grab all scrolling motion,
// we have to scroll as well.
- float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f;
- float newScrollY = mOwnScrollY + scrollAmount;
- if (newScrollY < 0) {
- float currentTopPixels = getCurrentOverScrolledPixels(true);
- // We overScroll on the top
- setOverScrolledPixels(currentTopPixels - newScrollY,
- true /* onTop */,
- false /* animate */);
- setOwnScrollY(0);
- scrollAmount = 0.0f;
+ if (ANCHOR_SCROLLING) {
+ float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f;
+ // TODO: once we're recycling this will need to check the adapter position of the child
+ ExpandableView firstChild = getFirstChildNotGone();
+ float top = firstChild.getTranslationY();
+ float distanceToTop = mScrollAnchorView.getTranslationY() - top - mScrollAnchorViewY;
+ if (distanceToTop < -scrollAmount) {
+ float currentTopPixels = getCurrentOverScrolledPixels(true);
+ // We overScroll on the top
+ setOverScrolledPixels(currentTopPixels + (-scrollAmount - distanceToTop),
+ true /* onTop */,
+ false /* animate */);
+ mScrollAnchorView = firstChild;
+ mScrollAnchorViewY = 0;
+ scrollAmount = 0f;
+ }
+ return scrollAmount;
+ } else {
+ float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f;
+ float newScrollY = mOwnScrollY + scrollAmount;
+ if (newScrollY < 0) {
+ float currentTopPixels = getCurrentOverScrolledPixels(true);
+ // We overScroll on the top
+ setOverScrolledPixels(currentTopPixels - newScrollY,
+ true /* onTop */,
+ false /* animate */);
+ setOwnScrollY(0);
+ scrollAmount = 0.0f;
+ }
+ return scrollAmount;
}
- return scrollAmount;
}
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
@@ -1661,26 +1765,43 @@
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
private void animateScroll() {
if (mScroller.computeScrollOffset()) {
- int oldY = mOwnScrollY;
- int y = mScroller.getCurrY();
-
- if (oldY != y) {
- int range = getScrollRange();
- if (y < 0 && oldY >= 0 || y > range && oldY <= range) {
- float currVelocity = mScroller.getCurrVelocity();
- if (currVelocity >= mMinimumVelocity) {
- mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
+ if (ANCHOR_SCROLLING) {
+ int oldY = mLastScrollerY;
+ int y = mScroller.getCurrY();
+ int deltaY = y - oldY;
+ if (deltaY != 0) {
+ int maxNegativeScrollAmount = getMaxNegativeScrollAmount();
+ int maxPositiveScrollAmount = getMaxPositiveScrollAmount();
+ if ((maxNegativeScrollAmount < 0 && deltaY < maxNegativeScrollAmount)
+ || (maxPositiveScrollAmount > 0 && deltaY > maxPositiveScrollAmount)) {
+ // This frame takes us into overscroll, so set the max overscroll based on
+ // the current velocity
+ setMaxOverScrollFromCurrentVelocity();
}
+ customOverScrollBy(deltaY, oldY, 0, (int) mMaxOverScroll);
+ mLastScrollerY = y;
}
+ } else {
+ int oldY = mOwnScrollY;
+ int y = mScroller.getCurrY();
- if (mDontClampNextScroll) {
- range = Math.max(range, oldY);
+ if (oldY != y) {
+ int range = getScrollRange();
+ if (y < 0 && oldY >= 0 || y > range && oldY <= range) {
+ // This frame takes us into overscroll, so set the max overscroll based on
+ // the current velocity
+ setMaxOverScrollFromCurrentVelocity();
+ }
+
+ if (mDontClampNextScroll) {
+ range = Math.max(range, oldY);
+ }
+ customOverScrollBy(y - oldY, oldY, range,
+ (int) (mMaxOverScroll));
}
- customOverScrollBy(y - oldY, oldY, range,
- (int) (mMaxOverScroll));
}
- postOnAnimation(mAnimateScroll);
+ postOnAnimation(mReflingAndAnimateScroll);
} else {
mDontClampNextScroll = false;
if (mFinishScrollingCallback != null) {
@@ -1689,26 +1810,67 @@
}
}
- @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
- private boolean customOverScrollBy(int deltaY, int scrollY, int scrollRangeY,
- int maxOverScrollY) {
-
- int newScrollY = scrollY + deltaY;
- final int top = -maxOverScrollY;
- final int bottom = maxOverScrollY + scrollRangeY;
-
- boolean clampedY = false;
- if (newScrollY > bottom) {
- newScrollY = bottom;
- clampedY = true;
- } else if (newScrollY < top) {
- newScrollY = top;
- clampedY = true;
+ private void setMaxOverScrollFromCurrentVelocity() {
+ float currVelocity = mScroller.getCurrVelocity();
+ if (currVelocity >= mMinimumVelocity) {
+ mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
}
+ }
- onCustomOverScrolled(newScrollY, clampedY);
+ /**
+ * Scrolls by the given delta, overscrolling if needed. If called during a fling and the delta
+ * would cause us to exceed the provided maximum overscroll, springs back instead.
+ *
+ * This method performs the determination of whether we're exceeding the overscroll and clamps
+ * the scroll amount if so. The actual scrolling/overscrolling happens in
+ * {@link #onCustomOverScrolled(int, boolean)} (absolute scrolling) or
+ * {@link #onCustomOverScrolledBy(int, boolean)} (anchor scrolling).
+ *
+ * @param deltaY The (signed) number of pixels to scroll.
+ * @param scrollY The current scroll position (absolute scrolling only).
+ * @param scrollRangeY The maximum allowable scroll position (absolute scrolling only).
+ * @param maxOverScrollY The current (unsigned) limit on number of pixels to overscroll by.
+ */
+ @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
+ private void customOverScrollBy(int deltaY, int scrollY, int scrollRangeY, int maxOverScrollY) {
+ if (ANCHOR_SCROLLING) {
+ boolean clampedY = false;
+ if (deltaY < 0) {
+ int maxScrollAmount = getMaxNegativeScrollAmount();
+ if (maxScrollAmount > Integer.MIN_VALUE) {
+ maxScrollAmount -= maxOverScrollY;
+ if (deltaY < maxScrollAmount) {
+ deltaY = maxScrollAmount;
+ clampedY = true;
+ }
+ }
+ } else {
+ int maxScrollAmount = getMaxPositiveScrollAmount();
+ if (maxScrollAmount < Integer.MAX_VALUE) {
+ maxScrollAmount += maxOverScrollY;
+ if (deltaY > maxScrollAmount) {
+ deltaY = maxScrollAmount;
+ clampedY = true;
+ }
+ }
+ }
+ onCustomOverScrolledBy(deltaY, clampedY);
+ } else {
+ int newScrollY = scrollY + deltaY;
+ final int top = -maxOverScrollY;
+ final int bottom = maxOverScrollY + scrollRangeY;
- return clampedY;
+ boolean clampedY = false;
+ if (newScrollY > bottom) {
+ newScrollY = bottom;
+ clampedY = true;
+ } else if (newScrollY < top) {
+ newScrollY = top;
+ clampedY = true;
+ }
+
+ onCustomOverScrolled(newScrollY, clampedY);
+ }
}
/**
@@ -1826,8 +1988,46 @@
}
}
+ /**
+ * Scrolls by the given delta, overscrolling if needed. If called during a fling and the delta
+ * would cause us to exceed the provided maximum overscroll, springs back instead.
+ *
+ * @param deltaY The (signed) number of pixels to scroll.
+ * @param clampedY Whether this value was clamped by the calling method, meaning we've reached
+ * the overscroll limit.
+ */
+ private void onCustomOverScrolledBy(int deltaY, boolean clampedY) {
+ assert ANCHOR_SCROLLING;
+ mScrollAnchorViewY -= deltaY;
+ // Treat animating scrolls differently; see #computeScroll() for why.
+ if (!mScroller.isFinished()) {
+ if (clampedY) {
+ springBack();
+ } else {
+ float overScrollTop = getCurrentOverScrollAmount(true /* top */);
+ if (isScrolledToTop() && mScrollAnchorViewY > 0) {
+ notifyOverscrollTopListener(mScrollAnchorViewY,
+ isRubberbanded(true /* onTop */));
+ } else {
+ notifyOverscrollTopListener(overScrollTop, isRubberbanded(true /* onTop */));
+ }
+ }
+ }
+ updateScrollAnchor();
+ updateOnScrollChange();
+ }
+
+ /**
+ * Scrolls to the given position, overscrolling if needed. If called during a fling and the
+ * position exceeds the provided maximum overscroll, springs back instead.
+ *
+ * @param scrollY The target scroll position.
+ * @param clampedY Whether this value was clamped by the calling method, meaning we've reached
+ * the overscroll limit.
+ */
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
private void onCustomOverScrolled(int scrollY, boolean clampedY) {
+ assert !ANCHOR_SCROLLING;
// Treat animating scrolls differently; see #computeScroll() for why.
if (!mScroller.isFinished()) {
setOwnScrollY(scrollY);
@@ -1846,27 +2046,51 @@
}
}
+ /**
+ * Springs back from an overscroll by stopping the {@link #mScroller} and animating the
+ * overscroll amount back to zero.
+ */
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
private void springBack() {
- int scrollRange = getScrollRange();
- boolean overScrolledTop = mOwnScrollY <= 0;
- boolean overScrolledBottom = mOwnScrollY >= scrollRange;
- if (overScrolledTop || overScrolledBottom) {
- boolean onTop;
- float newAmount;
- if (overScrolledTop) {
- onTop = true;
- newAmount = -mOwnScrollY;
- setOwnScrollY(0);
- mDontReportNextOverScroll = true;
- } else {
- onTop = false;
- newAmount = mOwnScrollY - scrollRange;
- setOwnScrollY(scrollRange);
+ if (ANCHOR_SCROLLING) {
+ boolean overScrolledTop = isScrolledToTop() && mScrollAnchorViewY > 0;
+ int maxPositiveScrollAmount = getMaxPositiveScrollAmount();
+ boolean overscrolledBottom = maxPositiveScrollAmount < 0;
+ if (overScrolledTop || overscrolledBottom) {
+ float newAmount;
+ if (overScrolledTop) {
+ newAmount = mScrollAnchorViewY;
+ mScrollAnchorViewY = 0;
+ mDontReportNextOverScroll = true;
+ } else {
+ newAmount = -maxPositiveScrollAmount;
+ mScrollAnchorViewY -= maxPositiveScrollAmount;
+ }
+ setOverScrollAmount(newAmount, overScrolledTop, false);
+ setOverScrollAmount(0.0f, overScrolledTop, true);
+ mScroller.forceFinished(true);
}
- setOverScrollAmount(newAmount, onTop, false);
- setOverScrollAmount(0.0f, onTop, true);
- mScroller.forceFinished(true);
+ } else {
+ int scrollRange = getScrollRange();
+ boolean overScrolledTop = mOwnScrollY <= 0;
+ boolean overScrolledBottom = mOwnScrollY >= scrollRange;
+ if (overScrolledTop || overScrolledBottom) {
+ boolean onTop;
+ float newAmount;
+ if (overScrolledTop) {
+ onTop = true;
+ newAmount = -mOwnScrollY;
+ setOwnScrollY(0);
+ mDontReportNextOverScroll = true;
+ } else {
+ onTop = false;
+ newAmount = mOwnScrollY - scrollRange;
+ setOwnScrollY(scrollRange);
+ }
+ setOverScrollAmount(newAmount, onTop, false);
+ setOverScrollAmount(0.0f, onTop, true);
+ mScroller.forceFinished(true);
+ }
}
}
@@ -1971,6 +2195,17 @@
return null;
}
+ private ExpandableNotificationRow getLastRowNotGone() {
+ int childCount = getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (child instanceof ExpandableNotificationRow && child.getVisibility() != View.GONE) {
+ return (ExpandableNotificationRow) child;
+ }
+ }
+ return null;
+ }
+
/**
* @return the number of children which have visibility unequal to GONE
*/
@@ -2081,8 +2316,8 @@
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
private void updateForwardAndBackwardScrollability() {
- boolean forwardScrollable = mScrollable && mOwnScrollY < getScrollRange();
- boolean backwardsScrollable = mScrollable && mOwnScrollY > 0;
+ boolean forwardScrollable = mScrollable && !isScrolledToBottom();
+ boolean backwardsScrollable = mScrollable && !isScrolledToTop();
boolean changed = forwardScrollable != mForwardScrollable
|| backwardsScrollable != mBackwardScrollable;
mForwardScrollable = forwardScrollable;
@@ -2403,18 +2638,24 @@
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
protected void fling(int velocityY) {
if (getChildCount() > 0) {
- int scrollRange = getScrollRange();
-
float topAmount = getCurrentOverScrollAmount(true);
float bottomAmount = getCurrentOverScrollAmount(false);
if (velocityY < 0 && topAmount > 0) {
- setOwnScrollY(mOwnScrollY - (int) topAmount);
+ if (ANCHOR_SCROLLING) {
+ mScrollAnchorViewY += topAmount;
+ } else {
+ setOwnScrollY(mOwnScrollY - (int) topAmount);
+ }
mDontReportNextOverScroll = true;
setOverScrollAmount(0, true, false);
mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(true /* onTop */)
* mOverflingDistance + topAmount;
} else if (velocityY > 0 && bottomAmount > 0) {
- setOwnScrollY((int) (mOwnScrollY + bottomAmount));
+ if (ANCHOR_SCROLLING) {
+ mScrollAnchorViewY -= bottomAmount;
+ } else {
+ setOwnScrollY((int) (mOwnScrollY + bottomAmount));
+ }
setOverScrollAmount(0, false, false);
mMaxOverScroll = Math.abs(velocityY) / 1000f
* getRubberBandFactor(false /* onTop */) * mOverflingDistance
@@ -2423,18 +2664,138 @@
// it will be set once we reach the boundary
mMaxOverScroll = 0.0f;
}
- int minScrollY = Math.max(0, scrollRange);
- if (mExpandedInThisMotion) {
- minScrollY = Math.min(minScrollY, mMaxScrollAfterExpand);
+ if (ANCHOR_SCROLLING) {
+ flingScroller(velocityY);
+ } else {
+ int scrollRange = getScrollRange();
+ int minScrollY = Math.max(0, scrollRange);
+ if (mExpandedInThisMotion) {
+ minScrollY = Math.min(minScrollY, mMaxScrollAfterExpand);
+ }
+ mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0, minScrollY, 0,
+ mExpandedInThisMotion && mOwnScrollY >= 0 ? 0 : Integer.MAX_VALUE / 2);
}
- mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0, minScrollY, 0,
- mExpandedInThisMotion && mOwnScrollY >= 0 ? 0 : Integer.MAX_VALUE / 2);
animateScroll();
}
}
/**
+ * Flings the overscroller with the given velocity (anchor-based scrolling).
+ *
+ * Because anchor-based scrolling can't track the current scroll position, the overscroller is
+ * always started at startY = 0, and we interpret the positions it computes as relative to the
+ * start of the scroll.
+ */
+ private void flingScroller(int velocityY) {
+ assert ANCHOR_SCROLLING;
+ mIsScrollerBoundSet = false;
+ maybeFlingScroller(velocityY, true /* always fling */);
+ }
+
+ private void maybeFlingScroller(int velocityY, boolean alwaysFling) {
+ assert ANCHOR_SCROLLING;
+ // Attempt to determine the maximum amount to scroll before we reach the end.
+ // If the first view is not materialized (for an upwards scroll) or the last view is either
+ // not materialized or is pinned to the shade (for a downwards scroll), we don't know this
+ // amount, so we do an unbounded fling and rely on {@link #maybeReflingScroller()} to update
+ // the scroller once we approach the start/end of the list.
+ int minY = Integer.MIN_VALUE;
+ int maxY = Integer.MAX_VALUE;
+ if (velocityY < 0) {
+ minY = getMaxNegativeScrollAmount();
+ if (minY > Integer.MIN_VALUE) {
+ mIsScrollerBoundSet = true;
+ }
+ } else {
+ maxY = getMaxPositiveScrollAmount();
+ if (maxY < Integer.MAX_VALUE) {
+ mIsScrollerBoundSet = true;
+ }
+ }
+ if (mIsScrollerBoundSet || alwaysFling) {
+ mLastScrollerY = 0;
+ // x velocity is set to 1 to avoid overscroller bug
+ mScroller.fling(0, 0, 1, velocityY, 0, 0, minY, maxY, 0,
+ mExpandedInThisMotion && !isScrolledToTop() ? 0 : Integer.MAX_VALUE / 2);
+ }
+ }
+
+ /**
+ * Returns the maximum number of pixels we can scroll in the positive direction (downwards)
+ * before reaching the bottom of the list (discounting overscroll).
+ *
+ * If the return value is negative then we have overscrolled; this is a transient state which
+ * should immediately be handled by adjusting the anchor position and adding the extra space to
+ * the bottom overscroll amount.
+ *
+ * If we don't know how many pixels we have left to scroll (because the last row has not been
+ * materialized, or it's in the shelf so it doesn't have its "natural" position), we return
+ * {@link Integer#MAX_VALUE}.
+ */
+ private int getMaxPositiveScrollAmount() {
+ assert ANCHOR_SCROLLING;
+ // TODO: once we're recycling we need to check the adapter position of the last child.
+ ExpandableNotificationRow lastRow = getLastRowNotGone();
+ if (mScrollAnchorView != null && lastRow != null && !lastRow.isInShelf()) {
+ // distance from bottom of last child to bottom of notifications area is:
+ // distance from bottom of last child
+ return (int) (lastRow.getTranslationY() + lastRow.getActualHeight()
+ // to top of anchor view
+ - mScrollAnchorView.getTranslationY()
+ // plus distance from anchor view to top of notifications area
+ + mScrollAnchorViewY
+ // minus height of notifications area.
+ - (mMaxLayoutHeight - getIntrinsicPadding() - mFooterView.getActualHeight()));
+ } else {
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ /**
+ * Returns the maximum number of pixels (as a negative number) we can scroll in the negative
+ * direction (upwards) before reaching the top of the list (discounting overscroll).
+ *
+ * If the return value is positive then we have overscrolled; this is a transient state which
+ * should immediately be handled by adjusting the anchor position and adding the extra space to
+ * the top overscroll amount.
+ *
+ * If we don't know how many pixels we have left to scroll (because the first row has not been
+ * materialized), we return {@link Integer#MIN_VALUE}.
+ */
+ private int getMaxNegativeScrollAmount() {
+ assert ANCHOR_SCROLLING;
+ // TODO: once we're recycling we need to check the adapter position of the first child.
+ ExpandableView firstChild = getFirstChildNotGone();
+ if (mScrollAnchorView != null && firstChild != null) {
+ // distance from top of first child to top of notifications area is:
+ // distance from top of anchor view
+ return (int) -(mScrollAnchorView.getTranslationY()
+ // to top of first child
+ - firstChild.getTranslationY()
+ // minus distance from top of anchor view to top of notifications area.
+ - mScrollAnchorViewY);
+ } else {
+ return Integer.MIN_VALUE;
+ }
+ }
+
+ /**
+ * During a fling, if we were unable to set the bounds of the fling due to the top/bottom view
+ * not being materialized or being pinned to the shelf, we need to check on every frame if we're
+ * able to set the bounds. If we are, we fling the scroller again with the newly computed
+ * bounds.
+ */
+ private void maybeReflingScroller() {
+ if (!mIsScrollerBoundSet) {
+ // Because mScroller is a flywheel scroller, we fling with the minimum possible
+ // velocity to establish direction, so as not to perceptibly affect the velocity.
+ maybeFlingScroller((int) Math.signum(mScroller.getCurrVelocity()),
+ false /* alwaysFling */);
+ }
+ }
+
+ /**
* @return Whether a fling performed on the top overscroll edge lead to the expanded
* overScroll view (i.e QS).
*/
@@ -2485,25 +2846,10 @@
}
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
- public int getFirstChildIntrinsicHeight() {
- final ExpandableView firstChild = getFirstChildNotGone();
- int firstChildMinHeight = firstChild != null
- ? firstChild.getIntrinsicHeight()
- : mEmptyShadeView != null
- ? mEmptyShadeView.getIntrinsicHeight()
- : mCollapsedSize;
- if (mOwnScrollY > 0) {
- firstChildMinHeight = Math.max(firstChildMinHeight - mOwnScrollY, mCollapsedSize);
- }
- return firstChildMinHeight;
- }
-
- @ShadeViewRefactor(RefactorComponent.COORDINATOR)
public float getTopPaddingOverflow() {
return mTopPaddingOverflow;
}
-
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
public int getPeekHeight() {
final ExpandableView firstChild = getFirstChildNotGone();
@@ -2711,30 +3057,51 @@
*/
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
private void updateScrollStateForRemovedChild(ExpandableView removedChild) {
- int startingPosition = getPositionInLinearLayout(removedChild);
- float increasedPaddingAmount = removedChild.getIncreasedPaddingAmount();
- int padding;
- if (increasedPaddingAmount >= 0) {
- padding = (int) NotificationUtils.interpolate(
- mPaddingBetweenElements,
- mIncreasedPaddingBetweenElements,
- increasedPaddingAmount);
+ if (ANCHOR_SCROLLING) {
+ if (removedChild == mScrollAnchorView) {
+ ExpandableView firstChild = getFirstChildNotGone();
+ if (firstChild != null) {
+ mScrollAnchorView = firstChild;
+ } else {
+ mScrollAnchorView = mShelf;
+ }
+ // Adjust anchor view Y by the distance between the old and new anchors
+ // so that there's no visible change.
+ mScrollAnchorViewY +=
+ mScrollAnchorView.getTranslationY() - removedChild.getTranslationY();
+ }
+ updateScrollAnchor();
+ // TODO: once we're recycling this will need to check the adapter position of the child
+ if (mScrollAnchorView == getFirstChildNotGone() && mScrollAnchorViewY > 0) {
+ mScrollAnchorViewY = 0;
+ }
+ updateOnScrollChange();
} else {
- padding = (int) NotificationUtils.interpolate(
- 0,
- mPaddingBetweenElements,
- 1.0f + increasedPaddingAmount);
- }
- int childHeight = getIntrinsicHeight(removedChild) + padding;
- int endPosition = startingPosition + childHeight;
- if (endPosition <= mOwnScrollY) {
- // This child is fully scrolled of the top, so we have to deduct its height from the
- // scrollPosition
- setOwnScrollY(mOwnScrollY - childHeight);
- } else if (startingPosition < mOwnScrollY) {
- // This child is currently being scrolled into, set the scroll position to the start of
- // this child
- setOwnScrollY(startingPosition);
+ int startingPosition = getPositionInLinearLayout(removedChild);
+ float increasedPaddingAmount = removedChild.getIncreasedPaddingAmount();
+ int padding;
+ if (increasedPaddingAmount >= 0) {
+ padding = (int) NotificationUtils.interpolate(
+ mPaddingBetweenElements,
+ mIncreasedPaddingBetweenElements,
+ increasedPaddingAmount);
+ } else {
+ padding = (int) NotificationUtils.interpolate(
+ 0,
+ mPaddingBetweenElements,
+ 1.0f + increasedPaddingAmount);
+ }
+ int childHeight = getIntrinsicHeight(removedChild) + padding;
+ int endPosition = startingPosition + childHeight;
+ if (endPosition <= mOwnScrollY) {
+ // This child is fully scrolled of the top, so we have to deduct its height from the
+ // scrollPosition
+ setOwnScrollY(mOwnScrollY - childHeight);
+ } else if (startingPosition < mOwnScrollY) {
+ // This child is currently being scrolled into, set the scroll position to the
+ // start of this child
+ setOwnScrollY(startingPosition);
+ }
}
}
@@ -2888,6 +3255,14 @@
generateAddAnimation(child, false /* fromMoreCard */);
updateAnimationState(child);
updateChronometerForChild(child);
+ if (ANCHOR_SCROLLING) {
+ // TODO: once we're recycling this will need to check the adapter position of the child
+ if (child == getFirstChildNotGone() && (isScrolledToTop() || !mIsExpanded)) {
+ // New child was added at the top while we're scrolled to the top;
+ // make it the new anchor view so that we stay at the top.
+ mScrollAnchorView = child;
+ }
+ }
}
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
@@ -3381,17 +3756,24 @@
final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
if (vscroll != 0) {
final int delta = (int) (vscroll * getVerticalScrollFactor());
- final int range = getScrollRange();
- int oldScrollY = mOwnScrollY;
- int newScrollY = oldScrollY - delta;
- if (newScrollY < 0) {
- newScrollY = 0;
- } else if (newScrollY > range) {
- newScrollY = range;
- }
- if (newScrollY != oldScrollY) {
- setOwnScrollY(newScrollY);
- return true;
+ if (ANCHOR_SCROLLING) {
+ mScrollAnchorViewY -= delta;
+ updateScrollAnchor();
+ clampScrollPosition();
+ updateOnScrollChange();
+ } else {
+ final int range = getScrollRange();
+ int oldScrollY = mOwnScrollY;
+ int newScrollY = oldScrollY - delta;
+ if (newScrollY < 0) {
+ newScrollY = 0;
+ } else if (newScrollY > range) {
+ newScrollY = range;
+ }
+ if (newScrollY != oldScrollY) {
+ setOwnScrollY(newScrollY);
+ return true;
+ }
}
}
}
@@ -3459,12 +3841,16 @@
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y;
- int range = getScrollRange();
- if (mExpandedInThisMotion) {
- range = Math.min(range, mMaxScrollAfterExpand);
- }
-
float scrollAmount;
+ int range;
+ if (ANCHOR_SCROLLING) {
+ range = 0; // unused in the methods it's being passed to
+ } else {
+ range = getScrollRange();
+ if (mExpandedInThisMotion) {
+ range = Math.min(range, mMaxScrollAfterExpand);
+ }
+ }
if (deltaY < 0) {
scrollAmount = overScrollDown(deltaY);
} else {
@@ -3501,9 +3887,13 @@
onOverScrollFling(false, initialVelocity);
}
} else {
- if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
- getScrollRange())) {
- animateScroll();
+ if (ANCHOR_SCROLLING) {
+ // TODO
+ } else {
+ if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
+ getScrollRange())) {
+ animateScroll();
+ }
}
}
}
@@ -3515,8 +3905,13 @@
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
- if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
- animateScroll();
+ if (ANCHOR_SCROLLING) {
+ // TODO
+ } else {
+ if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
+ getScrollRange())) {
+ animateScroll();
+ }
}
mActivePointerId = INVALID_POINTER;
endDrag();
@@ -3585,12 +3980,6 @@
}
}
- @ShadeViewRefactor(RefactorComponent.INPUT)
- private void transformTouchEvent(MotionEvent ev, View sourceView, View targetView) {
- ev.offsetLocation(sourceView.getX(), sourceView.getY());
- ev.offsetLocation(-targetView.getX(), -targetView.getY());
- }
-
@Override
@ShadeViewRefactor(RefactorComponent.INPUT)
public boolean onInterceptTouchEvent(MotionEvent ev) {
@@ -3763,8 +4152,12 @@
setIsBeingDragged(false);
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
- if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
- animateScroll();
+ if (ANCHOR_SCROLLING) {
+ // TODO
+ } else {
+ if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
+ animateScroll();
+ }
}
break;
case MotionEvent.ACTION_POINTER_UP:
@@ -3839,14 +4232,20 @@
case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
// fall through
case android.R.id.accessibilityActionScrollUp:
- final int viewportHeight = getHeight() - mPaddingBottom - mTopPadding - mPaddingTop
- - mShelf.getIntrinsicHeight();
- final int targetScrollY = Math.max(0,
- Math.min(mOwnScrollY + direction * viewportHeight, getScrollRange()));
- if (targetScrollY != mOwnScrollY) {
- mScroller.startScroll(mScrollX, mOwnScrollY, 0, targetScrollY - mOwnScrollY);
- animateScroll();
- return true;
+ if (ANCHOR_SCROLLING) {
+ // TODO
+ } else {
+ final int viewportHeight =
+ getHeight() - mPaddingBottom - mTopPadding - mPaddingTop
+ - mShelf.getIntrinsicHeight();
+ final int targetScrollY = Math.max(0,
+ Math.min(mOwnScrollY + direction * viewportHeight, getScrollRange()));
+ if (targetScrollY != mOwnScrollY) {
+ mScroller.startScroll(mScrollX, mOwnScrollY, 0,
+ targetScrollY - mOwnScrollY);
+ animateScroll();
+ return true;
+ }
}
break;
}
@@ -3905,13 +4304,23 @@
@Override
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
public boolean isScrolledToTop() {
- return mOwnScrollY == 0;
+ if (ANCHOR_SCROLLING) {
+ updateScrollAnchor();
+ // TODO: once we're recycling this will need to check the adapter position of the child
+ return mScrollAnchorView == getFirstChildNotGone() && mScrollAnchorViewY >= 0;
+ } else {
+ return mOwnScrollY == 0;
+ }
}
@Override
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
public boolean isScrolledToBottom() {
- return mOwnScrollY >= getScrollRange();
+ if (ANCHOR_SCROLLING) {
+ return getMaxPositiveScrollAmount() <= 0;
+ } else {
+ return mOwnScrollY >= getScrollRange();
+ }
}
@Override
@@ -3953,7 +4362,7 @@
resetCheckSnoozeLeavebehind();
mAmbientState.setExpansionChanging(false);
if (!mIsExpanded) {
- setOwnScrollY(0);
+ resetScrollPosition();
mStatusBar.resetUserExpandedStates();
clearTemporaryViews();
clearUserLockedViews();
@@ -4012,7 +4421,14 @@
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
public void resetScrollPosition() {
mScroller.abortAnimation();
- setOwnScrollY(0);
+ if (ANCHOR_SCROLLING) {
+ // TODO: once we're recycling this will need to modify the adapter position instead
+ mScrollAnchorView = getFirstChildNotGone();
+ mScrollAnchorViewY = 0;
+ updateOnScrollChange();
+ } else {
+ setOwnScrollY(0);
+ }
}
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
@@ -4081,6 +4497,7 @@
private void updateScrollPositionOnExpandInBottom(ExpandableView view) {
if (view instanceof ExpandableNotificationRow && !onKeyguard()) {
ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+ // TODO: once we're recycling this will need to check the adapter position of the child
if (row.isUserLocked() && row != getFirstChildNotGone()) {
if (row.isSummaryWithChildren()) {
return;
@@ -4098,7 +4515,13 @@
layoutEnd -= mShelf.getIntrinsicHeight() + mPaddingBetweenElements;
}
if (endPosition > layoutEnd) {
- setOwnScrollY((int) (mOwnScrollY + endPosition - layoutEnd));
+ if (ANCHOR_SCROLLING) {
+ mScrollAnchorViewY -= (endPosition - layoutEnd);
+ updateScrollAnchor();
+ updateOnScrollChange();
+ } else {
+ setOwnScrollY((int) (mOwnScrollY + endPosition - layoutEnd));
+ }
mDisallowScrollingInThisMotion = true;
}
}
@@ -4663,9 +5086,13 @@
super.onInitializeAccessibilityEventInternal(event);
event.setScrollable(mScrollable);
event.setScrollX(mScrollX);
- event.setScrollY(mOwnScrollY);
event.setMaxScrollX(mScrollX);
- event.setMaxScrollY(getScrollRange());
+ if (ANCHOR_SCROLLING) {
+ // TODO
+ } else {
+ event.setScrollY(mOwnScrollY);
+ event.setMaxScrollY(getScrollRange());
+ }
}
@Override
@@ -4850,13 +5277,63 @@
}
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
- public void setOwnScrollY(int ownScrollY) {
+ private void setOwnScrollY(int ownScrollY) {
+ assert !ANCHOR_SCROLLING;
if (ownScrollY != mOwnScrollY) {
// We still want to call the normal scrolled changed for accessibility reasons
onScrollChanged(mScrollX, ownScrollY, mScrollX, mOwnScrollY);
mOwnScrollY = ownScrollY;
- updateForwardAndBackwardScrollability();
- requestChildrenUpdate();
+ updateOnScrollChange();
+ }
+ }
+
+ private void updateOnScrollChange() {
+ updateForwardAndBackwardScrollability();
+ requestChildrenUpdate();
+ }
+
+ private void updateScrollAnchor() {
+ int anchorIndex = indexOfChild(mScrollAnchorView);
+ // If the anchor view has been scrolled off the top, move to the next view.
+ while (mScrollAnchorViewY < 0) {
+ View nextAnchor = null;
+ for (int i = anchorIndex + 1; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE
+ && child instanceof ExpandableNotificationRow) {
+ anchorIndex = i;
+ nextAnchor = child;
+ break;
+ }
+ }
+ if (nextAnchor == null) {
+ break;
+ }
+ mScrollAnchorViewY +=
+ (int) (nextAnchor.getTranslationY() - mScrollAnchorView.getTranslationY());
+ mScrollAnchorView = nextAnchor;
+ }
+ // If the view above the anchor view is fully visible, make it the anchor view.
+ while (anchorIndex > 0 && mScrollAnchorViewY > 0) {
+ View prevAnchor = null;
+ for (int i = anchorIndex - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE
+ && child instanceof ExpandableNotificationRow) {
+ anchorIndex = i;
+ prevAnchor = child;
+ break;
+ }
+ }
+ if (prevAnchor == null) {
+ break;
+ }
+ float distanceToPreviousAnchor =
+ mScrollAnchorView.getTranslationY() - prevAnchor.getTranslationY();
+ if (distanceToPreviousAnchor < mScrollAnchorViewY) {
+ mScrollAnchorViewY -= (int) distanceToPreviousAnchor;
+ mScrollAnchorView = prevAnchor;
+ }
}
}
@@ -4872,6 +5349,9 @@
mAmbientState.setShelf(shelf);
mStateAnimator.setShelf(shelf);
shelf.bind(mAmbientState, this);
+ if (ANCHOR_SCROLLING) {
+ mScrollAnchorView = mShelf;
+ }
}
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -6022,7 +6502,11 @@
public void expansionStateChanged(boolean isExpanding) {
mExpandingNotification = isExpanding;
if (!mExpandedInThisMotion) {
- mMaxScrollAfterExpand = mOwnScrollY;
+ if (ANCHOR_SCROLLING) {
+ // TODO
+ } else {
+ mMaxScrollAfterExpand = mOwnScrollY;
+ }
mExpandedInThisMotion = true;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 25fb7f9..2a88080 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -41,6 +41,8 @@
*/
public class StackScrollAlgorithm {
+ static final boolean ANCHOR_SCROLLING = false;
+
private static final String LOG_TAG = "StackScrollAlgorithm";
private final ViewGroup mHostView;
@@ -236,6 +238,10 @@
scrollY = Math.max(0, scrollY);
state.scrollY = (int) (scrollY + bottomOverScroll);
+ if (ANCHOR_SCROLLING) {
+ state.anchorViewY = (int) (ambientState.getAnchorViewY() - bottomOverScroll);
+ }
+
//now init the visible children and update paddings
int childCount = hostView.getChildCount();
state.visibleChildren.clear();
@@ -252,6 +258,11 @@
// iterating over it again, it's filled with the actual resolved value.
for (int i = 0; i < childCount; i++) {
+ if (ANCHOR_SCROLLING) {
+ if (i == ambientState.getAnchorViewIndex()) {
+ state.anchorViewIndex = state.visibleChildren.size();
+ }
+ }
ExpandableView v = (ExpandableView) hostView.getChildAt(i);
if (v.getVisibility() != View.GONE) {
if (v == ambientState.getShelf()) {
@@ -350,28 +361,74 @@
private void updatePositionsForState(StackScrollAlgorithmState algorithmState,
AmbientState ambientState) {
- // The y coordinate of the current child.
- float currentYPosition = -algorithmState.scrollY;
- int childCount = algorithmState.visibleChildren.size();
- for (int i = 0; i < childCount; i++) {
- currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition);
+ if (ANCHOR_SCROLLING) {
+ float currentYPosition = algorithmState.anchorViewY;
+ int childCount = algorithmState.visibleChildren.size();
+ for (int i = algorithmState.anchorViewIndex; i < childCount; i++) {
+ if (i > algorithmState.anchorViewIndex && ambientState.beginsNewSection(i)) {
+ currentYPosition += mGapHeight;
+ }
+ currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition,
+ false /* reverse */);
+ }
+ currentYPosition = algorithmState.anchorViewY;
+ for (int i = algorithmState.anchorViewIndex - 1; i >= 0; i--) {
+ if (ambientState.beginsNewSection(i + 1)) {
+ currentYPosition -= mGapHeight;
+ }
+ currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition,
+ true /* reverse */);
+ }
+ } else {
+ // The y coordinate of the current child.
+ float currentYPosition = -algorithmState.scrollY;
+ int childCount = algorithmState.visibleChildren.size();
+ for (int i = 0; i < childCount; i++) {
+ if (ambientState.beginsNewSection(i)) {
+ currentYPosition += mGapHeight;
+ }
+ currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition,
+ false /* reverse */);
+ }
}
}
+ /**
+ * Populates the {@link ExpandableViewState} for a single child.
+ *
+ * @param i The index of the child in
+ * {@link StackScrollAlgorithmState#visibleChildren}.
+ * @param algorithmState The overall output state of the algorithm.
+ * @param ambientState The input state provided to the algorithm.
+ * @param currentYPosition The Y position of the current pass of the algorithm. For a forward
+ * pass, this should be the top of the child; for a reverse pass, the
+ * bottom of the child.
+ * @param reverse Whether we're laying out children in the reverse direction (Y
+ * positions
+ * decreasing) instead of the forward direction (Y positions
+ * increasing).
+ * @return The Y position after laying out the child. This will be the {@code currentYPosition}
+ * for the next call to this method, after adjusting for any gaps between children.
+ */
protected float updateChild(
int i,
StackScrollAlgorithmState algorithmState,
AmbientState ambientState,
- float currentYPosition) {
+ float currentYPosition,
+ boolean reverse) {
ExpandableView child = algorithmState.visibleChildren.get(i);
ExpandableViewState childViewState = child.getViewState();
childViewState.location = ExpandableViewState.LOCATION_UNKNOWN;
int paddingAfterChild = getPaddingAfterChild(algorithmState, child);
int childHeight = getMaxAllowedChildHeight(child);
- if (ambientState.beginsNewSection(i)) {
- currentYPosition += mGapHeight;
+ if (reverse) {
+ childViewState.yTranslation = currentYPosition - (childHeight + paddingAfterChild);
+ if (currentYPosition <= 0) {
+ childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
+ }
+ } else {
+ childViewState.yTranslation = currentYPosition;
}
- childViewState.yTranslation = currentYPosition;
boolean isFooterView = child instanceof FooterView;
boolean isEmptyShadeView = child instanceof EmptyShadeView;
@@ -396,9 +453,13 @@
clampPositionToShelf(child, childViewState, ambientState);
}
- currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild;
- if (currentYPosition <= 0) {
- childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
+ if (reverse) {
+ currentYPosition = childViewState.yTranslation;
+ } else {
+ currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild;
+ if (currentYPosition <= 0) {
+ childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
+ }
}
if (childViewState.location == ExpandableViewState.LOCATION_UNKNOWN) {
Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
@@ -464,6 +525,7 @@
// To check if the row need to do translation according to scroll Y
// heads up show full of row's content and any scroll y indicate that the
// translationY need to move up the HUN.
+ // TODO: fix this check for anchor scrolling.
if (!mIsExpanded && isTopEntry && ambientState.getScrollY() > 0) {
childState.yTranslation -= ambientState.getScrollY();
}
@@ -607,10 +669,18 @@
public class StackScrollAlgorithmState {
/**
- * The scroll position of the algorithm
+ * The scroll position of the algorithm (absolute scrolling).
*/
public int scrollY;
+ /** The index of the anchor view (anchor scrolling). */
+ public int anchorViewIndex;
+
+ /**
+ * The Y position, relative to the top of the screen, of the anchor view (anchor scrolling).
+ */
+ public int anchorViewY;
+
/**
* The children from the host view which are not gone.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 9c4db34..7881df9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -188,6 +188,7 @@
LayoutInflater.from(context).inflate(R.layout.remote_input, root, false);
v.mController = controller;
v.mEntry = entry;
+ v.mEditText.setTextOperationUser(computeTextOperationUser(entry.notification.getUser()));
v.setTag(VIEW_TAG);
return v;
@@ -298,7 +299,6 @@
if (mWrapper != null) {
mWrapper.setRemoteInputVisible(true);
}
- mEditText.setTextOperationUser(computeTextOperationUser(mEntry.notification.getUser()));
mEditText.setInnerFocusable(true);
mEditText.mShowImeOnInputConnection = true;
mEditText.setText(mEntry.remoteInputText);
@@ -328,7 +328,6 @@
mResetting = true;
mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText());
- mEditText.setTextOperationUser(null);
mEditText.getText().clear();
mEditText.setEnabled(true);
mSendButton.setVisibility(VISIBLE);
diff --git a/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java
index 35abfd4..49db488 100644
--- a/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java
@@ -676,6 +676,10 @@
// 3tap and hold
afterLongTapTimeoutTransitionToDraggingState(event);
+ } else if (isTapOutOfDistanceSlop()) {
+
+ transitionToDelegatingStateAndClear();
+
} else if (mDetectTripleTap
// If magnified, delay an ACTION_DOWN for mMultiTapMaxDelay
// to ensure reachability of
@@ -921,6 +925,31 @@
// TODO: multi-display support for magnification gesture handler
mMagnificationController.setForceShowMagnifiableBounds(Display.DEFAULT_DISPLAY, state);
}
+
+ /**
+ * Detects if last action down is out of distance slop between with previous
+ * one, when triple tap is enabled.
+ *
+ * @return true if tap is out of distance slop
+ */
+ boolean isTapOutOfDistanceSlop() {
+ if (!mDetectTripleTap) return false;
+ if (mPreLastDown == null || mLastDown == null) {
+ return false;
+ }
+ final boolean outOfDistanceSlop =
+ GestureUtils.distance(mPreLastDown, mLastDown) > mMultiTapMaxDistance;
+ if (tapCount() > 0) {
+ return outOfDistanceSlop;
+ }
+ // There's no tap in the queue here. We still need to check if this is the case that
+ // user tap screen quickly and out of distance slop.
+ if (outOfDistanceSlop
+ && !GestureUtils.isTimedOut(mPreLastDown, mLastDown, mMultiTapMaxDelay)) {
+ return true;
+ }
+ return false;
+ }
}
private void zoomOn(float centerX, float centerY) {
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index fae7a8d..38594ef 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -2640,8 +2640,7 @@
}
private boolean networkRequiresValidation(NetworkAgentInfo nai) {
- return isValidationRequired(
- mDefaultRequest.networkCapabilities, nai.networkCapabilities);
+ return isValidationRequired(nai.networkCapabilities);
}
private void handleFreshlyValidatedNetwork(NetworkAgentInfo nai) {
diff --git a/services/core/java/com/android/server/DeviceIdleController.java b/services/core/java/com/android/server/DeviceIdleController.java
index 121a830..39030aa 100644
--- a/services/core/java/com/android/server/DeviceIdleController.java
+++ b/services/core/java/com/android/server/DeviceIdleController.java
@@ -341,6 +341,29 @@
@VisibleForTesting
static final int STATE_QUICK_DOZE_DELAY = 7;
+ private static final int ACTIVE_REASON_UNKNOWN = 0;
+ private static final int ACTIVE_REASON_MOTION = 1;
+ private static final int ACTIVE_REASON_SCREEN = 2;
+ private static final int ACTIVE_REASON_CHARGING = 3;
+ private static final int ACTIVE_REASON_UNLOCKED = 4;
+ private static final int ACTIVE_REASON_FROM_BINDER_CALL = 5;
+ private static final int ACTIVE_REASON_FORCED = 6;
+ private static final int ACTIVE_REASON_ALARM = 7;
+ @VisibleForTesting
+ static final int SET_IDLE_FACTOR_RESULT_UNINIT = -1;
+ @VisibleForTesting
+ static final int SET_IDLE_FACTOR_RESULT_IGNORED = 0;
+ @VisibleForTesting
+ static final int SET_IDLE_FACTOR_RESULT_OK = 1;
+ @VisibleForTesting
+ static final int SET_IDLE_FACTOR_RESULT_NOT_SUPPORT = 2;
+ @VisibleForTesting
+ static final int SET_IDLE_FACTOR_RESULT_INVALID = 3;
+ @VisibleForTesting
+ static final long MIN_STATE_STEP_ALARM_CHANGE = 60 * 1000;
+ @VisibleForTesting
+ static final float MIN_PRE_IDLE_FACTOR_CHANGE = 0.05f;
+
@VisibleForTesting
static String stateToString(int state) {
switch (state) {
@@ -405,6 +428,7 @@
private long mNextSensingTimeoutAlarmTime;
private long mCurIdleBudget;
private long mMaintenanceStartTime;
+ private long mIdleStartTime;
private int mActiveIdleOpCount;
private PowerManager.WakeLock mActiveIdleWakeLock; // held when there are operations in progress
@@ -415,6 +439,17 @@
private boolean mAlarmsActive;
private boolean mReportedMaintenanceActivity;
+ /* Factor to apply to INACTIVE_TIMEOUT and IDLE_AFTER_INACTIVE_TIMEOUT in order to enter
+ * STATE_IDLE faster or slower. Don't apply this to SENSING_TIMEOUT or LOCATING_TIMEOUT because:
+ * - Both of them are shorter
+ * - Device sensor might take time be to become be stabilized
+ * Also don't apply the factor if the device is in motion because device motion provides a
+ * stronger signal than a prediction algorithm.
+ */
+ private float mPreIdleFactor;
+ private float mLastPreIdleFactor;
+ private int mActiveReason;
+
public final AtomicFile mConfigFile;
private final RemoteCallbackList<IMaintenanceActivityListener> mMaintenanceActivityListeners =
@@ -760,6 +795,10 @@
* exit doze. Default = true
*/
private static final String KEY_WAIT_FOR_UNLOCK = "wait_for_unlock";
+ private static final String KEY_PRE_IDLE_FACTOR_LONG =
+ "pre_idle_factor_long";
+ private static final String KEY_PRE_IDLE_FACTOR_SHORT =
+ "pre_idle_factor_short";
/**
* This is the time, after becoming inactive, that we go in to the first
@@ -987,6 +1026,16 @@
*/
public long NOTIFICATION_WHITELIST_DURATION;
+ /**
+ * Pre idle time factor use to make idle delay longer
+ */
+ public float PRE_IDLE_FACTOR_LONG;
+
+ /**
+ * Pre idle time factor use to make idle delay shorter
+ */
+ public float PRE_IDLE_FACTOR_SHORT;
+
public boolean WAIT_FOR_UNLOCK;
private final ContentResolver mResolver;
@@ -1082,6 +1131,8 @@
NOTIFICATION_WHITELIST_DURATION = mParser.getDurationMillis(
KEY_NOTIFICATION_WHITELIST_DURATION, 30 * 1000L);
WAIT_FOR_UNLOCK = mParser.getBoolean(KEY_WAIT_FOR_UNLOCK, false);
+ PRE_IDLE_FACTOR_LONG = mParser.getFloat(KEY_PRE_IDLE_FACTOR_LONG, 1.67f);
+ PRE_IDLE_FACTOR_SHORT = mParser.getFloat(KEY_PRE_IDLE_FACTOR_SHORT, 0.33f);
}
}
@@ -1196,6 +1247,12 @@
pw.print(" "); pw.print(KEY_WAIT_FOR_UNLOCK); pw.print("=");
pw.println(WAIT_FOR_UNLOCK);
+
+ pw.print(" "); pw.print(KEY_PRE_IDLE_FACTOR_LONG); pw.print("=");
+ pw.println(PRE_IDLE_FACTOR_LONG);
+
+ pw.print(" "); pw.print(KEY_PRE_IDLE_FACTOR_SHORT); pw.print("=");
+ pw.println(PRE_IDLE_FACTOR_SHORT);
}
}
@@ -1244,6 +1301,8 @@
private static final int MSG_FINISH_IDLE_OP = 8;
private static final int MSG_REPORT_TEMP_APP_WHITELIST_CHANGED = 9;
private static final int MSG_SEND_CONSTRAINT_MONITORING = 10;
+ private static final int MSG_UPDATE_PRE_IDLE_TIMEOUT_FACTOR = 11;
+ private static final int MSG_RESET_PRE_IDLE_TIMEOUT_FACTOR = 12;
final class MyHandler extends Handler {
MyHandler(Looper looper) {
@@ -1373,6 +1432,13 @@
constraint.stopMonitoring();
}
} break;
+ case MSG_UPDATE_PRE_IDLE_TIMEOUT_FACTOR: {
+ updatePreIdleFactor();
+ } break;
+ case MSG_RESET_PRE_IDLE_TIMEOUT_FACTOR: {
+ updatePreIdleFactor();
+ maybeDoImmediateMaintenance();
+ } break;
}
}
}
@@ -1526,6 +1592,28 @@
DeviceIdleController.this.unregisterMaintenanceActivityListener(listener);
}
+ @Override public int setPreIdleTimeoutMode(int mode) {
+ getContext().enforceCallingOrSelfPermission(Manifest.permission.DEVICE_POWER,
+ null);
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return DeviceIdleController.this.setPreIdleTimeoutMode(mode);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override public void resetPreIdleTimeoutMode() {
+ getContext().enforceCallingOrSelfPermission(Manifest.permission.DEVICE_POWER,
+ null);
+ long ident = Binder.clearCallingIdentity();
+ try {
+ DeviceIdleController.this.resetPreIdleTimeoutMode();
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
@Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
DeviceIdleController.this.dump(fd, pw, args);
}
@@ -1768,9 +1856,12 @@
// Start out assuming we are charging. If we aren't, we will at least get
// a battery update the next time the level drops.
mCharging = true;
+ mActiveReason = ACTIVE_REASON_UNKNOWN;
mState = STATE_ACTIVE;
mLightState = LIGHT_STATE_ACTIVE;
mInactiveTimeout = mConstants.INACTIVE_TIMEOUT;
+ mPreIdleFactor = 1.0f;
+ mLastPreIdleFactor = 1.0f;
}
mBinderService = new BinderService();
@@ -2394,6 +2485,7 @@
public void exitIdleInternal(String reason) {
synchronized (this) {
+ mActiveReason = ACTIVE_REASON_FROM_BINDER_CALL;
becomeActiveLocked(reason, Binder.getCallingUid());
}
}
@@ -2463,6 +2555,7 @@
} else if (screenOn) {
mScreenOn = true;
if (!mForceIdle && (!mScreenLocked || !mConstants.WAIT_FOR_UNLOCK)) {
+ mActiveReason = ACTIVE_REASON_SCREEN;
becomeActiveLocked("screen", Process.myUid());
}
}
@@ -2485,6 +2578,7 @@
} else if (charging) {
mCharging = charging;
if (!mForceIdle) {
+ mActiveReason = ACTIVE_REASON_CHARGING;
becomeActiveLocked("charging", Process.myUid());
}
}
@@ -2516,6 +2610,7 @@
if (mScreenLocked != showing) {
mScreenLocked = showing;
if (mScreenOn && !mForceIdle && !mScreenLocked) {
+ mActiveReason = ACTIVE_REASON_UNLOCKED;
becomeActiveLocked("unlocked", Process.myUid());
}
}
@@ -2587,7 +2682,11 @@
mState = STATE_INACTIVE;
if (DEBUG) Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE");
resetIdleManagementLocked();
- scheduleAlarmLocked(mInactiveTimeout, false);
+ long delay = mInactiveTimeout;
+ if (shouldUseIdleTimeoutFactorLocked()) {
+ delay = (long) (mPreIdleFactor * delay);
+ }
+ scheduleAlarmLocked(delay, false);
EventLogTags.writeDeviceIdle(mState, "no activity");
}
}
@@ -2605,6 +2704,7 @@
mNextIdlePendingDelay = 0;
mNextIdleDelay = 0;
mNextLightIdleDelay = 0;
+ mIdleStartTime = 0;
cancelAlarmLocked();
cancelSensingTimeoutAlarmLocked();
cancelLocatingLocked();
@@ -2621,6 +2721,7 @@
if (mForceIdle) {
mForceIdle = false;
if (mScreenOn || mCharging) {
+ mActiveReason = ACTIVE_REASON_FORCED;
becomeActiveLocked("exit-force", Process.myUid());
}
}
@@ -2740,6 +2841,7 @@
if ((now+mConstants.MIN_TIME_TO_ALARM) > mAlarmManager.getNextWakeFromIdleTime()) {
// Whoops, there is an upcoming alarm. We don't actually want to go idle.
if (mState != STATE_ACTIVE) {
+ mActiveReason = ACTIVE_REASON_ALARM;
becomeActiveLocked("alarm", Process.myUid());
becomeInactiveIfAppropriateLocked();
}
@@ -2763,7 +2865,11 @@
// We have now been inactive long enough, it is time to start looking
// for motion and sleep some more while doing so.
startMonitoringMotionLocked();
- scheduleAlarmLocked(mConstants.IDLE_AFTER_INACTIVE_TIMEOUT, false);
+ long delay = mConstants.IDLE_AFTER_INACTIVE_TIMEOUT;
+ if (shouldUseIdleTimeoutFactorLocked()) {
+ delay = (long) (mPreIdleFactor * delay);
+ }
+ scheduleAlarmLocked(delay, false);
moveToStateLocked(STATE_IDLE_PENDING, reason);
break;
case STATE_IDLE_PENDING:
@@ -2834,6 +2940,7 @@
" ms.");
mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR);
if (DEBUG) Slog.d(TAG, "Setting mNextIdleDelay = " + mNextIdleDelay);
+ mIdleStartTime = SystemClock.elapsedRealtime();
mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT);
if (mNextIdleDelay < mConstants.IDLE_TIMEOUT) {
mNextIdleDelay = mConstants.IDLE_TIMEOUT;
@@ -2934,6 +3041,127 @@
}
}
+ @VisibleForTesting
+ int setPreIdleTimeoutMode(int mode) {
+ return setPreIdleTimeoutFactor(getPreIdleTimeoutByMode(mode));
+ }
+
+ @VisibleForTesting
+ float getPreIdleTimeoutByMode(int mode) {
+ switch (mode) {
+ case PowerManager.PRE_IDLE_TIMEOUT_MODE_LONG: {
+ return mConstants.PRE_IDLE_FACTOR_LONG;
+ }
+ case PowerManager.PRE_IDLE_TIMEOUT_MODE_SHORT: {
+ return mConstants.PRE_IDLE_FACTOR_SHORT;
+ }
+ case PowerManager.PRE_IDLE_TIMEOUT_MODE_NORMAL: {
+ return 1.0f;
+ }
+ default: {
+ Slog.w(TAG, "Invalid time out factor mode: " + mode);
+ return 1.0f;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ float getPreIdleTimeoutFactor() {
+ return mPreIdleFactor;
+ }
+
+ @VisibleForTesting
+ int setPreIdleTimeoutFactor(float ratio) {
+ if (!mDeepEnabled) {
+ if (DEBUG) Slog.d(TAG, "setPreIdleTimeoutFactor: Deep Idle disable");
+ return SET_IDLE_FACTOR_RESULT_NOT_SUPPORT;
+ } else if (ratio <= MIN_PRE_IDLE_FACTOR_CHANGE) {
+ if (DEBUG) Slog.d(TAG, "setPreIdleTimeoutFactor: Invalid input");
+ return SET_IDLE_FACTOR_RESULT_INVALID;
+ } else if (Math.abs(ratio - mPreIdleFactor) < MIN_PRE_IDLE_FACTOR_CHANGE) {
+ if (DEBUG) Slog.d(TAG, "setPreIdleTimeoutFactor: New factor same as previous factor");
+ return SET_IDLE_FACTOR_RESULT_IGNORED;
+ }
+ synchronized (this) {
+ mLastPreIdleFactor = mPreIdleFactor;
+ mPreIdleFactor = ratio;
+ }
+ if (DEBUG) Slog.d(TAG, "setPreIdleTimeoutFactor: " + ratio);
+ postUpdatePreIdleFactor();
+ return SET_IDLE_FACTOR_RESULT_OK;
+ }
+
+ @VisibleForTesting
+ void resetPreIdleTimeoutMode() {
+ synchronized (this) {
+ mLastPreIdleFactor = mPreIdleFactor;
+ mPreIdleFactor = 1.0f;
+ }
+ if (DEBUG) Slog.d(TAG, "resetPreIdleTimeoutMode to 1.0");
+ postResetPreIdleTimeoutFactor();
+ }
+
+ private void postUpdatePreIdleFactor() {
+ mHandler.sendEmptyMessage(MSG_UPDATE_PRE_IDLE_TIMEOUT_FACTOR);
+ }
+
+ private void postResetPreIdleTimeoutFactor() {
+ mHandler.sendEmptyMessage(MSG_RESET_PRE_IDLE_TIMEOUT_FACTOR);
+ }
+
+ @VisibleForTesting
+ void updatePreIdleFactor() {
+ synchronized (this) {
+ if (!shouldUseIdleTimeoutFactorLocked()) {
+ return;
+ }
+ if (mState == STATE_INACTIVE || mState == STATE_IDLE_PENDING) {
+ if (mNextAlarmTime == 0) {
+ return;
+ }
+ long delay = mNextAlarmTime - SystemClock.elapsedRealtime();
+ if (delay < MIN_STATE_STEP_ALARM_CHANGE) {
+ return;
+ }
+ long newDelay = (long) (delay / mLastPreIdleFactor * mPreIdleFactor);
+ if (Math.abs(delay - newDelay) < MIN_STATE_STEP_ALARM_CHANGE) {
+ return;
+ }
+ scheduleAlarmLocked(newDelay, false);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void maybeDoImmediateMaintenance() {
+ synchronized (this) {
+ if (mState == STATE_IDLE) {
+ long duration = SystemClock.elapsedRealtime() - mIdleStartTime;
+ /* Let's trgger a immediate maintenance,
+ * if it has been idle for a long time */
+ if (duration > mConstants.IDLE_TIMEOUT) {
+ scheduleAlarmLocked(0, false);
+ }
+ }
+ }
+ }
+
+ private boolean shouldUseIdleTimeoutFactorLocked() {
+ // exclude ACTIVE_REASON_MOTION, for exclude device in pocket case
+ if (mActiveReason == ACTIVE_REASON_MOTION) {
+ return false;
+ }
+ return true;
+ }
+
+ /** Must only be used in tests. */
+ @VisibleForTesting
+ void setIdleStartTimeForTest(long idleStartTime) {
+ synchronized (this) {
+ mIdleStartTime = idleStartTime;
+ }
+ }
+
void reportMaintenanceActivityIfNeededLocked() {
boolean active = mJobsActive;
if (active == mReportedMaintenanceActivity) {
@@ -2945,6 +3173,11 @@
mHandler.sendMessage(msg);
}
+ @VisibleForTesting
+ long getNextAlarmTime() {
+ return mNextAlarmTime;
+ }
+
boolean isOpsInactiveLocked() {
return mActiveIdleOpCount <= 0 && !mJobsActive && !mAlarmsActive;
}
@@ -2994,6 +3227,7 @@
scheduleReportActiveLocked(type, Process.myUid());
addEvent(EVENT_NORMAL, type);
}
+ mActiveReason = ACTIVE_REASON_MOTION;
mState = STATE_ACTIVE;
mInactiveTimeout = timeout;
mCurIdleBudget = 0;
@@ -3401,6 +3635,11 @@
+ "and any [-d] is ignored");
pw.println(" motion");
pw.println(" Simulate a motion event to bring the device out of deep doze");
+ pw.println(" pre-idle-factor [0|1|2]");
+ pw.println(" Set a new factor to idle time before step to idle"
+ + "(inactive_to and idle_after_inactive_to)");
+ pw.println(" reset-pre-idle-factor");
+ pw.println(" Reset factor to idle time to default");
}
class Shell extends ShellCommand {
@@ -3571,6 +3810,7 @@
}
}
if (becomeActive) {
+ mActiveReason = ACTIVE_REASON_FORCED;
becomeActiveLocked((arg == null ? "all" : arg) + "-disabled",
Process.myUid());
}
@@ -3820,6 +4060,52 @@
Binder.restoreCallingIdentity(token);
}
}
+ } else if ("pre-idle-factor".equals(cmd)) {
+ getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER,
+ null);
+ synchronized (this) {
+ long token = Binder.clearCallingIdentity();
+ int ret = SET_IDLE_FACTOR_RESULT_UNINIT;
+ try {
+ String arg = shell.getNextArg();
+ boolean valid = false;
+ int mode = 0;
+ if (arg != null) {
+ mode = Integer.parseInt(arg);
+ ret = setPreIdleTimeoutMode(mode);
+ if (ret == SET_IDLE_FACTOR_RESULT_OK) {
+ pw.println("pre-idle-factor: " + mode);
+ valid = true;
+ } else if (ret == SET_IDLE_FACTOR_RESULT_NOT_SUPPORT) {
+ valid = true;
+ pw.println("Deep idle not supported");
+ } else if (ret == SET_IDLE_FACTOR_RESULT_IGNORED) {
+ valid = true;
+ pw.println("Idle timeout factor not changed");
+ }
+ }
+ if (!valid) {
+ pw.println("Unknown idle timeout factor: " + arg
+ + ",(error code: " + ret + ")");
+ }
+ } catch (NumberFormatException e) {
+ pw.println("Unknown idle timeout factor"
+ + ",(error code: " + ret + ")");
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ } else if ("reset-pre-idle-factor".equals(cmd)) {
+ getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER,
+ null);
+ synchronized (this) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ resetPreIdleTimeoutMode();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
} else {
return shell.handleDefaultCommands(cmd);
}
@@ -4053,6 +4339,9 @@
if (mAlarmsActive) {
pw.print(" mAlarmsActive="); pw.println(mAlarmsActive);
}
+ if (Math.abs(mPreIdleFactor - 1.0f) > MIN_PRE_IDLE_FACTOR_CHANGE) {
+ pw.print(" mPreIdleFactor="); pw.println(mPreIdleFactor);
+ }
}
}
diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/NetworkManagementService.java
index c84389b..2ce2f8d 100644
--- a/services/core/java/com/android/server/NetworkManagementService.java
+++ b/services/core/java/com/android/server/NetworkManagementService.java
@@ -46,7 +46,9 @@
import android.app.ActivityManager;
import android.content.Context;
import android.net.ConnectivityManager;
+import android.net.InetAddresses;
import android.net.INetd;
+import android.net.INetdUnsolicitedEventListener;
import android.net.INetworkManagementEventObserver;
import android.net.ITetheringStatsProvider;
import android.net.InterfaceConfiguration;
@@ -205,6 +207,8 @@
private INetd mNetdService;
+ private final NetdUnsolicitedEventListener mNetdUnsolicitedEventListener;
+
private IBatteryStats mBatteryStats;
private final Thread mThread;
@@ -322,6 +326,8 @@
mDaemonHandler = new Handler(FgThread.get().getLooper());
+ mNetdUnsolicitedEventListener = new NetdUnsolicitedEventListener();
+
// Add ourself to the Watchdog monitors.
Watchdog.getInstance().addMonitor(this);
@@ -340,6 +346,7 @@
mFgHandler = null;
mThread = null;
mServices = null;
+ mNetdUnsolicitedEventListener = null;
}
static NetworkManagementService create(Context context, String socket, SystemServices services)
@@ -446,7 +453,6 @@
// our sanity-checking state.
mActiveAlerts.remove(iface);
mActiveQuotas.remove(iface);
-
invokeForAllObservers(o -> o.interfaceRemoved(iface));
}
@@ -552,7 +558,7 @@
return;
}
// No current code examines the interface parameter in a global alert. Just pass null.
- notifyLimitReached(LIMIT_GLOBAL_ALERT, null);
+ mDaemonHandler.post(() -> notifyLimitReached(LIMIT_GLOBAL_ALERT, null));
}
}
@@ -583,6 +589,12 @@
private void connectNativeNetdService() {
mNetdService = mServices.getNetd();
+ try {
+ mNetdService.registerUnsolicitedEventListener(mNetdUnsolicitedEventListener);
+ if (DBG) Slog.d(TAG, "Register unsolicited event listener");
+ } catch (RemoteException | ServiceSpecificException e) {
+ Slog.e(TAG, "Failed to set Netd unsolicited event listener " + e);
+ }
}
/**
@@ -709,14 +721,96 @@
/**
* Notify our observers of a route change.
*/
- private void notifyRouteChange(String action, RouteInfo route) {
- if (action.equals("updated")) {
+ private void notifyRouteChange(boolean updated, RouteInfo route) {
+ if (updated) {
invokeForAllObservers(o -> o.routeUpdated(route));
} else {
invokeForAllObservers(o -> o.routeRemoved(route));
}
}
+ private class NetdUnsolicitedEventListener extends INetdUnsolicitedEventListener.Stub {
+ @Override
+ public void onInterfaceClassActivityChanged(boolean isActive,
+ int label, long timestamp, int uid) throws RemoteException {
+ final long timestampNanos;
+ if (timestamp <= 0) {
+ timestampNanos = SystemClock.elapsedRealtimeNanos();
+ } else {
+ timestampNanos = timestamp;
+ }
+ mDaemonHandler.post(() -> notifyInterfaceClassActivity(label,
+ isActive ? DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH
+ : DataConnectionRealTimeInfo.DC_POWER_STATE_LOW,
+ timestampNanos, uid, false));
+ }
+
+ @Override
+ public void onQuotaLimitReached(String alertName, String ifName)
+ throws RemoteException {
+ mDaemonHandler.post(() -> notifyLimitReached(alertName, ifName));
+ }
+
+ @Override
+ public void onInterfaceDnsServerInfo(String ifName,
+ long lifetime, String[] servers) throws RemoteException {
+ mDaemonHandler.post(() -> notifyInterfaceDnsServerInfo(ifName, lifetime, servers));
+ }
+
+ @Override
+ public void onInterfaceAddressUpdated(String addr,
+ String ifName, int flags, int scope) throws RemoteException {
+ final LinkAddress address = new LinkAddress(addr, flags, scope);
+ mDaemonHandler.post(() -> notifyAddressUpdated(ifName, address));
+ }
+
+ @Override
+ public void onInterfaceAddressRemoved(String addr,
+ String ifName, int flags, int scope) throws RemoteException {
+ final LinkAddress address = new LinkAddress(addr, flags, scope);
+ mDaemonHandler.post(() -> notifyAddressRemoved(ifName, address));
+ }
+
+ @Override
+ public void onInterfaceAdded(String ifName) throws RemoteException {
+ mDaemonHandler.post(() -> notifyInterfaceAdded(ifName));
+ }
+
+ @Override
+ public void onInterfaceRemoved(String ifName) throws RemoteException {
+ mDaemonHandler.post(() -> notifyInterfaceRemoved(ifName));
+ }
+
+ @Override
+ public void onInterfaceChanged(String ifName, boolean up)
+ throws RemoteException {
+ mDaemonHandler.post(() -> notifyInterfaceStatusChanged(ifName, up));
+ }
+
+ @Override
+ public void onInterfaceLinkStateChanged(String ifName, boolean up)
+ throws RemoteException {
+ mDaemonHandler.post(() -> notifyInterfaceLinkStateChanged(ifName, up));
+ }
+
+ @Override
+ public void onRouteChanged(boolean updated,
+ String route, String gateway, String ifName) throws RemoteException {
+ final RouteInfo processRoute = new RouteInfo(new IpPrefix(route),
+ ("".equals(gateway)) ? null : InetAddresses.parseNumericAddress(gateway),
+ ifName);
+ mDaemonHandler.post(() -> notifyRouteChange(updated, processRoute));
+ }
+
+ @Override
+ public void onStrictCleartextDetected(int uid, String hex) throws RemoteException {
+ // Don't need to post to mDaemonHandler because the only thing
+ // that notifyCleartextNetwork does is post to a handler
+ ActivityManager.getService().notifyCleartextNetwork(uid,
+ HexDump.hexStringToByteArray(hex));
+ }
+ }
+
//
// Netd Callback handling
//
@@ -904,7 +998,7 @@
InetAddress gateway = null;
if (via != null) gateway = InetAddress.parseNumericAddress(via);
RouteInfo route = new RouteInfo(new IpPrefix(cooked[3]), gateway, dev);
- notifyRouteChange(cooked[2], route);
+ notifyRouteChange(cooked[2].equals("updated"), route);
return true;
} catch (IllegalArgumentException e) {}
}
@@ -1367,13 +1461,9 @@
if (ConnectivityManager.isNetworkTypeMobile(type)) {
mNetworkActive = false;
}
- mDaemonHandler.post(new Runnable() {
- @Override public void run() {
- notifyInterfaceClassActivity(type,
- DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
- SystemClock.elapsedRealtimeNanos(), -1, false);
- }
- });
+ mDaemonHandler.post(() -> notifyInterfaceClassActivity(type,
+ DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+ SystemClock.elapsedRealtimeNanos(), -1, false));
}
}
@@ -1396,13 +1486,9 @@
throw new IllegalStateException(e);
}
mActiveIdleTimers.remove(iface);
- mDaemonHandler.post(new Runnable() {
- @Override public void run() {
- notifyInterfaceClassActivity(params.type,
- DataConnectionRealTimeInfo.DC_POWER_STATE_LOW,
- SystemClock.elapsedRealtimeNanos(), -1, false);
- }
- });
+ mDaemonHandler.post(() -> notifyInterfaceClassActivity(params.type,
+ DataConnectionRealTimeInfo.DC_POWER_STATE_LOW,
+ SystemClock.elapsedRealtimeNanos(), -1, false));
}
}
diff --git a/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java b/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java
index 1e9a007..190fff1 100644
--- a/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java
+++ b/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java
@@ -31,6 +31,19 @@
*/
byte[] getFrpCredentialHandle();
+ /** Stores the data used to enable the Test Harness Mode after factory-resetting. */
+ void setTestHarnessModeData(byte[] data);
+
+ /**
+ * Retrieves the data used to place the device into Test Harness Mode.
+ *
+ * @throws IllegalStateException if the underlying storage is corrupt or inaccessible.
+ */
+ byte[] getTestHarnessModeData();
+
+ /** Clear out the Test Harness Mode data. */
+ void clearTestHarnessModeData();
+
/** Update the OEM unlock enabled bit, bypassing user restriction checks. */
void forceOemUnlockEnabled(boolean enabled);
}
diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java
index 21093b9..bd5ad96 100644
--- a/services/core/java/com/android/server/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/PersistentDataBlockService.java
@@ -16,6 +16,8 @@
package com.android.server;
+import static com.android.internal.util.Preconditions.checkArgument;
+
import android.Manifest;
import android.app.ActivityManager;
import android.content.Context;
@@ -28,12 +30,10 @@
import android.os.UserManager;
import android.service.persistentdata.IPersistentDataBlockService;
import android.service.persistentdata.PersistentDataBlockManager;
-import android.util.Log;
import android.util.Slog;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.Preconditions;
import libcore.io.IoUtils;
@@ -65,6 +65,40 @@
*
* Clients can read any number of bytes from the currently written block up to its total size by
* invoking {@link IPersistentDataBlockService#read}
+ *
+ * The persistent data block is currently laid out as follows:
+ * | ---------BEGINNING OF PARTITION-------------|
+ * | Partition digest (32 bytes) |
+ * | --------------------------------------------|
+ * | PARTITION_TYPE_MARKER (4 bytes) |
+ * | --------------------------------------------|
+ * | FRP data block length (4 bytes) |
+ * | --------------------------------------------|
+ * | FRP data (variable length) |
+ * | --------------------------------------------|
+ * | ... |
+ * | --------------------------------------------|
+ * | Test mode data block (10000 bytes) |
+ * | --------------------------------------------|
+ * | | Test mode data length (4 bytes) |
+ * | --------------------------------------------|
+ * | | Test mode data (variable length) |
+ * | | ... |
+ * | --------------------------------------------|
+ * | FRP credential handle block (1000 bytes) |
+ * | --------------------------------------------|
+ * | | FRP credential handle length (4 bytes)|
+ * | --------------------------------------------|
+ * | | FRP credential handle (variable len) |
+ * | | ... |
+ * | --------------------------------------------|
+ * | OEM Unlock bit (1 byte) |
+ * | ---------END OF PARTITION-------------------|
+ *
+ * TODO: now that the persistent partition contains several blocks, next time someone wants a new
+ * block, we should look at adding more generic block definitions and get rid of the various raw
+ * XXX_RESERVED_SIZE and XXX_DATA_SIZE constants. That will ensure the code is easier to maintain
+ * and less likely to introduce out-of-bounds read/write.
*/
public class PersistentDataBlockService extends SystemService {
private static final String TAG = PersistentDataBlockService.class.getSimpleName();
@@ -73,10 +107,16 @@
private static final int HEADER_SIZE = 8;
// Magic number to mark block device as adhering to the format consumed by this service
private static final int PARTITION_TYPE_MARKER = 0x19901873;
- /** Size of the block reserved for FPR credential, including 4 bytes for the size header. */
+ /** Size of the block reserved for FRP credential, including 4 bytes for the size header. */
private static final int FRP_CREDENTIAL_RESERVED_SIZE = 1000;
/** Maximum size of the FRP credential handle that can be stored. */
private static final int MAX_FRP_CREDENTIAL_HANDLE_SIZE = FRP_CREDENTIAL_RESERVED_SIZE - 4;
+ /**
+ * Size of the block reserved for Test Harness Mode data, including 4 bytes for the size header.
+ */
+ private static final int TEST_MODE_RESERVED_SIZE = 10000;
+ /** Maximum size of the Test Harness Mode data that can be stored. */
+ private static final int MAX_TEST_MODE_DATA_SIZE = TEST_MODE_RESERVED_SIZE - 4;
// Limit to 100k as blocks larger than this might cause strain on Binder.
private static final int MAX_DATA_BLOCK_SIZE = 1024 * 100;
@@ -221,6 +261,14 @@
return mBlockDeviceSize;
}
+ private long getFrpCredentialDataOffset() {
+ return getBlockDeviceSize() - 1 - FRP_CREDENTIAL_RESERVED_SIZE;
+ }
+
+ private long getTestHarnessModeDataOffset() {
+ return getFrpCredentialDataOffset() - TEST_MODE_RESERVED_SIZE;
+ }
+
private boolean enforceChecksumValidity() {
byte[] storedDigest = new byte[DIGEST_SIZE_BYTES];
@@ -383,7 +431,7 @@
private long doGetMaximumDataBlockSize() {
long actualSize = getBlockDeviceSize() - HEADER_SIZE - DIGEST_SIZE_BYTES
- - FRP_CREDENTIAL_RESERVED_SIZE - 1;
+ - TEST_MODE_RESERVED_SIZE - FRP_CREDENTIAL_RESERVED_SIZE - 1;
return actualSize <= MAX_DATA_BLOCK_SIZE ? actualSize : MAX_DATA_BLOCK_SIZE;
}
@@ -391,6 +439,13 @@
private native int nativeWipe(String path);
private final IBinder mService = new IPersistentDataBlockService.Stub() {
+
+ /**
+ * Write the data to the persistent data block.
+ *
+ * @return a positive integer of the number of bytes that were written if successful,
+ * otherwise a negative integer indicating there was a problem
+ */
@Override
public int write(byte[] data) throws RemoteException {
enforceUid(Binder.getCallingUid());
@@ -597,12 +652,51 @@
@Override
public void setFrpCredentialHandle(byte[] handle) {
- Preconditions.checkArgument(handle == null || handle.length > 0,
- "handle must be null or non-empty");
- Preconditions.checkArgument(handle == null
- || handle.length <= MAX_FRP_CREDENTIAL_HANDLE_SIZE,
- "handle must not be longer than " + MAX_FRP_CREDENTIAL_HANDLE_SIZE);
+ writeInternal(handle, getFrpCredentialDataOffset(), MAX_FRP_CREDENTIAL_HANDLE_SIZE);
+ }
+ @Override
+ public byte[] getFrpCredentialHandle() {
+ return readInternal(getFrpCredentialDataOffset(), MAX_FRP_CREDENTIAL_HANDLE_SIZE);
+ }
+
+ @Override
+ public void setTestHarnessModeData(byte[] data) {
+ writeInternal(data, getTestHarnessModeDataOffset(), MAX_TEST_MODE_DATA_SIZE);
+ }
+
+ @Override
+ public byte[] getTestHarnessModeData() {
+ byte[] data = readInternal(getTestHarnessModeDataOffset(), MAX_TEST_MODE_DATA_SIZE);
+ if (data == null) {
+ return new byte[0];
+ }
+ return data;
+ }
+
+ @Override
+ public void clearTestHarnessModeData() {
+ int size = Math.min(MAX_TEST_MODE_DATA_SIZE, getTestHarnessModeData().length) + 4;
+ writeDataBuffer(getTestHarnessModeDataOffset(), ByteBuffer.allocate(size));
+ }
+
+ private void writeInternal(byte[] data, long offset, int dataLength) {
+ checkArgument(data == null || data.length > 0, "data must be null or non-empty");
+ checkArgument(
+ data == null || data.length <= dataLength,
+ "data must not be longer than " + dataLength);
+
+ ByteBuffer dataBuffer = ByteBuffer.allocate(dataLength + 4);
+ dataBuffer.putInt(data == null ? 0 : data.length);
+ if (data != null) {
+ dataBuffer.put(data);
+ }
+ dataBuffer.flip();
+
+ writeDataBuffer(offset, dataBuffer);
+ }
+
+ private void writeDataBuffer(long offset, ByteBuffer dataBuffer) {
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(new File(mDataBlockFile));
@@ -610,25 +704,15 @@
Slog.e(TAG, "partition not available", e);
return;
}
-
- ByteBuffer data = ByteBuffer.allocate(FRP_CREDENTIAL_RESERVED_SIZE);
- data.putInt(handle == null ? 0 : handle.length);
- if (handle != null) {
- data.put(handle);
- }
- data.flip();
-
synchronized (mLock) {
if (!mIsWritable) {
IoUtils.closeQuietly(outputStream);
return;
}
-
try {
FileChannel channel = outputStream.getChannel();
-
- channel.position(getBlockDeviceSize() - 1 - FRP_CREDENTIAL_RESERVED_SIZE);
- channel.write(data);
+ channel.position(offset);
+ channel.write(dataBuffer);
outputStream.flush();
} catch (IOException e) {
Slog.e(TAG, "unable to access persistent partition", e);
@@ -641,8 +725,7 @@
}
}
- @Override
- public byte[] getFrpCredentialHandle() {
+ private byte[] readInternal(long offset, int maxLength) {
if (!enforceChecksumValidity()) {
throw new IllegalStateException("invalid checksum");
}
@@ -652,14 +735,14 @@
inputStream = new DataInputStream(
new FileInputStream(new File(mDataBlockFile)));
} catch (FileNotFoundException e) {
- throw new IllegalStateException("frp partition not available");
+ throw new IllegalStateException("persistent partition not available");
}
try {
synchronized (mLock) {
- inputStream.skip(getBlockDeviceSize() - 1 - FRP_CREDENTIAL_RESERVED_SIZE);
+ inputStream.skip(offset);
int length = inputStream.readInt();
- if (length <= 0 || length > MAX_FRP_CREDENTIAL_HANDLE_SIZE) {
+ if (length <= 0 || length > maxLength) {
return null;
}
byte[] bytes = new byte[length];
@@ -667,7 +750,7 @@
return bytes;
}
} catch (IOException e) {
- throw new IllegalStateException("frp handle not readable", e);
+ throw new IllegalStateException("persistent partition not readable", e);
} finally {
IoUtils.closeQuietly(inputStream);
}
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index ac392a1..a96676e 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -82,7 +82,6 @@
import android.webkit.WebViewZygote;
import com.android.internal.R;
-import com.android.internal.app.procstats.ProcessStats;
import com.android.internal.app.procstats.ServiceState;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.notification.SystemNotificationChannels;
@@ -169,8 +168,6 @@
/** Temporary list for holding the results of calls to {@link #collectPackageServicesLocked} */
private ArrayList<ServiceRecord> mTmpCollectionResults = null;
- private int mNumServiceRestarts = 0;
-
/**
* For keeping ActiveForegroundApps retaining state while the screen is off.
*/
@@ -2330,50 +2327,19 @@
r.restartDelay = mAm.mConstants.BOUND_SERVICE_CRASH_RESTART_DURATION
* (r.crashCount - 1);
} else {
- mNumServiceRestarts++;
- if (!mAm.mConstants.FLAG_USE_MEM_AWARE_SERVICE_RESTARTS) {
- // If it has been a "reasonably long time" since the service
- // was started, then reset our restart duration back to
- // the beginning, so we don't infinitely increase the duration
- // on a service that just occasionally gets killed (which is
- // a normal case, due to process being killed to reclaim memory).
- if (now > (r.restartTime + resetTime)) {
- r.restartCount = 1;
- r.restartDelay = minDuration;
- } else {
- r.restartDelay *= mAm.mConstants.SERVICE_RESTART_DURATION_FACTOR;
- if (r.restartDelay < minDuration) {
- r.restartDelay = minDuration;
- }
- }
+ // If it has been a "reasonably long time" since the service
+ // was started, then reset our restart duration back to
+ // the beginning, so we don't infinitely increase the duration
+ // on a service that just occasionally gets killed (which is
+ // a normal case, due to process being killed to reclaim memory).
+ if (now > (r.restartTime+resetTime)) {
+ r.restartCount = 1;
+ r.restartDelay = minDuration;
} else {
- // The service will be restarted based on the last known oom_adj value
- // for the process when it was destroyed. A higher oom_adj value
- // means that the service will be restarted quicker.
- // If the service seems to keep crashing, the service restart counter will be
- // incremented and the restart delay will have an exponential backoff.
- final int lastOomAdj = r.app == null ? r.lastKnownOomAdj : r.app.setAdj;
- if (r.restartCount > 1) {
- r.restartDelay *= mAm.mConstants.SERVICE_RESTART_DURATION_FACTOR;
- if (r.restartDelay < minDuration) {
- r.restartDelay = minDuration;
- }
- } else {
- if (lastOomAdj <= ProcessList.VISIBLE_APP_ADJ) {
- r.restartDelay = 1 * 1000; // 1 second
- } else if (lastOomAdj <= ProcessList.PERCEPTIBLE_APP_ADJ) {
- r.restartDelay = 2 * 1000; // 2 seconds
- } else if (lastOomAdj <= ProcessList.SERVICE_ADJ) {
- r.restartDelay = 5 * 1000; // 5 seconds
- } else if (lastOomAdj <= ProcessList.PREVIOUS_APP_ADJ) {
- r.restartDelay = 10 * 1000; // 10 seconds
- } else {
- r.restartDelay = 20 * 1000; // 20 seconds
- }
+ r.restartDelay *= mAm.mConstants.SERVICE_RESTART_DURATION_FACTOR;
+ if (r.restartDelay < minDuration) {
+ r.restartDelay = minDuration;
}
- // If the last time the service restarted was more than a minute ago,
- // reset the service restart count, otherwise increment by one.
- r.restartCount = (now > (r.restartTime + resetTime)) ? 1 : (r.restartCount + 1);
}
}
@@ -2381,7 +2347,21 @@
// Make sure that we don't end up restarting a bunch of services
// all at the same time.
- ensureSpacedServiceRestarts(r, now);
+ boolean repeat;
+ do {
+ repeat = false;
+ final long restartTimeBetween = mAm.mConstants.SERVICE_MIN_RESTART_TIME_BETWEEN;
+ for (int i=mRestartingServices.size()-1; i>=0; i--) {
+ ServiceRecord r2 = mRestartingServices.get(i);
+ if (r2 != r && r.nextRestartTime >= (r2.nextRestartTime-restartTimeBetween)
+ && r.nextRestartTime < (r2.nextRestartTime+restartTimeBetween)) {
+ r.nextRestartTime = r2.nextRestartTime + restartTimeBetween;
+ r.restartDelay = r.nextRestartTime - now;
+ repeat = true;
+ break;
+ }
+ }
+ } while (repeat);
} else {
// Persistent processes are immediately restarted, so there is no
@@ -2411,24 +2391,6 @@
return canceled;
}
- private void ensureSpacedServiceRestarts(ServiceRecord r, long now) {
- boolean repeat;
- do {
- repeat = false;
- final long restartTimeBetween = mAm.mConstants.SERVICE_MIN_RESTART_TIME_BETWEEN;
- for (int i = mRestartingServices.size() - 1; i >= 0; i--) {
- ServiceRecord r2 = mRestartingServices.get(i);
- if (r2 != r && r.nextRestartTime >= (r2.nextRestartTime - restartTimeBetween)
- && r.nextRestartTime < (r2.nextRestartTime + restartTimeBetween)) {
- r.nextRestartTime = r2.nextRestartTime + restartTimeBetween;
- r.restartDelay = r.nextRestartTime - now;
- repeat = true;
- break;
- }
- }
- } while (repeat);
- }
-
final void performServiceRestartLocked(ServiceRecord r) {
if (!mRestartingServices.contains(r)) {
return;
@@ -2503,26 +2465,6 @@
return null;
}
- // If the service is restarting, check the memory pressure - if it's low or critical, then
- // further delay the restart because it will most likely get killed again; if the pressure
- // is not low or critical, continue restarting.
- if (mAm.mConstants.FLAG_USE_MEM_AWARE_SERVICE_RESTARTS && whileRestarting) {
- final int memLevel = mAm.getMemoryTrimLevel();
- if (memLevel >= ProcessStats.ADJ_MEM_FACTOR_LOW) {
- final long now = SystemClock.uptimeMillis();
- // Delay the restart based on the current memory pressure
- // Default delay duration is 5 seconds
- r.restartDelay = memLevel * mAm.mConstants.SERVICE_RESTART_DELAY_DURATION;
- r.nextRestartTime = now + r.restartDelay;
- ensureSpacedServiceRestarts(r, now);
- mAm.mHandler.removeCallbacks(r.restarter);
- mAm.mHandler.postAtTime(r.restarter, r.nextRestartTime);
- Slog.w(TAG, "Delaying restart of crashed service " + r.shortInstanceName
- + " in " + r.restartDelay + "ms due to continued memory pressure");
- return null;
- }
- }
-
if (DEBUG_SERVICE) {
Slog.v(TAG_SERVICE, "Bringing up " + r + " " + r.intent + " fg=" + r.fgRequired);
}
@@ -3637,7 +3579,6 @@
|| !mAm.mUserController.isUserRunning(sr.userId, 0)) {
bringDownServiceLocked(sr);
} else {
- sr.lastKnownOomAdj = app.setAdj;
boolean canceled = scheduleServiceRestartLocked(sr, true);
// Should the service remain running? Note that in the
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 546975a..dd2b33a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -76,8 +76,6 @@
static final String KEY_COMPACT_THROTTLE_2 = "compact_throttle_2";
static final String KEY_COMPACT_THROTTLE_3 = "compact_throttle_3";
static final String KEY_COMPACT_THROTTLE_4 = "compact_throttle_4";
- static final String KEY_USE_MEM_AWARE_SERVICE_RESTARTS = "use_mem_aware_service_restarts";
- static final String KEY_SERVICE_RESTART_DELAY_DURATION = "service_restart_delay_duration";
private static final int DEFAULT_MAX_CACHED_PROCESSES = 32;
private static final long DEFAULT_BACKGROUND_SETTLE_TIME = 60*1000;
@@ -115,8 +113,6 @@
public static final long DEFAULT_COMPACT_THROTTLE_2 = 10000;
public static final long DEFAULT_COMPACT_THROTTLE_3 = 500;
public static final long DEFAULT_COMPACT_THROTTLE_4 = 10000;
- private static final boolean DEFAULT_USE_MEM_AWARE_SERVICE_RESTARTS = true;
- private static final long DEFAULT_SERVICE_RESTART_DELAY_DURATION = 5 * 1000;
// Maximum number of cached processes we will allow.
public int MAX_CACHED_PROCESSES = DEFAULT_MAX_CACHED_PROCESSES;
@@ -253,12 +249,6 @@
// How long we'll skip second compactAppFull after first compactAppFull
public long COMPACT_THROTTLE_4 = DEFAULT_COMPACT_THROTTLE_4;
- // Use memory aware optimized logic to restart services
- public boolean FLAG_USE_MEM_AWARE_SERVICE_RESTARTS = DEFAULT_USE_MEM_AWARE_SERVICE_RESTARTS;
-
- // How long a service restart will be delayed by if there is memory pressure
- public long SERVICE_RESTART_DELAY_DURATION = DEFAULT_SERVICE_RESTART_DELAY_DURATION;
-
// Indicates whether the activity starts logging is enabled.
// Controlled by Settings.Global.ACTIVITY_STARTS_LOGGING_ENABLED
volatile boolean mFlagActivityStartsLoggingEnabled;
@@ -423,10 +413,6 @@
COMPACT_THROTTLE_2 = mParser.getLong(KEY_COMPACT_THROTTLE_2, DEFAULT_COMPACT_THROTTLE_2);
COMPACT_THROTTLE_3 = mParser.getLong(KEY_COMPACT_THROTTLE_3, DEFAULT_COMPACT_THROTTLE_3);
COMPACT_THROTTLE_4 = mParser.getLong(KEY_COMPACT_THROTTLE_4, DEFAULT_COMPACT_THROTTLE_4);
- FLAG_USE_MEM_AWARE_SERVICE_RESTARTS = mParser.getBoolean(
- KEY_USE_MEM_AWARE_SERVICE_RESTARTS, DEFAULT_USE_MEM_AWARE_SERVICE_RESTARTS);
- SERVICE_RESTART_DELAY_DURATION = mParser.getLong(
- KEY_SERVICE_RESTART_DELAY_DURATION, DEFAULT_SERVICE_RESTART_DELAY_DURATION);
updateMaxCachedProcesses();
}
@@ -519,10 +505,6 @@
pw.println(TOP_TO_FGS_GRACE_DURATION);
pw.print(" "); pw.print(KEY_USE_COMPACTION); pw.print("=");
pw.println(USE_COMPACTION);
- pw.print(" "); pw.print(KEY_USE_MEM_AWARE_SERVICE_RESTARTS); pw.print("=");
- pw.println(FLAG_USE_MEM_AWARE_SERVICE_RESTARTS);
- pw.print(" "); pw.print(KEY_SERVICE_RESTART_DELAY_DURATION); pw.print("=");
- pw.println(SERVICE_RESTART_DELAY_DURATION);
pw.println();
if (mOverrideMaxCachedProcesses >= 0) {
diff --git a/services/core/java/com/android/server/am/AppCompactor.java b/services/core/java/com/android/server/am/AppCompactor.java
index fe27c49..fd402cc 100644
--- a/services/core/java/com/android/server/am/AppCompactor.java
+++ b/services/core/java/com/android/server/am/AppCompactor.java
@@ -64,7 +64,7 @@
final private String COMPACT_ACTION_FILE = "file";
final private String COMPACT_ACTION_ANON = "anon";
- final private String COMPACT_ACTION_FULL = "full";
+ final private String COMPACT_ACTION_FULL = "all";
final private String compactActionSome;
final private String compactActionFull;
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index fc2a444..da5ce1c 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -16,8 +16,10 @@
package com.android.server.am;
-import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
-import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import com.android.internal.app.procstats.ServiceState;
+import com.android.internal.os.BatteryStatsImpl;
+import com.android.server.LocalServices;
+import com.android.server.notification.NotificationManagerInternal;
import android.app.INotificationManager;
import android.app.Notification;
@@ -42,11 +44,6 @@
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
import android.util.proto.ProtoUtils;
-
-import com.android.internal.app.procstats.ServiceState;
-import com.android.internal.os.BatteryStatsImpl;
-import com.android.server.LocalServices;
-import com.android.server.notification.NotificationManagerInternal;
import com.android.server.uri.NeededUriGrants;
import com.android.server.uri.UriPermissionOwner;
@@ -55,6 +52,9 @@
import java.util.List;
import java.util.Objects;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
/**
* A running application service.
*/
@@ -114,7 +114,6 @@
long executingStart; // start time of last execute request.
boolean createdFromFg; // was this service last created due to a foreground process call?
int crashCount; // number of times proc has crashed with service running
- int lastKnownOomAdj; // last known OOM adjustment for process (used if ProcessRecord is null)
int totalRestartCount; // number of times we have had to restart.
int restartCount; // number of restarts performed in a row.
long restartDelay; // delay until next restart attempt.
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index d4b8eb2..944a95d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -18,7 +18,6 @@
import android.annotation.NonNull;
import android.annotation.UserIdInt;
-import android.content.ComponentName;
import android.view.inputmethod.InputMethodInfo;
import com.android.server.LocalServices;
@@ -42,11 +41,6 @@
public abstract void hideCurrentInputMethod();
/**
- * Switches to VR InputMethod defined in the packageName of {@param componentName}.
- */
- public abstract void startVrInputMethodNoCheck(ComponentName componentName);
-
- /**
* Returns the list of installed input methods for the specified user.
*
* @param userId The user ID to be queried.
@@ -76,10 +70,6 @@
}
@Override
- public void startVrInputMethodNoCheck(ComponentName componentName) {
- }
-
- @Override
public List<InputMethodInfo> getInputMethodListAsUser(int userId) {
return Collections.emptyList();
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 52074a7..eca371a 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -91,8 +91,6 @@
import android.os.UserManager;
import android.os.UserManagerInternal;
import android.provider.Settings;
-import android.service.vr.IVrManager;
-import android.service.vr.IVrStateCallbacks;
import android.text.TextUtils;
import android.text.style.SuggestionSpan;
import android.util.ArrayMap;
@@ -203,7 +201,6 @@
static final int MSG_CREATE_SESSION = 1050;
static final int MSG_START_INPUT = 2000;
- static final int MSG_START_VR_INPUT = 2010;
static final int MSG_UNBIND_CLIENT = 3000;
static final int MSG_BIND_CLIENT = 3010;
@@ -381,28 +378,6 @@
}
}
- /**
- * VR state callback.
- * Listens for when VR mode finishes.
- */
- private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
- @Override
- public void onVrStateChanged(boolean enabled) {
- if (!enabled) {
- restoreNonVrImeFromSettingsNoCheck();
- }
- }
- };
-
- private void restoreNonVrImeFromSettingsNoCheck() {
- // switch back to non-VR InputMethod from settings.
- synchronized (mMethodMap) {
- if (!mIsVrImeStarted) return;
- mIsVrImeStarted = false;
- updateFromSettingsLocked(false);
- }
- }
-
private static final class ClientDeathRecipient implements IBinder.DeathRecipient {
private final InputMethodManagerService mImms;
private final IInputMethodClient mClient;
@@ -597,9 +572,6 @@
final ImeDisplayValidator mImeDisplayValidator;
- /** True if VR IME started by {@link #startVrInputMethodNoCheck}. */
- boolean mIsVrImeStarted;
-
/**
* If non-null, this is the input method service we are currently connected
* to.
@@ -977,31 +949,6 @@
}
/**
- * Start a VR InputMethod that matches IME with package name of {@param component}.
- * Note: This method is called from {@link android.app.VrManager}.
- */
- private void startVrInputMethodNoCheck(@Nullable ComponentName component) {
- if (component == null) {
- // clear the current VR-only IME (if any) and restore normal IME.
- restoreNonVrImeFromSettingsNoCheck();
- return;
- }
-
- synchronized (mMethodMap) {
- String packageName = component.getPackageName();
- for (InputMethodInfo info : mMethodList) {
- if (TextUtils.equals(info.getPackageName(), packageName) && info.isVrOnly()) {
- // set this is as current inputMethod without updating settings.
- setInputMethodEnabledLocked(info.getId(), true);
- setInputMethodLocked(info.getId(), NOT_A_SUBTYPE_ID);
- mIsVrImeStarted = true;
- break;
- }
- }
- }
- }
-
- /**
* Handles {@link Intent#ACTION_LOCALE_CHANGED}.
*
* <p>Note: For historical reasons, {@link Intent#ACTION_LOCALE_CHANGED} has been sent to all
@@ -1452,15 +1399,6 @@
AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId);
mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
mSettings, context);
- // Register VR-state listener.
- IVrManager vrManager = (IVrManager) ServiceManager.getService(Context.VR_SERVICE);
- if (vrManager != null) {
- try {
- vrManager.registerListener(mVrStateCallbacks);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to register VR mode state listener.");
- }
- }
}
private void resetDefaultImeLocked(Context context) {
@@ -1688,25 +1626,7 @@
}
final long ident = Binder.clearCallingIdentity();
try {
- return getInputMethodListLocked(false /* isVrOnly */, resolvedUserIds[0]);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- }
- }
-
- @Override
- public List<InputMethodInfo> getVrInputMethodList() {
- final int callingUserId = UserHandle.getCallingUserId();
- synchronized (mMethodMap) {
- final int[] resolvedUserIds = InputMethodUtils.resolveUserId(callingUserId,
- mSettings.getCurrentUserId(), null);
- if (resolvedUserIds.length != 1) {
- return Collections.emptyList();
- }
- final long ident = Binder.clearCallingIdentity();
- try {
- return getInputMethodListLocked(true /* isVrOnly */, resolvedUserIds[0]);
+ return getInputMethodListLocked(resolvedUserIds[0]);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -1732,8 +1652,7 @@
}
@GuardedBy("mMethodMap")
- private List<InputMethodInfo> getInputMethodListLocked(boolean isVrOnly,
- @UserIdInt int userId) {
+ private List<InputMethodInfo> getInputMethodListLocked(@UserIdInt int userId) {
final ArrayList<InputMethodInfo> methodList;
if (userId == mSettings.getCurrentUserId()) {
// Create a copy.
@@ -1747,7 +1666,6 @@
queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
methodList);
}
- methodList.removeIf(imi -> imi.isVrOnly() != isVrOnly);
return methodList;
}
@@ -2021,8 +1939,8 @@
}
// Compute the final shown display ID with validated cs.selfReportedDisplayId for this
// session & other conditions.
- final int displayIdToShowIme = computeImeDisplayIdForTarget(
- cs.selfReportedDisplayId, mIsVrImeStarted, mImeDisplayValidator);
+ final int displayIdToShowIme = computeImeDisplayIdForTarget(cs.selfReportedDisplayId,
+ mImeDisplayValidator);
if (mCurClient != cs) {
// Was the keyguard locked when switching over to the new client?
@@ -2131,17 +2049,11 @@
* Find the display where the IME should be shown.
*
* @param displayId the ID of the display where the IME client target is.
- * @param isVrImeStarted {@code true} if VR IME started, {@code false} otherwise.
* @param checker instance of {@link ImeDisplayValidator} which is used for
* checking display config to adjust the final target display.
* @return The ID of the display where the IME should be shown.
*/
- static int computeImeDisplayIdForTarget(int displayId, boolean isVrImeStarted,
- @NonNull ImeDisplayValidator checker) {
- // For VR IME, we always show in default display.
- if (isVrImeStarted) {
- return DEFAULT_DISPLAY;
- }
+ static int computeImeDisplayIdForTarget(int displayId, @NonNull ImeDisplayValidator checker) {
if (displayId == DEFAULT_DISPLAY || displayId == INVALID_DISPLAY) {
// We always assume that the default display id suitable to show the IME window.
return DEFAULT_DISPLAY;
@@ -3627,9 +3539,6 @@
case MSG_SET_INTERACTIVE:
handleSetInteractive(msg.arg1 != 0);
return true;
- case MSG_START_VR_INPUT:
- startVrInputMethodNoCheck((ComponentName) msg.obj);
- return true;
case MSG_REPORT_FULLSCREEN_MODE: {
final boolean fullscreen = msg.arg1 != 0;
final ClientState clientState = (ClientState)msg.obj;
@@ -3716,6 +3625,9 @@
try {
final InputMethodInfo imi = new InputMethodInfo(context, ri,
additionalSubtypeMap.get(imeId));
+ if (imi.isVrOnly()) {
+ continue; // Skip VR-only IME, which isn't supported for now.
+ }
methodList.add(imi);
methodMap.put(imi.getId(), imi);
if (DEBUG) {
@@ -4099,17 +4011,7 @@
private void setSelectedInputMethodAndSubtypeLocked(InputMethodInfo imi, int subtypeId,
boolean setSubtypeOnly) {
- // Updates to InputMethod are transient in VR mode. Its not included in history.
- final boolean isVrInput = imi != null && imi.isVrOnly();
- if (!isVrInput) {
- // Update the history of InputMethod and Subtype
- mSettings.saveCurrentInputMethodAndSubtypeToHistory(mCurMethodId, mCurrentSubtype);
- }
-
- if (isVrInput) {
- // Updates to InputMethod are transient in VR mode. Any changes to Settings are skipped.
- return;
- }
+ mSettings.saveCurrentInputMethodAndSubtypeToHistory(mCurMethodId, mCurrentSubtype);
// Set Subtype here
if (imi == null || subtypeId < 0) {
@@ -4225,7 +4127,7 @@
private List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId) {
synchronized (mMethodMap) {
- return getInputMethodListLocked(false, userId);
+ return getInputMethodListLocked(userId);
}
}
@@ -4257,11 +4159,6 @@
}
@Override
- public void startVrInputMethodNoCheck(@Nullable ComponentName componentName) {
- mService.mHandler.obtainMessage(MSG_START_VR_INPUT, componentName).sendToTarget();
- }
-
- @Override
public List<InputMethodInfo> getInputMethodListAsUser(int userId) {
return mService.getInputMethodListAsUser(userId);
}
@@ -4648,7 +4545,7 @@
mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter());
for (int userId : userIds) {
final List<InputMethodInfo> methods = all
- ? getInputMethodListLocked(false, userId)
+ ? getInputMethodListLocked(userId)
: getEnabledInputMethodListLocked(userId);
if (userIds.length > 1) {
pr.print("User #");
diff --git a/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java
index 6f0c5e8..a31b3b4 100644
--- a/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java
@@ -159,11 +159,6 @@
}
@Override
- public void startVrInputMethodNoCheck(ComponentName componentName) {
- reportNotSupported();
- }
-
- @Override
public List<InputMethodInfo> getInputMethodListAsUser(
@UserIdInt int userId) {
return userIdToInputMethodInfoMapper.getAsList(userId);
@@ -1244,13 +1239,6 @@
@BinderThread
@Override
- public List<InputMethodInfo> getVrInputMethodList() {
- reportNotSupported();
- return Collections.emptyList();
- }
-
- @BinderThread
- @Override
public List<InputMethodInfo> getEnabledInputMethodList() {
return mInputMethodInfoMap.getAsList(UserHandle.getUserId(Binder.getCallingUid()));
}
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index cc0ac9a..bd12075 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -443,6 +443,16 @@
"qc_window_size_rare_ms";
private static final String KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS =
"qc_max_execution_time_ms";
+ private static final String KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE =
+ "qc_max_job_count_active";
+ private static final String KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING =
+ "qc_max_job_count_working";
+ private static final String KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT =
+ "qc_max_job_count_frequent";
+ private static final String KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE =
+ "qc_max_job_count_rare";
+ private static final String KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME =
+ "qc_max_count_per_allowed_time";
private static final int DEFAULT_MIN_IDLE_COUNT = 1;
private static final int DEFAULT_MIN_CHARGING_COUNT = 1;
@@ -479,6 +489,15 @@
24 * 60 * 60 * 1000L; // 24 hours
private static final long DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS =
4 * 60 * 60 * 1000L; // 4 hours
+ private static final int DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE =
+ 200; // 1200/hr
+ private static final int DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING =
+ 1200; // 600/hr
+ private static final int DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT =
+ 1800; // 225/hr
+ private static final int DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE =
+ 2400; // 100/hr
+ private static final int DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME = 20;
/**
* Minimum # of idle jobs that must be ready in order to force the JMS to schedule things
@@ -682,6 +701,41 @@
public long QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS =
DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS;
+ /**
+ * The maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE =
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE;
+
+ /**
+ * The maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING =
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING;
+
+ /**
+ * The maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT =
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT;
+
+ /**
+ * The maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE =
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE;
+
+ /**
+ * The maximum number of jobs that can run within the past
+ * {@link #QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS}.
+ */
+ public int QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME =
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME;
+
private final KeyValueListParser mParser = new KeyValueListParser(',');
void updateConstantsLocked(String value) {
@@ -767,6 +821,21 @@
QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = mParser.getDurationMillis(
KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS,
DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS);
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE = mParser.getInt(
+ KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE,
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE);
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING = mParser.getInt(
+ KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING,
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING);
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT = mParser.getInt(
+ KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT,
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT);
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE = mParser.getInt(
+ KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE,
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE);
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME = mParser.getInt(
+ KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME,
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME);
}
void dump(IndentingPrintWriter pw) {
@@ -823,6 +892,16 @@
QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS).println();
pw.printPair(KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS,
QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS).println();
+ pw.printPair(KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE).println();
+ pw.printPair(KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING).println();
+ pw.printPair(KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT).println();
+ pw.printPair(KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE).println();
+ pw.printPair(KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME).println();
pw.decreaseIndent();
}
@@ -872,6 +951,16 @@
QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS);
proto.write(ConstantsProto.QuotaController.MAX_EXECUTION_TIME_MS,
QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_ACTIVE,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_WORKING,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_FREQUENT,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RARE,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_ALLOWED_TIME,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME);
proto.end(qcToken);
proto.end(token);
diff --git a/services/core/java/com/android/server/job/controllers/QuotaController.java b/services/core/java/com/android/server/job/controllers/QuotaController.java
index c16d1b4..5a0b991 100644
--- a/services/core/java/com/android/server/job/controllers/QuotaController.java
+++ b/services/core/java/com/android/server/job/controllers/QuotaController.java
@@ -230,10 +230,10 @@
@VisibleForTesting
static class ExecutionStats {
/**
- * The time at which this record should be considered invalid, in the elapsed realtime
- * timebase.
+ * The time after which this record should be considered invalid (out of date), in the
+ * elapsed realtime timebase.
*/
- public long invalidTimeElapsed;
+ public long expirationTimeElapsed;
public long windowSizeMs;
@@ -241,29 +241,45 @@
public long executionTimeInWindowMs;
public int bgJobCountInWindow;
- /** The total amount of time the app ran in the last {@link MAX_PERIOD_MS}. */
+ /** The total amount of time the app ran in the last {@link #MAX_PERIOD_MS}. */
public long executionTimeInMaxPeriodMs;
public int bgJobCountInMaxPeriod;
/**
- * The time after which the sum of all the app's sessions plus {@link mQuotaBufferMs} equals
- * the quota. This is only valid if
- * executionTimeInWindowMs >= {@link mAllowedTimePerPeriodMs} or
- * executionTimeInMaxPeriodMs >= {@link mMaxExecutionTimeMs}.
+ * The time after which the sum of all the app's sessions plus {@link #mQuotaBufferMs}
+ * equals the quota. This is only valid if
+ * executionTimeInWindowMs >= {@link #mAllowedTimePerPeriodMs} or
+ * executionTimeInMaxPeriodMs >= {@link #mMaxExecutionTimeMs}.
*/
public long quotaCutoffTimeElapsed;
+ /**
+ * The time after which {@link #jobCountInAllowedTime} should be considered invalid, in the
+ * elapsed realtime timebase.
+ */
+ public long jobCountExpirationTimeElapsed;
+
+ /**
+ * The number of jobs that ran in at least the last {@link #mAllowedTimePerPeriodMs}.
+ * It may contain a few stale entries since cleanup won't happen exactly every
+ * {@link #mAllowedTimePerPeriodMs}.
+ */
+ public int jobCountInAllowedTime;
+
@Override
public String toString() {
return new StringBuilder()
- .append("invalidTime=").append(invalidTimeElapsed).append(", ")
+ .append("expirationTime=").append(expirationTimeElapsed).append(", ")
.append("windowSize=").append(windowSizeMs).append(", ")
.append("executionTimeInWindow=").append(executionTimeInWindowMs).append(", ")
.append("bgJobCountInWindow=").append(bgJobCountInWindow).append(", ")
.append("executionTimeInMaxPeriod=").append(executionTimeInMaxPeriodMs)
.append(", ")
.append("bgJobCountInMaxPeriod=").append(bgJobCountInMaxPeriod).append(", ")
- .append("quotaCutoffTime=").append(quotaCutoffTimeElapsed)
+ .append("quotaCutoffTime=").append(quotaCutoffTimeElapsed).append(", ")
+ .append("jobCountExpirationTime").append(jobCountExpirationTimeElapsed)
+ .append(", ")
+ .append("jobCountInAllowedTime").append(jobCountInAllowedTime)
.toString();
}
@@ -271,13 +287,15 @@
public boolean equals(Object obj) {
if (obj instanceof ExecutionStats) {
ExecutionStats other = (ExecutionStats) obj;
- return this.invalidTimeElapsed == other.invalidTimeElapsed
+ return this.expirationTimeElapsed == other.expirationTimeElapsed
&& this.windowSizeMs == other.windowSizeMs
&& this.executionTimeInWindowMs == other.executionTimeInWindowMs
&& this.bgJobCountInWindow == other.bgJobCountInWindow
&& this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs
&& this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod
- && this.quotaCutoffTimeElapsed == other.quotaCutoffTimeElapsed;
+ && this.quotaCutoffTimeElapsed == other.quotaCutoffTimeElapsed
+ && this.jobCountExpirationTimeElapsed == other.jobCountExpirationTimeElapsed
+ && this.jobCountInAllowedTime == other.jobCountInAllowedTime;
} else {
return false;
}
@@ -286,13 +304,15 @@
@Override
public int hashCode() {
int result = 0;
- result = 31 * result + hashLong(invalidTimeElapsed);
+ result = 31 * result + hashLong(expirationTimeElapsed);
result = 31 * result + hashLong(windowSizeMs);
result = 31 * result + hashLong(executionTimeInWindowMs);
result = 31 * result + bgJobCountInWindow;
result = 31 * result + hashLong(executionTimeInMaxPeriodMs);
result = 31 * result + bgJobCountInMaxPeriod;
result = 31 * result + hashLong(quotaCutoffTimeElapsed);
+ result = 31 * result + hashLong(jobCountExpirationTimeElapsed);
+ result = 31 * result + jobCountInAllowedTime;
return result;
}
}
@@ -320,7 +340,7 @@
/**
* List of jobs that started while the UID was in the TOP state. There will be no more than
- * 16 ({@link JobSchedulerService.MAX_JOB_CONTEXTS_COUNT}) running at once, so an ArraySet is
+ * 16 ({@link JobSchedulerService#MAX_JOB_CONTEXTS_COUNT}) running at once, so an ArraySet is
* fine.
*/
private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>();
@@ -343,7 +363,7 @@
private long mAllowedTimePerPeriodMs = 10 * MINUTE_IN_MILLIS;
/**
- * The maximum amount of time an app can have its jobs running within a {@link MAX_PERIOD_MS}
+ * The maximum amount of time an app can have its jobs running within a {@link #MAX_PERIOD_MS}
* window.
*/
private long mMaxExecutionTimeMs = 4 * 60 * MINUTE_IN_MILLIS;
@@ -355,17 +375,20 @@
private long mQuotaBufferMs = 30 * 1000L; // 30 seconds
/**
- * {@link mAllowedTimePerPeriodMs} - {@link mQuotaBufferMs}. This can be used to determine when
- * an app will have enough quota to transition from out-of-quota to in-quota.
+ * {@link #mAllowedTimePerPeriodMs} - {@link #mQuotaBufferMs}. This can be used to determine
+ * when an app will have enough quota to transition from out-of-quota to in-quota.
*/
private long mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
/**
- * {@link mMaxExecutionTimeMs} - {@link mQuotaBufferMs}. This can be used to determine when an
+ * {@link #mMaxExecutionTimeMs} - {@link #mQuotaBufferMs}. This can be used to determine when an
* app will have enough quota to transition from out-of-quota to in-quota.
*/
private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+ /** The maximum number of jobs that can run within the past {@link #mAllowedTimePerPeriodMs}. */
+ private int mMaxJobCountPerAllowedTime = 20;
+
private long mNextCleanupTimeElapsed = 0;
private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener =
new AlarmManager.OnAlarmListener() {
@@ -412,6 +435,23 @@
/** The maximum period any bucket can have. */
private static final long MAX_PERIOD_MS = 24 * 60 * MINUTE_IN_MILLIS;
+ /**
+ * The maximum number of jobs based on its standby bucket. For each max value count in the
+ * array, the app will not be allowed to run more than that many number of jobs within the
+ * latest time interval of its rolling window size.
+ *
+ * @see #mBucketPeriodsMs
+ */
+ private final int[] mMaxBucketJobCounts = new int[] {
+ 200, // ACTIVE -- 1200/hr
+ 1200, // WORKING -- 600/hr
+ 1800, // FREQUENT -- 225/hr
+ 2400 // RARE -- 100/hr
+ };
+
+ /** The minimum number of jobs that any bucket will be allowed to run. */
+ private static final int MIN_BUCKET_JOB_COUNT = 100;
+
/** An app has reached its quota. The message should contain a {@link Package} object. */
private static final int MSG_REACHED_QUOTA = 0;
/** Drop any old timing sessions. */
@@ -463,17 +503,21 @@
@Override
public void prepareForExecutionLocked(JobStatus jobStatus) {
if (DEBUG) Slog.d(TAG, "Prepping for " + jobStatus.toShortString());
+
+ final int uid = jobStatus.getSourceUid();
+ if (mActivityManagerInternal.getUidProcessState(uid) <= ActivityManager.PROCESS_STATE_TOP) {
+ mTopStartedJobs.add(jobStatus);
+ // Top jobs won't count towards quota so there's no need to involve the Timer.
+ return;
+ }
+
final int userId = jobStatus.getSourceUserId();
final String packageName = jobStatus.getSourcePackageName();
- final int uid = jobStatus.getSourceUid();
Timer timer = mPkgTimers.get(userId, packageName);
if (timer == null) {
timer = new Timer(uid, userId, packageName);
mPkgTimers.add(userId, packageName, timer);
}
- if (mActivityManagerInternal.getUidProcessState(uid) == ActivityManager.PROCESS_STATE_TOP) {
- mTopStartedJobs.add(jobStatus);
- }
timer.startTrackingJob(jobStatus);
}
@@ -548,6 +592,36 @@
mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
changed = true;
}
+ int newMaxCountPerAllowedPeriod = Math.max(10,
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME);
+ if (mMaxJobCountPerAllowedTime != newMaxCountPerAllowedPeriod) {
+ mMaxJobCountPerAllowedTime = newMaxCountPerAllowedPeriod;
+ changed = true;
+ }
+ int newActiveMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
+ Math.max(MIN_BUCKET_JOB_COUNT, mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE));
+ if (mMaxBucketJobCounts[ACTIVE_INDEX] != newActiveMaxJobCount) {
+ mMaxBucketJobCounts[ACTIVE_INDEX] = newActiveMaxJobCount;
+ changed = true;
+ }
+ int newWorkingMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
+ Math.max(MIN_BUCKET_JOB_COUNT, mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING));
+ if (mMaxBucketJobCounts[WORKING_INDEX] != newWorkingMaxJobCount) {
+ mMaxBucketJobCounts[WORKING_INDEX] = newWorkingMaxJobCount;
+ changed = true;
+ }
+ int newFrequentMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
+ Math.max(MIN_BUCKET_JOB_COUNT, mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT));
+ if (mMaxBucketJobCounts[FREQUENT_INDEX] != newFrequentMaxJobCount) {
+ mMaxBucketJobCounts[FREQUENT_INDEX] = newFrequentMaxJobCount;
+ changed = true;
+ }
+ int newRareMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
+ Math.max(MIN_BUCKET_JOB_COUNT, mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE));
+ if (mMaxBucketJobCounts[RARE_INDEX] != newRareMaxJobCount) {
+ mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount;
+ changed = true;
+ }
if (changed) {
// Update job bookkeeping out of band.
@@ -631,18 +705,39 @@
return isTopStartedJob(jobStatus)
|| isUidInForeground(jobStatus.getSourceUid())
|| isWithinQuotaLocked(
- jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
+ jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
}
- private boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
+ @VisibleForTesting
+ boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
final int standbyBucket) {
if (standbyBucket == NEVER_INDEX) return false;
// This check is needed in case the flag is toggled after a job has been registered.
if (!mShouldThrottle) return true;
// Quota constraint is not enforced while charging or when parole is on.
- return mChargeTracker.isCharging() || mInParole
- || getRemainingExecutionTimeLocked(userId, packageName, standbyBucket) > 0;
+ if (mChargeTracker.isCharging() || mInParole) {
+ return true;
+ }
+
+ return getRemainingExecutionTimeLocked(userId, packageName, standbyBucket) > 0
+ && isUnderJobCountQuotaLocked(userId, packageName, standbyBucket);
+ }
+
+ private boolean isUnderJobCountQuotaLocked(final int userId, @NonNull final String packageName,
+ final int standbyBucket) {
+ ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket, false);
+ return isUnderJobCountQuotaLocked(stats, standbyBucket);
+ }
+
+ private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats,
+ final int standbyBucket) {
+ final long now = sElapsedRealtimeClock.millis();
+ final boolean isUnderAllowedTimeQuota =
+ (stats.jobCountExpirationTimeElapsed <= now
+ || stats.jobCountInAllowedTime < mMaxJobCountPerAllowedTime);
+ return isUnderAllowedTimeQuota
+ && (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]);
}
@VisibleForTesting
@@ -679,6 +774,13 @@
@NonNull
ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName,
final int standbyBucket) {
+ return getExecutionStatsLocked(userId, packageName, standbyBucket, true);
+ }
+
+ @NonNull
+ private ExecutionStats getExecutionStatsLocked(final int userId,
+ @NonNull final String packageName, final int standbyBucket,
+ final boolean refreshStatsIfOld) {
if (standbyBucket == NEVER_INDEX) {
Slog.wtf(TAG, "getExecutionStatsLocked called for a NEVER app.");
return new ExecutionStats();
@@ -693,14 +795,16 @@
stats = new ExecutionStats();
appStats[standbyBucket] = stats;
}
- final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
- Timer timer = mPkgTimers.get(userId, packageName);
- if ((timer != null && timer.isActive())
- || stats.invalidTimeElapsed <= sElapsedRealtimeClock.millis()
- || stats.windowSizeMs != bucketWindowSizeMs) {
- // The stats are no longer valid.
- stats.windowSizeMs = bucketWindowSizeMs;
- updateExecutionStatsLocked(userId, packageName, stats);
+ if (refreshStatsIfOld) {
+ final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
+ Timer timer = mPkgTimers.get(userId, packageName);
+ if ((timer != null && timer.isActive())
+ || stats.expirationTimeElapsed <= sElapsedRealtimeClock.millis()
+ || stats.windowSizeMs != bucketWindowSizeMs) {
+ // The stats are no longer valid.
+ stats.windowSizeMs = bucketWindowSizeMs;
+ updateExecutionStatsLocked(userId, packageName, stats);
+ }
}
return stats;
@@ -717,14 +821,14 @@
Timer timer = mPkgTimers.get(userId, packageName);
final long nowElapsed = sElapsedRealtimeClock.millis();
- stats.invalidTimeElapsed = nowElapsed + MAX_PERIOD_MS;
+ stats.expirationTimeElapsed = nowElapsed + MAX_PERIOD_MS;
if (timer != null && timer.isActive()) {
stats.executionTimeInWindowMs =
stats.executionTimeInMaxPeriodMs = timer.getCurrentDuration(nowElapsed);
stats.bgJobCountInWindow = stats.bgJobCountInMaxPeriod = timer.getBgJobCount();
// If the timer is active, the value will be stale at the next method call, so
// invalidate now.
- stats.invalidTimeElapsed = nowElapsed;
+ stats.expirationTimeElapsed = nowElapsed;
if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
nowElapsed - mAllowedTimeIntoQuotaMs);
@@ -800,7 +904,7 @@
break;
}
}
- stats.invalidTimeElapsed = nowElapsed + emptyTimeMs;
+ stats.expirationTimeElapsed = nowElapsed + emptyTimeMs;
}
private void invalidateAllExecutionStatsLocked(final int userId,
@@ -811,13 +915,35 @@
for (int i = 0; i < appStats.length; ++i) {
ExecutionStats stats = appStats[i];
if (stats != null) {
- stats.invalidTimeElapsed = nowElapsed;
+ stats.expirationTimeElapsed = nowElapsed;
}
}
}
}
@VisibleForTesting
+ void incrementJobCount(final int userId, @NonNull final String packageName, int count) {
+ final long now = sElapsedRealtimeClock.millis();
+ ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
+ if (appStats == null) {
+ appStats = new ExecutionStats[mBucketPeriodsMs.length];
+ mExecutionStatsCache.add(userId, packageName, appStats);
+ }
+ for (int i = 0; i < appStats.length; ++i) {
+ ExecutionStats stats = appStats[i];
+ if (stats == null) {
+ stats = new ExecutionStats();
+ appStats[i] = stats;
+ }
+ if (stats.jobCountExpirationTimeElapsed <= now) {
+ stats.jobCountExpirationTimeElapsed = now + mAllowedTimePerPeriodMs;
+ stats.jobCountInAllowedTime = 0;
+ }
+ stats.jobCountInAllowedTime += count;
+ }
+ }
+
+ @VisibleForTesting
void saveTimingSession(final int userId, @NonNull final String packageName,
@NonNull final TimingSession session) {
synchronized (mLock) {
@@ -1023,9 +1149,12 @@
final String pkgString = string(userId, packageName);
ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
+ final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket);
+
QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs
- && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs) {
+ && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs
+ && isUnderJobCountQuota) {
// Already in quota. Why was this method called?
if (DEBUG) {
Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
@@ -1042,18 +1171,22 @@
mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
return;
}
+
if (alarmListener == null) {
alarmListener = new QcAlarmListener(userId, packageName);
mInQuotaAlarmListeners.add(userId, packageName, alarmListener);
}
// The time this app will have quota again.
- long inQuotaTimeElapsed =
- stats.quotaCutoffTimeElapsed + stats.windowSizeMs;
+ long inQuotaTimeElapsed = stats.quotaCutoffTimeElapsed + stats.windowSizeMs;
if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeMs) {
inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
stats.quotaCutoffTimeElapsed + MAX_PERIOD_MS);
}
+ if (!isUnderJobCountQuota) {
+ inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
+ stats.jobCountExpirationTimeElapsed + mAllowedTimePerPeriodMs);
+ }
// Only schedule the alarm if:
// 1. There isn't one currently scheduled
// 2. The new alarm is significantly earlier than the previous alarm (which could be the
@@ -1228,6 +1361,7 @@
mRunningBgJobs.add(jobStatus);
if (shouldTrackLocked()) {
mBgJobCount++;
+ incrementJobCount(mPkg.userId, mPkg.packageName, 1);
if (mRunningBgJobs.size() == 1) {
// Started tracking the first job.
mStartTimeElapsed = sElapsedRealtimeClock.millis();
@@ -1324,6 +1458,7 @@
// repeatedly plugged in and unplugged, or an app changes foreground state
// very frequently, the job count for a package may be artificially high.
mBgJobCount = mRunningBgJobs.size();
+ incrementJobCount(mPkg.userId, mPkg.packageName, mBgJobCount);
// Starting the timer means that all cached execution stats are now
// incorrect.
invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
@@ -1604,6 +1739,12 @@
@VisibleForTesting
@NonNull
+ int[] getBucketMaxJobCounts() {
+ return mMaxBucketJobCounts;
+ }
+
+ @VisibleForTesting
+ @NonNull
long[] getBucketWindowSizes() {
return mBucketPeriodsMs;
}
@@ -1631,6 +1772,11 @@
}
@VisibleForTesting
+ int getMaxJobCountPerAllowedTime() {
+ return mMaxJobCountPerAllowedTime;
+ }
+
+ @VisibleForTesting
@Nullable
List<TimingSession> getTimingSessions(int userId, String packageName) {
return mTimingSessions.get(userId, packageName);
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 8734ceb6..a9ae74f 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -947,6 +947,10 @@
public void setSeparateProfileChallengeEnabled(int userId, boolean enabled,
String managedUserPassword) {
checkWritePermission(userId);
+ if (!mLockPatternUtils.hasSecureLockScreen()) {
+ throw new UnsupportedOperationException(
+ "This operation requires secure lock screen feature.");
+ }
synchronized (mSeparateChallengeLock) {
setSeparateProfileChallengeEnabledLocked(userId, enabled, managedUserPassword);
}
@@ -1305,6 +1309,10 @@
public void setLockCredential(String credential, int type, String savedCredential,
int requestedQuality, int userId)
throws RemoteException {
+ if (!mLockPatternUtils.hasSecureLockScreen()) {
+ throw new UnsupportedOperationException(
+ "This operation requires secure lock screen feature");
+ }
checkWritePermission(userId);
synchronized (mSeparateChallengeLock) {
setLockCredentialInternal(credential, type, savedCredential, requestedQuality, userId);
@@ -2906,6 +2914,10 @@
@Override
public boolean setLockCredentialWithToken(String credential, int type, long tokenHandle,
byte[] token, int requestedQuality, int userId) {
+ if (!mLockPatternUtils.hasSecureLockScreen()) {
+ throw new UnsupportedOperationException(
+ "This operation requires secure lock screen feature.");
+ }
try {
return LockSettingsService.this.setLockCredentialWithToken(credential, type,
tokenHandle, token, requestedQuality, userId);
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java
index 07f23ce..6163077 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java
@@ -18,6 +18,7 @@
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+
import static com.android.internal.widget.LockPatternUtils.stringToPattern;
import android.app.ActivityManager;
@@ -58,6 +59,18 @@
mCurrentUserId = ActivityManager.getService().getCurrentUser().id;
parseArgs();
+ if (!mLockPatternUtils.hasSecureLockScreen()) {
+ switch (cmd) {
+ case COMMAND_HELP:
+ case COMMAND_GET_DISABLED:
+ case COMMAND_SET_DISABLED:
+ break;
+ default:
+ getErrPrintWriter().println(
+ "The device does not support lock screen - ignoring the command.");
+ return -1;
+ }
+ }
if (!checkCredential()) {
return -1;
}
diff --git a/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java b/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java
index 2ae424d..5b765df 100644
--- a/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java
+++ b/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java
@@ -22,63 +22,117 @@
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
+import android.os.Process;
import android.os.ServiceManager;
+import android.util.ByteStringUtils;
+import android.util.EventLog;
import android.util.Log;
import com.android.server.pm.dex.DexLogger;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
- * Scheduled job to trigger logging of app dynamic code loading. This runs daily while idle and
- * charging. The actual logging is performed by {@link DexLogger}.
+ * Scheduled jobs related to logging of app dynamic code loading. The idle logging job runs daily
+ * while idle and charging and calls {@link DexLogger} to write dynamic code information to the
+ * event log. The audit watching job scans the event log periodically while idle to find AVC audit
+ * messages indicating use of dynamic native code and adds the information to {@link DexLogger}.
* {@hide}
*/
public class DynamicCodeLoggingService extends JobService {
private static final String TAG = DynamicCodeLoggingService.class.getName();
- private static final int JOB_ID = 2030028;
- private static final long PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1);
-
- private volatile boolean mStopRequested = false;
-
private static final boolean DEBUG = false;
+ private static final int IDLE_LOGGING_JOB_ID = 2030028;
+ private static final int AUDIT_WATCHING_JOB_ID = 203142925;
+
+ private static final long IDLE_LOGGING_PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1);
+ private static final long AUDIT_WATCHING_PERIOD_MILLIS = TimeUnit.HOURS.toMillis(2);
+
+ private static final int AUDIT_AVC = 1400; // Defined in linux/audit.h
+ private static final String AVC_PREFIX = "type=" + AUDIT_AVC + " ";
+
+ private static final Pattern EXECUTE_NATIVE_AUDIT_PATTERN =
+ Pattern.compile(".*\\bavc: granted \\{ execute(?:_no_trans|) \\} .*"
+ + "\\bpath=(?:\"([^\" ]*)\"|([0-9A-F]+)) .*"
+ + "\\bscontext=u:r:untrusted_app_2(?:5|7):.*"
+ + "\\btcontext=u:object_r:app_data_file:.*"
+ + "\\btclass=file\\b.*");
+
+ private volatile boolean mIdleLoggingStopRequested = false;
+ private volatile boolean mAuditWatchingStopRequested = false;
+
/**
- * Schedule our job with the {@link JobScheduler}.
+ * Schedule our jobs with the {@link JobScheduler}.
*/
public static void schedule(Context context) {
ComponentName serviceName = new ComponentName(
"android", DynamicCodeLoggingService.class.getName());
JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
- js.schedule(new JobInfo.Builder(JOB_ID, serviceName)
+ js.schedule(new JobInfo.Builder(IDLE_LOGGING_JOB_ID, serviceName)
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
- .setPeriodic(PERIOD_MILLIS)
+ .setPeriodic(IDLE_LOGGING_PERIOD_MILLIS)
.build());
+ js.schedule(new JobInfo.Builder(AUDIT_WATCHING_JOB_ID, serviceName)
+ .setRequiresDeviceIdle(true)
+ .setRequiresBatteryNotLow(true)
+ .setPeriodic(AUDIT_WATCHING_PERIOD_MILLIS)
+ .build());
+
if (DEBUG) {
- Log.d(TAG, "Job scheduled");
+ Log.d(TAG, "Jobs scheduled");
}
}
@Override
public boolean onStartJob(JobParameters params) {
+ int jobId = params.getJobId();
if (DEBUG) {
- Log.d(TAG, "onStartJob");
+ Log.d(TAG, "onStartJob " + jobId);
}
- mStopRequested = false;
- new IdleLoggingThread(params).start();
- return true; // Job is running on another thread
+ switch (jobId) {
+ case IDLE_LOGGING_JOB_ID:
+ mIdleLoggingStopRequested = false;
+ new IdleLoggingThread(params).start();
+ return true; // Job is running on another thread
+ case AUDIT_WATCHING_JOB_ID:
+ mAuditWatchingStopRequested = false;
+ new AuditWatchingThread(params).start();
+ return true; // Job is running on another thread
+ default:
+ // Shouldn't happen, but indicate nothing is running.
+ return false;
+ }
}
@Override
public boolean onStopJob(JobParameters params) {
+ int jobId = params.getJobId();
if (DEBUG) {
- Log.d(TAG, "onStopJob");
+ Log.d(TAG, "onStopJob " + jobId);
}
- mStopRequested = true;
- return true; // Requests job be re-scheduled.
+ switch (jobId) {
+ case IDLE_LOGGING_JOB_ID:
+ mIdleLoggingStopRequested = true;
+ return true; // Requests job be re-scheduled.
+ case AUDIT_WATCHING_JOB_ID:
+ mAuditWatchingStopRequested = true;
+ return true; // Requests job be re-scheduled.
+ default:
+ return false;
+ }
+ }
+
+ private static DexLogger getDexLogger() {
+ PackageManagerService pm = (PackageManagerService) ServiceManager.getService("package");
+ return pm.getDexManager().getDexLogger();
}
private class IdleLoggingThread extends Thread {
@@ -92,14 +146,13 @@
@Override
public void run() {
if (DEBUG) {
- Log.d(TAG, "Starting logging run");
+ Log.d(TAG, "Starting IdleLoggingJob run");
}
- PackageManagerService pm = (PackageManagerService) ServiceManager.getService("package");
- DexLogger dexLogger = pm.getDexManager().getDexLogger();
+ DexLogger dexLogger = getDexLogger();
for (String packageName : dexLogger.getAllPackagesWithDynamicCodeLoading()) {
- if (mStopRequested) {
- Log.w(TAG, "Stopping logging run at scheduler request");
+ if (mIdleLoggingStopRequested) {
+ Log.w(TAG, "Stopping IdleLoggingJob run at scheduler request");
return;
}
@@ -108,8 +161,128 @@
jobFinished(mParams, /* reschedule */ false);
if (DEBUG) {
- Log.d(TAG, "Finished logging run");
+ Log.d(TAG, "Finished IdleLoggingJob run");
}
}
}
+
+ private class AuditWatchingThread extends Thread {
+ private final JobParameters mParams;
+
+ AuditWatchingThread(JobParameters params) {
+ super("DynamicCodeLoggingService_AuditWatchingJob");
+ mParams = params;
+ }
+
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(TAG, "Starting AuditWatchingJob run");
+ }
+
+ if (processAuditEvents()) {
+ jobFinished(mParams, /* reschedule */ false);
+ if (DEBUG) {
+ Log.d(TAG, "Finished AuditWatchingJob run");
+ }
+ }
+ }
+
+ private boolean processAuditEvents() {
+ // Scan the event log for SELinux (avc) audit messages indicating when an
+ // (untrusted) app has executed native code from an app data
+ // file. Matches are recorded in DexLogger.
+ //
+ // These messages come from the kernel audit system via logd. (Note that
+ // some devices may not generate these messages at all, or the format may
+ // be different, in which case nothing will be recorded.)
+ //
+ // The messages use the auditd tag and the uid of the app that executed
+ // the code.
+ //
+ // A typical message might look like this:
+ // type=1400 audit(0.0:521): avc: granted { execute } for comm="executable"
+ // path="/data/data/com.dummy.app/executable" dev="sda13" ino=1655302
+ // scontext=u:r:untrusted_app_27:s0:c66,c257,c512,c768
+ // tcontext=u:object_r:app_data_file:s0:c66,c257,c512,c768 tclass=file
+ //
+ // The information we want is the uid and the path. (Note this may be
+ // either a quoted string, as shown above, or a sequence of hex-encoded
+ // bytes.)
+ //
+ // On each run we process all the matching events in the log. This may
+ // mean re-processing events we have already seen, and in any case there
+ // may be duplicate events for the same app+file. These are de-duplicated
+ // by DexLogger.
+ //
+ // Note that any app can write a message to the event log, including one
+ // that looks exactly like an AVC audit message, so the information may
+ // be spoofed by an app; in such a case the uid we see will be the app
+ // that generated the spoof message.
+
+ try {
+ int[] tags = { EventLog.getTagCode("auditd") };
+ if (tags[0] == -1) {
+ // auditd is not a registered tag on this system, so there can't be any messages
+ // of interest.
+ return true;
+ }
+
+ DexLogger dexLogger = getDexLogger();
+
+ List<EventLog.Event> events = new ArrayList<>();
+ EventLog.readEvents(tags, events);
+
+ for (int i = 0; i < events.size(); ++i) {
+ if (mAuditWatchingStopRequested) {
+ Log.w(TAG, "Stopping AuditWatchingJob run at scheduler request");
+ return false;
+ }
+
+ EventLog.Event event = events.get(i);
+
+ // Discard clearly unrelated messages as quickly as we can.
+ int uid = event.getUid();
+ if (!Process.isApplicationUid(uid)) {
+ continue;
+ }
+ Object data = event.getData();
+ if (!(data instanceof String)) {
+ continue;
+ }
+ String message = (String) data;
+ if (!message.startsWith(AVC_PREFIX)) {
+ continue;
+ }
+
+ // And then use a regular expression to verify it's one of the messages we're
+ // interested in and to extract the path of the file being loaded.
+ Matcher matcher = EXECUTE_NATIVE_AUDIT_PATTERN.matcher(message);
+ if (!matcher.matches()) {
+ continue;
+ }
+ String path = matcher.group(1);
+ if (path == null) {
+ // If the path contains spaces or various weird characters the kernel
+ // hex-encodes the bytes; we need to undo that.
+ path = unhex(matcher.group(2));
+ }
+ dexLogger.recordNative(uid, path);
+ }
+
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "AuditWatchingJob failed", e);
+ return true;
+ }
+ }
+ }
+
+ private static String unhex(String hexEncodedPath) {
+ byte[] bytes = ByteStringUtils.fromHexToByteArray(hexEncodedPath);
+ if (bytes == null || bytes.length == 0) {
+ return "";
+ }
+ return new String(bytes);
+ }
}
diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java
index 8a6105c..efafdfa 100644
--- a/services/core/java/com/android/server/pm/Installer.java
+++ b/services/core/java/com/android/server/pm/Installer.java
@@ -611,6 +611,31 @@
}
}
+ public boolean snapshotAppData(String pkg, @UserIdInt int userId, int storageFlags)
+ throws InstallerException {
+ if (!checkBeforeRemote()) return false;
+
+ try {
+ mInstalld.snapshotAppData(null, pkg, userId, storageFlags);
+ return true;
+ } catch (Exception e) {
+ throw InstallerException.from(e);
+ }
+ }
+
+ public boolean restoreAppDataSnapshot(String pkg, @AppIdInt int appId, long ceDataInode,
+ String seInfo, @UserIdInt int userId, int storageFlags) throws InstallerException {
+ if (!checkBeforeRemote()) return false;
+
+ try {
+ mInstalld.restoreAppDataSnapshot(null, pkg, appId, ceDataInode, seInfo, userId,
+ storageFlags);
+ return true;
+ } catch (Exception e) {
+ throw InstallerException.from(e);
+ }
+ }
+
private static void assertValidInstructionSet(String instructionSet)
throws InstallerException {
for (String abi : Build.SUPPORTED_ABIS) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 09fe26d..b8342cf 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -42,6 +42,7 @@
import static android.content.pm.PackageManager.FLAG_PERMISSION_SYSTEM_FIXED;
import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_FIXED;
import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET;
+import static android.content.pm.PackageManager.INSTALL_ALLOW_DOWNGRADE;
import static android.content.pm.PackageManager.INSTALL_FAILED_ALREADY_EXISTS;
import static android.content.pm.PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE;
import static android.content.pm.PackageManager.INSTALL_FAILED_DUPLICATE_PERMISSION;
@@ -201,6 +202,7 @@
import android.content.pm.dex.ArtManager;
import android.content.pm.dex.DexMetadataHelper;
import android.content.pm.dex.IArtManager;
+import android.content.rollback.IRollbackManager;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Bitmap;
@@ -13872,6 +13874,38 @@
}
}
+ // If this is an update to a package that might be potentially downgraded, then we
+ // need to check with the rollback manager whether there's any userdata that might
+ // need to be restored for the package.
+ //
+ // TODO(narayan): Get this working for cases where userId == UserHandle.USER_ALL.
+ if (res.returnCode == PackageManager.INSTALL_SUCCEEDED && !doRestore && update) {
+ IRollbackManager rm = IRollbackManager.Stub.asInterface(
+ ServiceManager.getService(Context.ROLLBACK_SERVICE));
+
+ final String packageName = res.pkg.applicationInfo.packageName;
+ final String seInfo = res.pkg.applicationInfo.seInfo;
+ final PackageSetting ps;
+ int appId = -1;
+ long ceDataInode = -1;
+ synchronized (mSettings) {
+ ps = mSettings.getPackageLPr(packageName);
+ if (ps != null) {
+ appId = ps.appId;
+ ceDataInode = ps.getCeDataInode(userId);
+ }
+ }
+
+ if (ps != null) {
+ try {
+ rm.restoreUserData(packageName, userId, appId, ceDataInode, seInfo, token);
+ } catch (RemoteException re) {
+ // Cannot happen, the RollbackManager is hosted in the same process.
+ }
+ doRestore = true;
+ }
+ }
+
if (!doRestore) {
// No restore possible, or the Backup Manager was mysteriously not
// available -- just fire the post-install work request directly.
@@ -14569,6 +14603,9 @@
enableRollbackIntent.putExtra(
PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_INSTALL_FLAGS,
installFlags);
+ enableRollbackIntent.putExtra(
+ PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_INSTALLED_USERS,
+ resolveUserIds(args.user.getIdentifier()));
enableRollbackIntent.setDataAndType(Uri.fromFile(new File(origin.resolvedPath)),
PACKAGE_MIME_TYPE);
enableRollbackIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@@ -23791,6 +23828,11 @@
}
return mArtManagerService.compileLayouts(pkg);
}
+
+ @Override
+ public void finishPackageInstall(int token, boolean didLaunch) {
+ PackageManagerService.this.finishPackageInstall(token, didLaunch);
+ }
}
@GuardedBy("mPackages")
diff --git a/services/core/java/com/android/server/pm/dex/DexLogger.java b/services/core/java/com/android/server/pm/dex/DexLogger.java
index 78fa82c..59cc0cf 100644
--- a/services/core/java/com/android/server/pm/dex/DexLogger.java
+++ b/services/core/java/com/android/server/pm/dex/DexLogger.java
@@ -16,11 +16,15 @@
package com.android.server.pm.dex;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.FILE_TYPE_DEX;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.FILE_TYPE_NATIVE;
+
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.os.FileUtils;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.util.ByteStringUtils;
import android.util.EventLog;
@@ -35,20 +39,23 @@
import com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
import java.io.File;
+import java.io.IOException;
import java.util.Map;
import java.util.Set;
/**
- * This class is responsible for logging data about secondary dex files.
- * The data logged includes hashes of the name and content of each file.
+ * This class is responsible for logging data about secondary dex files and, despite the name,
+ * native code executed from an app's private directory. The data logged includes hashes of the
+ * name and content of each file.
*/
public class DexLogger {
private static final String TAG = "DexLogger";
- // Event log tag & subtag used for SafetyNet logging of dynamic
- // code loading (DCL) - see b/63927552.
+ // Event log tag & subtags used for SafetyNet logging of dynamic code loading (DCL) -
+ // see b/63927552.
private static final int SNET_TAG = 0x534e4554;
- private static final String DCL_SUBTAG = "dcl";
+ private static final String DCL_DEX_SUBTAG = "dcl";
+ private static final String DCL_NATIVE_SUBTAG = "dcln";
private final IPackageManager mPackageManager;
private final PackageDynamicCodeLoading mPackageDynamicCodeLoading;
@@ -114,12 +121,11 @@
}
int storageFlags;
- if (appInfo.deviceProtectedDataDir != null
- && FileUtils.contains(appInfo.deviceProtectedDataDir, filePath)) {
- storageFlags = StorageManager.FLAG_STORAGE_DE;
- } else if (appInfo.credentialProtectedDataDir != null
- && FileUtils.contains(appInfo.credentialProtectedDataDir, filePath)) {
+
+ if (fileIsUnder(filePath, appInfo.credentialProtectedDataDir)) {
storageFlags = StorageManager.FLAG_STORAGE_CE;
+ } else if (fileIsUnder(filePath, appInfo.deviceProtectedDataDir)) {
+ storageFlags = StorageManager.FLAG_STORAGE_DE;
} else {
Slog.e(TAG, "Could not infer CE/DE storage for path " + filePath);
needWrite |= mPackageDynamicCodeLoading.removeFile(packageName, filePath, userId);
@@ -139,6 +145,9 @@
+ ": " + e.getMessage());
}
+ String subtag = fileInfo.mFileType == FILE_TYPE_DEX
+ ? DCL_DEX_SUBTAG
+ : DCL_NATIVE_SUBTAG;
String fileName = new File(filePath).getName();
String message = PackageUtils.computeSha256Digest(fileName.getBytes());
@@ -165,7 +174,7 @@
}
if (loadingUid != -1) {
- writeDclEvent(loadingUid, message);
+ writeDclEvent(subtag, loadingUid, message);
}
}
}
@@ -175,21 +184,58 @@
}
}
+ private boolean fileIsUnder(String filePath, String directoryPath) {
+ if (directoryPath == null) {
+ return false;
+ }
+
+ try {
+ return FileUtils.contains(new File(directoryPath).getCanonicalPath(),
+ new File(filePath).getCanonicalPath());
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
@VisibleForTesting
PackageDynamicCode getPackageDynamicCodeInfo(String packageName) {
return mPackageDynamicCodeLoading.getPackageDynamicCodeInfo(packageName);
}
@VisibleForTesting
- void writeDclEvent(int uid, String message) {
- EventLog.writeEvent(SNET_TAG, DCL_SUBTAG, uid, message);
+ void writeDclEvent(String subtag, int uid, String message) {
+ EventLog.writeEvent(SNET_TAG, subtag, uid, message);
}
- void record(int loaderUserId, String dexPath,
- String owningPackageName, String loadingPackageName) {
+ void recordDex(int loaderUserId, String dexPath, String owningPackageName,
+ String loadingPackageName) {
if (mPackageDynamicCodeLoading.record(owningPackageName, dexPath,
- PackageDynamicCodeLoading.FILE_TYPE_DEX, loaderUserId,
- loadingPackageName)) {
+ FILE_TYPE_DEX, loaderUserId, loadingPackageName)) {
+ mPackageDynamicCodeLoading.maybeWriteAsync();
+ }
+ }
+
+ /**
+ * Record that an app running in the specified uid has executed native code from the file at
+ * {@link path}.
+ */
+ public void recordNative(int loadingUid, String path) {
+ String[] packages;
+ try {
+ packages = mPackageManager.getPackagesForUid(loadingUid);
+ if (packages == null || packages.length == 0) {
+ return;
+ }
+ } catch (RemoteException e) {
+ // Can't happen, we're local.
+ return;
+ }
+
+ String loadingPackageName = packages[0];
+ int loadingUserId = UserHandle.getUserId(loadingUid);
+
+ if (mPackageDynamicCodeLoading.record(loadingPackageName, path,
+ FILE_TYPE_NATIVE, loadingUserId, loadingPackageName)) {
mPackageDynamicCodeLoading.maybeWriteAsync();
}
}
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 b546836..1a2b115 100644
--- a/services/core/java/com/android/server/pm/dex/DexManager.java
+++ b/services/core/java/com/android/server/pm/dex/DexManager.java
@@ -235,7 +235,7 @@
continue;
}
- mDexLogger.record(loaderUserId, dexPath, searchResult.mOwningPackageName,
+ mDexLogger.recordDex(loaderUserId, dexPath, searchResult.mOwningPackageName,
loadingAppInfo.packageName);
if (classLoaderContexts != null) {
diff --git a/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
index 6d4bc82..cc26c9b 100644
--- a/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
+++ b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
@@ -53,6 +53,9 @@
// is represented in the text file format.)
static final int FILE_TYPE_DEX = 'D';
+ // Type code to indicate a secondary file containing native code.
+ static final int FILE_TYPE_NATIVE = 'N';
+
private static final String TAG = "PackageDynamicCodeLoading";
private static final String FILE_VERSION_HEADER = "DCL1";
@@ -107,7 +110,7 @@
*/
boolean record(String owningPackageName, String filePath, int fileType, int ownerUserId,
String loadingPackageName) {
- if (fileType != FILE_TYPE_DEX) {
+ if (!isValidFileType(fileType)) {
throw new IllegalArgumentException("Bad file type: " + fileType);
}
synchronized (mLock) {
@@ -120,6 +123,10 @@
}
}
+ private static boolean isValidFileType(int fileType) {
+ return fileType == FILE_TYPE_DEX || fileType == FILE_TYPE_NATIVE;
+ }
+
/**
* Return all packages that contain records of secondary dex files. (Note that data updates
* asynchronously, so {@link #getPackageDynamicCodeInfo} may still return null if passed
@@ -407,7 +414,7 @@
if (packages.length == 0) {
throw new IOException("Malformed line: " + line);
}
- if (type != FILE_TYPE_DEX) {
+ if (!isValidFileType(type)) {
throw new IOException("Unknown file type: " + line);
}
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index d5af313..20d6d4e 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -1290,6 +1290,7 @@
return mContext.getPackageManager().getPackageInfo(pkg,
DEFAULT_PACKAGE_INFO_QUERY_FLAGS | extraFlags);
} catch (NameNotFoundException e) {
+ Slog.e(TAG, "PackageNot found: " + pkg, e);
return null;
}
}
diff --git a/services/core/java/com/android/server/rollback/RollbackData.java b/services/core/java/com/android/server/rollback/RollbackData.java
index f0589aa..4015c4f 100644
--- a/services/core/java/com/android/server/rollback/RollbackData.java
+++ b/services/core/java/com/android/server/rollback/RollbackData.java
@@ -29,6 +29,11 @@
*/
class RollbackData {
/**
+ * A unique identifier for this rollback.
+ */
+ public final int rollbackId;
+
+ /**
* The per-package rollback information.
*/
public final List<PackageRollbackInfo> packages = new ArrayList<>();
@@ -44,7 +49,8 @@
*/
public Instant timestamp;
- RollbackData(File backupDir) {
+ RollbackData(int rollbackId, File backupDir) {
+ this.rollbackId = rollbackId;
this.backupDir = backupDir;
}
}
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index 9df0f72..7f515bf 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -16,7 +16,6 @@
package com.android.server.rollback;
-import android.Manifest;
import android.app.AppOpsManager;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -32,10 +31,10 @@
import android.content.pm.PackageParser;
import android.content.pm.ParceledListSlice;
import android.content.pm.StringParceledListSlice;
+import android.content.pm.VersionedPackage;
import android.content.rollback.IRollbackManager;
import android.content.rollback.PackageRollbackInfo;
import android.content.rollback.RollbackInfo;
-import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Environment;
@@ -43,14 +42,20 @@
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.storage.StorageManager;
import android.util.Log;
+import android.util.SparseBooleanArray;
import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
+import com.android.server.pm.Installer;
+import com.android.server.pm.Installer.InstallerException;
import com.android.server.pm.PackageManagerServiceUtils;
import java.io.File;
import java.io.IOException;
+import java.security.SecureRandom;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -59,9 +64,9 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Random;
import java.util.Set;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
/**
* Implementation of service that manages APK level rollbacks.
@@ -79,6 +84,13 @@
// mLock is held when they are called.
private final Object mLock = new Object();
+ // Used for generating rollback IDs.
+ private final Random mRandom = new SecureRandom();
+
+ // Set of allocated rollback ids
+ @GuardedBy("mLock")
+ private final SparseBooleanArray mAllocatedRollbackIds = new SparseBooleanArray();
+
// Package rollback data for rollback-enabled installs that have not yet
// been committed. Maps from sessionId to rollback data.
@GuardedBy("mLock")
@@ -103,9 +115,14 @@
private final Context mContext;
private final HandlerThread mHandlerThread;
+ private final Installer mInstaller;
RollbackManagerServiceImpl(Context context) {
mContext = context;
+ // Note that we're calling onStart here because this object is only constructed on
+ // SystemService#onStart.
+ mInstaller = new Installer(mContext);
+ mInstaller.onStart();
mHandlerThread = new HandlerThread("RollbackManagerServiceHandler");
mHandlerThread.start();
@@ -120,8 +137,8 @@
// expiration.
getHandler().post(() -> ensureRollbackDataLoaded());
- PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();
- installer.registerSessionCallback(new SessionCallback(), getHandler());
+ PackageInstaller packageInstaller = mContext.getPackageManager().getPackageInstaller();
+ packageInstaller.registerSessionCallback(new SessionCallback(), getHandler());
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
@@ -158,10 +175,13 @@
PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_TOKEN, -1);
int installFlags = intent.getIntExtra(
PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_INSTALL_FLAGS, 0);
+ int[] installedUsers = intent.getIntArrayExtra(
+ PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_INSTALLED_USERS);
File newPackageCodePath = new File(intent.getData().getPath());
getHandler().post(() -> {
- boolean success = enableRollback(installFlags, newPackageCodePath);
+ boolean success = enableRollback(installFlags, newPackageCodePath,
+ installedUsers);
int ret = PackageManagerInternal.ENABLE_ROLLBACK_SUCCEEDED;
if (!success) {
ret = PackageManagerInternal.ENABLE_ROLLBACK_FAILED;
@@ -198,10 +218,10 @@
// it's out of date or not, so no need to check package versions here.
for (PackageRollbackInfo info : data.packages) {
- if (info.packageName.equals(packageName)) {
+ if (info.getPackageName().equals(packageName)) {
// TODO: Once the RollbackInfo API supports info about
// dependant packages, add that info here.
- return new RollbackInfo(info);
+ return new RollbackInfo(data.rollbackId, info);
}
}
return null;
@@ -219,7 +239,7 @@
for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
RollbackData data = mAvailableRollbacks.get(i);
for (PackageRollbackInfo info : data.packages) {
- packageNames.add(info.packageName);
+ packageNames.add(info.getPackageName());
}
}
}
@@ -261,7 +281,7 @@
*/
private void executeRollbackInternal(RollbackInfo rollback,
String callerPackageName, IntentSender statusReceiver) {
- String targetPackageName = rollback.targetPackage.packageName;
+ String targetPackageName = rollback.targetPackage.getPackageName();
Log.i(TAG, "Initiating rollback of " + targetPackageName);
// Get the latest RollbackData for the target package.
@@ -271,15 +291,9 @@
return;
}
- // Verify the latest rollback matches the version requested.
- // TODO: Check dependant packages too once RollbackInfo includes that
- // information.
- for (PackageRollbackInfo info : data.packages) {
- if (info.packageName.equals(targetPackageName)
- && !rollback.targetPackage.higherVersion.equals(info.higherVersion)) {
- sendFailure(statusReceiver, "Rollback is out of date.");
- return;
- }
+ if (data.rollbackId != rollback.getRollbackId()) {
+ sendFailure(statusReceiver, "Rollback for package is out of date");
+ return;
}
// Verify the RollbackData is up to date with what's installed on
@@ -291,15 +305,14 @@
// Figure out how to ensure we don't commit the rollback if
// roll forward happens at the same time.
for (PackageRollbackInfo info : data.packages) {
- PackageRollbackInfo.PackageVersion installedVersion =
- getInstalledPackageVersion(info.packageName);
+ VersionedPackage installedVersion = getInstalledPackageVersion(info.getPackageName());
if (installedVersion == null) {
// TODO: Test this case
sendFailure(statusReceiver, "Package to roll back is not installed");
return;
}
- if (!info.higherVersion.equals(installedVersion)) {
+ if (!packageVersionsEqual(info.getVersionRolledBackFrom(), installedVersion)) {
// TODO: Test this case
sendFailure(statusReceiver, "Package version to roll back not installed.");
return;
@@ -342,7 +355,7 @@
// TODO: Will it always be called "base.apk"? What about splits?
// What about apex?
- File packageDir = new File(data.backupDir, info.packageName);
+ File packageDir = new File(data.backupDir, info.getPackageName());
File baseApk = new File(packageDir, "base.apk");
try (ParcelFileDescriptor fd = ParcelFileDescriptor.open(baseApk,
ParcelFileDescriptor.MODE_READ_ONLY)) {
@@ -356,28 +369,30 @@
parentSession.addChildSessionId(sessionId);
}
- final LocalIntentReceiver receiver = new LocalIntentReceiver();
+ final LocalIntentReceiver receiver = new LocalIntentReceiver(
+ (Intent result) -> {
+ int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
+ PackageInstaller.STATUS_FAILURE);
+ if (status != PackageInstaller.STATUS_SUCCESS) {
+ sendFailure(statusReceiver, "Rollback downgrade install failed: "
+ + result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE));
+ return;
+ }
+
+ addRecentlyExecutedRollback(rollback);
+ sendSuccess(statusReceiver);
+
+ Intent broadcast = new Intent(Intent.ACTION_PACKAGE_ROLLBACK_EXECUTED);
+
+ // TODO: This call emits the warning "Calling a method in the
+ // system process without a qualified user". Fix that.
+ // TODO: Limit this to receivers holding the
+ // MANAGE_ROLLBACKS permission?
+ mContext.sendBroadcast(broadcast);
+ }
+ );
+
parentSession.commit(receiver.getIntentSender());
-
- Intent result = receiver.getResult();
- int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
- PackageInstaller.STATUS_FAILURE);
- if (status != PackageInstaller.STATUS_SUCCESS) {
- sendFailure(statusReceiver, "Rollback downgrade install failed: "
- + result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE));
- return;
- }
-
- addRecentlyExecutedRollback(rollback);
- sendSuccess(statusReceiver);
-
- Intent broadcast = new Intent(Intent.ACTION_PACKAGE_ROLLBACK_EXECUTED,
- Uri.fromParts("package", targetPackageName,
- Manifest.permission.MANAGE_ROLLBACKS));
-
- // TODO: This call emits the warning "Calling a method in the
- // system process without a qualified user". Fix that.
- mContext.sendBroadcast(broadcast);
} catch (IOException e) {
Log.e(TAG, "Unable to roll back " + targetPackageName, e);
sendFailure(statusReceiver, "IOException: " + e.toString());
@@ -414,7 +429,7 @@
while (iter.hasNext()) {
RollbackData data = iter.next();
for (PackageRollbackInfo info : data.packages) {
- if (info.packageName.equals(packageName)) {
+ if (info.getPackageName().equals(packageName)) {
iter.remove();
mRollbackStore.deleteAvailableRollback(data);
break;
@@ -456,7 +471,15 @@
@GuardedBy("mLock")
private void loadAllRollbackDataLocked() {
mAvailableRollbacks = mRollbackStore.loadAvailableRollbacks();
+ for (RollbackData data : mAvailableRollbacks) {
+ mAllocatedRollbackIds.put(data.rollbackId, true);
+ }
+
mRecentlyExecutedRollbacks = mRollbackStore.loadRecentlyExecutedRollbacks();
+ for (RollbackInfo info : mRecentlyExecutedRollbacks) {
+ mAllocatedRollbackIds.put(info.getRollbackId(), true);
+ }
+
scheduleExpiration(0);
}
@@ -468,8 +491,7 @@
private void onPackageReplaced(String packageName) {
// TODO: Could this end up incorrectly deleting a rollback for a
// package that is about to be installed?
- PackageRollbackInfo.PackageVersion installedVersion =
- getInstalledPackageVersion(packageName);
+ VersionedPackage installedVersion = getInstalledPackageVersion(packageName);
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
@@ -477,8 +499,10 @@
while (iter.hasNext()) {
RollbackData data = iter.next();
for (PackageRollbackInfo info : data.packages) {
- if (info.packageName.equals(packageName)
- && !info.higherVersion.equals(installedVersion)) {
+ if (info.getPackageName().equals(packageName)
+ && !packageVersionsEqual(
+ info.getVersionRolledBackFrom(),
+ installedVersion)) {
iter.remove();
mRollbackStore.deleteAvailableRollback(data);
break;
@@ -501,7 +525,7 @@
boolean changed = false;
while (iter.hasNext()) {
RollbackInfo rollback = iter.next();
- if (packageName.equals(rollback.targetPackage.packageName)) {
+ if (packageName.equals(rollback.targetPackage.getPackageName())) {
iter.remove();
changed = true;
}
@@ -620,12 +644,13 @@
* staged for install with rollback enabled. Called before the package has
* been installed.
*
- * @param id the id of the enable rollback request
* @param installFlags information about what is being installed.
* @param newPackageCodePath path to the package about to be installed.
+ * @param installedUsers the set of users for which a given package is installed.
* @return true if enabling the rollback succeeds, false otherwise.
*/
- private boolean enableRollback(int installFlags, File newPackageCodePath) {
+ private boolean enableRollback(int installFlags, File newPackageCodePath,
+ int[] installedUsers) {
if ((installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
Log.e(TAG, "Rollbacks not supported for instant app install");
return false;
@@ -675,8 +700,7 @@
return false;
}
- PackageRollbackInfo.PackageVersion newVersion =
- new PackageRollbackInfo.PackageVersion(newPackage.versionCode);
+ VersionedPackage newVersion = new VersionedPackage(packageName, newPackage.versionCode);
// Get information about the currently installed package.
PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class);
@@ -687,11 +711,29 @@
Log.e(TAG, packageName + " is not installed");
return false;
}
- PackageRollbackInfo.PackageVersion installedVersion =
- new PackageRollbackInfo.PackageVersion(installedPackage.getLongVersionCode());
+ VersionedPackage installedVersion = new VersionedPackage(packageName,
+ installedPackage.getLongVersionCode());
- PackageRollbackInfo info = new PackageRollbackInfo(
- packageName, newVersion, installedVersion);
+ for (int user : installedUsers) {
+ final int storageFlags;
+ if (StorageManager.isFileEncryptedNativeOrEmulated()
+ && !StorageManager.isUserKeyUnlocked(user)) {
+ // We've encountered a user that hasn't unlocked on a FBE device, so we can't copy
+ // across app user data until the user unlocks their device.
+ Log.e(TAG, "User: " + user + " isn't unlocked, skipping CE userdata backup.");
+ storageFlags = Installer.FLAG_STORAGE_DE;
+ } else {
+ storageFlags = Installer.FLAG_STORAGE_CE | Installer.FLAG_STORAGE_DE;
+ }
+
+ try {
+ mInstaller.snapshotAppData(packageName, user, storageFlags);
+ } catch (InstallerException ie) {
+ Log.e(TAG, "Unable to create app data snapshot for: " + packageName, ie);
+ }
+ }
+
+ PackageRollbackInfo info = new PackageRollbackInfo(newVersion, installedVersion);
RollbackData data;
try {
@@ -699,7 +741,8 @@
mChildSessions.put(childSessionId, parentSessionId);
data = mPendingRollbacks.get(parentSessionId);
if (data == null) {
- data = mRollbackStore.createAvailableRollback();
+ int rollbackId = allocateRollbackIdLocked();
+ data = mRollbackStore.createAvailableRollback(rollbackId);
mPendingRollbacks.put(parentSessionId, data);
}
data.packages.add(info);
@@ -722,40 +765,71 @@
return true;
}
- // TODO: Don't copy this from PackageManagerShellCommand like this?
- private static class LocalIntentReceiver {
- private final LinkedBlockingQueue<Intent> mResult = new LinkedBlockingQueue<>();
+ @Override
+ public void restoreUserData(String packageName, int userId, int appId, long ceDataInode,
+ String seInfo, int token) {
+ if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("restoureUserData may only be called by the system.");
+ }
+
+ getHandler().post(() -> {
+ PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
+ // TODO(narayan): Should we make sure we're in the middle of a session commit for a
+ // a package with this package name ? Otherwise it's possible we may roll back data
+ // for some other downgrade.
+ if (getRollbackForPackage(packageName) == null) {
+ pmi.finishPackageInstall(token, false);
+ return;
+ }
+
+ final int storageFlags;
+ if (StorageManager.isFileEncryptedNativeOrEmulated()
+ && !StorageManager.isUserKeyUnlocked(userId)) {
+ // We've encountered a user that hasn't unlocked on a FBE device, so we can't copy
+ // across app user data until the user unlocks their device.
+ Log.e(TAG, "User: " + userId + " isn't unlocked, skipping CE userdata restore.");
+
+ storageFlags = Installer.FLAG_STORAGE_DE;
+ } else {
+ storageFlags = Installer.FLAG_STORAGE_CE | Installer.FLAG_STORAGE_DE;
+ }
+
+ try {
+ mInstaller.restoreAppDataSnapshot(packageName, appId, ceDataInode,
+ seInfo, userId, storageFlags);
+ } catch (InstallerException ie) {
+ Log.e(TAG, "Unable to restore app data snapshot: " + packageName, ie);
+ }
+
+ pmi.finishPackageInstall(token, false);
+ });
+ }
+
+ private class LocalIntentReceiver {
+ final Consumer<Intent> mConsumer;
+
+ LocalIntentReceiver(Consumer<Intent> consumer) {
+ mConsumer = consumer;
+ }
private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
@Override
public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
- try {
- mResult.offer(intent, 5, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
+ getHandler().post(() -> mConsumer.accept(intent));
}
};
public IntentSender getIntentSender() {
return new IntentSender((IIntentSender) mLocalSender);
}
-
- public Intent getResult() {
- try {
- return mResult.take();
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
}
/**
* Gets the version of the package currently installed.
* Returns null if the package is not currently installed.
*/
- private PackageRollbackInfo.PackageVersion getInstalledPackageVersion(String packageName) {
+ private VersionedPackage getInstalledPackageVersion(String packageName) {
PackageManager pm = mContext.getPackageManager();
PackageInfo pkgInfo = null;
try {
@@ -764,7 +838,12 @@
return null;
}
- return new PackageRollbackInfo.PackageVersion(pkgInfo.getLongVersionCode());
+ return new VersionedPackage(packageName, pkgInfo.getLongVersionCode());
+ }
+
+ private boolean packageVersionsEqual(VersionedPackage a, VersionedPackage b) {
+ return a.getPackageName().equals(b.getPackageName())
+ && a.getLongVersionCode() == b.getLongVersionCode();
}
private class SessionCallback extends PackageInstaller.SessionCallback {
@@ -840,7 +919,7 @@
for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
RollbackData data = mAvailableRollbacks.get(i);
for (PackageRollbackInfo info : data.packages) {
- if (info.packageName.equals(packageName)) {
+ if (info.getPackageName().equals(packageName)) {
return data;
}
}
@@ -848,4 +927,19 @@
}
return null;
}
+
+ @GuardedBy("mLock")
+ private int allocateRollbackIdLocked() throws IOException {
+ int n = 0;
+ int rollbackId;
+ do {
+ rollbackId = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
+ if (!mAllocatedRollbackIds.get(rollbackId, false)) {
+ mAllocatedRollbackIds.put(rollbackId, true);
+ return rollbackId;
+ }
+ } while (n++ < 32);
+
+ throw new IOException("Failed to allocate rollback ID");
+ }
}
diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java
index f9a838f..7738be9 100644
--- a/services/core/java/com/android/server/rollback/RollbackStore.java
+++ b/services/core/java/com/android/server/rollback/RollbackStore.java
@@ -16,6 +16,7 @@
package com.android.server.rollback;
+import android.content.pm.VersionedPackage;
import android.content.rollback.PackageRollbackInfo;
import android.content.rollback.RollbackInfo;
import android.util.Log;
@@ -29,7 +30,6 @@
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
-import java.nio.file.Files;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
@@ -58,7 +58,7 @@
// base.apk
// recently_executed.json
//
- // * XXX, YYY are random strings from Files.createTempDirectory
+ // * XXX, YYY are the rollbackIds for the corresponding rollbacks.
// * rollback.json contains all relevant metadata for the rollback. This
// file is not written until the rollback is made available.
//
@@ -113,13 +113,14 @@
JSONArray array = object.getJSONArray("recentlyExecuted");
for (int i = 0; i < array.length(); ++i) {
JSONObject element = array.getJSONObject(i);
+ int rollbackId = element.getInt("rollbackId");
String packageName = element.getString("packageName");
long higherVersionCode = element.getLong("higherVersionCode");
long lowerVersionCode = element.getLong("lowerVersionCode");
- PackageRollbackInfo target = new PackageRollbackInfo(packageName,
- new PackageRollbackInfo.PackageVersion(higherVersionCode),
- new PackageRollbackInfo.PackageVersion(lowerVersionCode));
- RollbackInfo rollback = new RollbackInfo(target);
+ PackageRollbackInfo target = new PackageRollbackInfo(
+ new VersionedPackage(packageName, higherVersionCode),
+ new VersionedPackage(packageName, lowerVersionCode));
+ RollbackInfo rollback = new RollbackInfo(rollbackId, target);
recentlyExecutedRollbacks.add(rollback);
}
} catch (IOException | JSONException e) {
@@ -135,9 +136,9 @@
/**
* Creates a new RollbackData instance with backupDir assigned.
*/
- RollbackData createAvailableRollback() throws IOException {
- File backupDir = Files.createTempDirectory(mAvailableRollbacksDir.toPath(), null).toFile();
- return new RollbackData(backupDir);
+ RollbackData createAvailableRollback(int rollbackId) throws IOException {
+ File backupDir = new File(mAvailableRollbacksDir, Integer.toString(rollbackId));
+ return new RollbackData(rollbackId, backupDir);
}
/**
@@ -157,11 +158,14 @@
JSONArray packagesJson = new JSONArray();
for (PackageRollbackInfo info : data.packages) {
JSONObject infoJson = new JSONObject();
- infoJson.put("packageName", info.packageName);
- infoJson.put("higherVersionCode", info.higherVersion.versionCode);
- infoJson.put("lowerVersionCode", info.lowerVersion.versionCode);
+ infoJson.put("packageName", info.getPackageName());
+ infoJson.put("higherVersionCode",
+ info.getVersionRolledBackFrom().getLongVersionCode());
+ infoJson.put("lowerVersionCode",
+ info.getVersionRolledBackTo().getVersionCode());
packagesJson.put(infoJson);
}
+ dataJson.put("rollbackId", data.rollbackId);
dataJson.put("packages", packagesJson);
dataJson.put("timestamp", data.timestamp.toString());
@@ -178,6 +182,8 @@
* rollback.
*/
void deleteAvailableRollback(RollbackData data) {
+ // TODO(narayan): Make sure we delete the userdata snapshot along with the backup of the
+ // actual app.
removeFile(data.backupDir);
}
@@ -193,9 +199,12 @@
for (int i = 0; i < recentlyExecutedRollbacks.size(); ++i) {
RollbackInfo rollback = recentlyExecutedRollbacks.get(i);
JSONObject element = new JSONObject();
- element.put("packageName", rollback.targetPackage.packageName);
- element.put("higherVersionCode", rollback.targetPackage.higherVersion.versionCode);
- element.put("lowerVersionCode", rollback.targetPackage.lowerVersion.versionCode);
+ element.put("rollbackId", rollback.getRollbackId());
+ element.put("packageName", rollback.targetPackage.getPackageName());
+ element.put("higherVersionCode",
+ rollback.targetPackage.getVersionRolledBackFrom().getLongVersionCode());
+ element.put("lowerVersionCode",
+ rollback.targetPackage.getVersionRolledBackTo().getLongVersionCode());
array.put(element);
}
@@ -214,19 +223,22 @@
*/
private RollbackData loadRollbackData(File backupDir) throws IOException {
try {
- RollbackData data = new RollbackData(backupDir);
File rollbackJsonFile = new File(backupDir, "rollback.json");
JSONObject dataJson = new JSONObject(
IoUtils.readFileAsString(rollbackJsonFile.getAbsolutePath()));
+
+ int rollbackId = dataJson.getInt("rollbackId");
+ RollbackData data = new RollbackData(rollbackId, backupDir);
+
JSONArray packagesJson = dataJson.getJSONArray("packages");
for (int i = 0; i < packagesJson.length(); ++i) {
JSONObject infoJson = packagesJson.getJSONObject(i);
String packageName = infoJson.getString("packageName");
long higherVersionCode = infoJson.getLong("higherVersionCode");
long lowerVersionCode = infoJson.getLong("lowerVersionCode");
- data.packages.add(new PackageRollbackInfo(packageName,
- new PackageRollbackInfo.PackageVersion(higherVersionCode),
- new PackageRollbackInfo.PackageVersion(lowerVersionCode)));
+ data.packages.add(new PackageRollbackInfo(
+ new VersionedPackage(packageName, higherVersionCode),
+ new VersionedPackage(packageName, lowerVersionCode)));
}
data.timestamp = Instant.parse(dataJson.getString("timestamp"));
diff --git a/services/core/java/com/android/server/testharness/TestHarnessModeService.java b/services/core/java/com/android/server/testharness/TestHarnessModeService.java
new file mode 100644
index 0000000..23c042a
--- /dev/null
+++ b/services/core/java/com/android/server/testharness/TestHarnessModeService.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.testharness;
+
+import android.annotation.Nullable;
+import android.app.KeyguardManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.UserInfo;
+import android.os.BatteryManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.os.ShellCommand;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+import com.android.server.PersistentDataBlockManagerInternal;
+import com.android.server.SystemService;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+
+/**
+ * Manages the Test Harness Mode service for setting up test harness mode on the device.
+ *
+ * <p>Test Harness Mode is a feature that allows the user to clean their device, retain ADB keys,
+ * and provision the device for Instrumentation testing. This means that all parts of the device
+ * that would otherwise interfere with testing (auto-syncing accounts, package verification,
+ * automatic updates, etc.) are all disabled by default but may be re-enabled by the user.
+ */
+public class TestHarnessModeService extends SystemService {
+ private static final String TAG = TestHarnessModeService.class.getSimpleName();
+ private static final String TEST_HARNESS_MODE_PROPERTY = "persist.sys.test_harness";
+
+ private PersistentDataBlockManagerInternal mPersistentDataBlockManagerInternal;
+
+ public TestHarnessModeService(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService("testharness", mService);
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ switch (phase) {
+ case PHASE_SYSTEM_SERVICES_READY:
+ setUpTestHarnessMode();
+ break;
+ case PHASE_BOOT_COMPLETED:
+ disableAutoSync();
+ break;
+ }
+ super.onBootPhase(phase);
+ }
+
+ private void setUpTestHarnessMode() {
+ Slog.d(TAG, "Setting up test harness mode");
+ byte[] testHarnessModeData = getPersistentDataBlock().getTestHarnessModeData();
+ if (testHarnessModeData == null || testHarnessModeData.length == 0) {
+ // There's no data to apply, so leave it as-is.
+ return;
+ }
+ PersistentData persistentData = PersistentData.fromBytes(testHarnessModeData);
+
+ SystemProperties.set(TEST_HARNESS_MODE_PROPERTY, persistentData.mEnabled ? "1" : "0");
+ writeAdbKeysFile(persistentData);
+ // Clear out the data block so that we don't revert the ADB keys on every boot.
+ getPersistentDataBlock().clearTestHarnessModeData();
+
+ ContentResolver cr = getContext().getContentResolver();
+ if (Settings.Global.getInt(cr, Settings.Global.ADB_ENABLED, 0) == 0) {
+ // Enable ADB
+ Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 1);
+ } else {
+ // ADB is already enabled, we should restart the service so it picks up the new keys
+ android.os.SystemService.restart("adbd");
+ }
+
+ Settings.Global.putInt(cr, Settings.Global.PACKAGE_VERIFIER_ENABLE, 0);
+ Settings.Global.putInt(
+ cr,
+ Settings.Global.STAY_ON_WHILE_PLUGGED_IN,
+ BatteryManager.BATTERY_PLUGGED_ANY);
+ Settings.Global.putInt(cr, Settings.Global.OTA_DISABLE_AUTOMATIC_UPDATE, 1);
+ Settings.Global.putInt(cr, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 1);
+
+ setDeviceProvisioned();
+ }
+
+ private void disableAutoSync() {
+ UserInfo primaryUser = UserManager.get(getContext()).getPrimaryUser();
+ ContentResolver
+ .setMasterSyncAutomaticallyAsUser(false, primaryUser.getUserHandle().getIdentifier());
+ }
+
+ private void writeAdbKeysFile(PersistentData persistentData) {
+ Path adbKeys = Paths.get("/data/misc/adb/adb_keys");
+ try {
+ OutputStream fileOutputStream = Files.newOutputStream(adbKeys);
+ fileOutputStream.write(persistentData.mAdbKeys);
+ fileOutputStream.close();
+
+ Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(adbKeys);
+ permissions.add(PosixFilePermission.GROUP_READ);
+ Files.setPosixFilePermissions(adbKeys, permissions);
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to set up adb keys", e);
+ // Note: if a device enters this block, it will remain UNAUTHORIZED in ADB, but all
+ // other settings will be set up.
+ }
+ }
+
+ // Setting the device as provisioned skips the setup wizard.
+ private void setDeviceProvisioned() {
+ ContentResolver cr = getContext().getContentResolver();
+ Settings.Global.putInt(cr, Settings.Global.DEVICE_PROVISIONED, 1);
+ Settings.Secure.putIntForUser(
+ cr,
+ Settings.Secure.USER_SETUP_COMPLETE,
+ 1,
+ UserHandle.USER_CURRENT);
+ }
+
+ @Nullable
+ private PersistentDataBlockManagerInternal getPersistentDataBlock() {
+ if (mPersistentDataBlockManagerInternal == null) {
+ Slog.d(TAG, "Getting PersistentDataBlockManagerInternal from LocalServices");
+ mPersistentDataBlockManagerInternal =
+ LocalServices.getService(PersistentDataBlockManagerInternal.class);
+ }
+ return mPersistentDataBlockManagerInternal;
+ }
+
+ private final IBinder mService = new Binder() {
+ @Override
+ public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
+ String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
+ (new TestHarnessModeShellCommand())
+ .exec(this, in, out, err, args, callback, resultReceiver);
+ }
+ };
+
+ private class TestHarnessModeShellCommand extends ShellCommand {
+ @Override
+ public int onCommand(String cmd) {
+ switch (cmd) {
+ case "enable":
+ case "restore":
+ checkPermissions();
+ final long originalId = Binder.clearCallingIdentity();
+ try {
+ if (isDeviceSecure()) {
+ getErrPrintWriter().println(
+ "Test Harness Mode cannot be enabled if there is a lock "
+ + "screen");
+ return 2;
+ }
+ return handleEnable();
+ } finally {
+ Binder.restoreCallingIdentity(originalId);
+ }
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ }
+
+ private void checkPermissions() {
+ getContext().enforceCallingPermission(
+ android.Manifest.permission.ENABLE_TEST_HARNESS_MODE,
+ "You must hold android.permission.ENABLE_TEST_HARNESS_MODE "
+ + "to enable Test Harness Mode");
+ }
+
+ private boolean isDeviceSecure() {
+ UserInfo primaryUser = UserManager.get(getContext()).getPrimaryUser();
+ KeyguardManager keyguardManager = getContext().getSystemService(KeyguardManager.class);
+ return keyguardManager.isDeviceSecure(primaryUser.id);
+ }
+
+ private int handleEnable() {
+ Path adbKeys = Paths.get("/data/misc/adb/adb_keys");
+ if (!Files.exists(adbKeys)) {
+ // This should only be accessible on eng builds that haven't yet set up ADB keys
+ getErrPrintWriter()
+ .println("No ADB keys stored; not enabling test harness mode");
+ return 1;
+ }
+
+ try (InputStream inputStream = Files.newInputStream(adbKeys)) {
+ long size = Files.size(adbKeys);
+ byte[] adbKeysBytes = new byte[(int) size];
+ int numBytes = inputStream.read(adbKeysBytes);
+ if (numBytes != size) {
+ getErrPrintWriter().println("Failed to read all bytes of adb_keys");
+ return 1;
+ }
+ PersistentData persistentData = new PersistentData(true, adbKeysBytes);
+ getPersistentDataBlock().setTestHarnessModeData(persistentData.toBytes());
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to store ADB keys.", e);
+ getErrPrintWriter().println("Failed to enable Test Harness Mode");
+ return 1;
+ }
+
+ Intent i = new Intent(Intent.ACTION_FACTORY_RESET);
+ i.setPackage("android");
+ i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ i.putExtra(Intent.EXTRA_REASON, TAG);
+ i.putExtra(Intent.EXTRA_WIPE_EXTERNAL_STORAGE, true);
+ getContext().sendBroadcastAsUser(i, UserHandle.SYSTEM);
+ return 0;
+ }
+
+ @Override
+ public void onHelp() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println("About:");
+ pw.println(" Test Harness Mode is a mode that the device can be placed in to prepare");
+ pw.println(" the device for running UI tests. The device is placed into this mode by");
+ pw.println(" first wiping all data from the device, preserving ADB keys.");
+ pw.println();
+ pw.println(" By default, the following settings are configured:");
+ pw.println(" * Package Verifier is disabled");
+ pw.println(" * Stay Awake While Charging is enabled");
+ pw.println(" * OTA Updates are disabled");
+ pw.println(" * Auto-Sync for accounts is disabled");
+ pw.println();
+ pw.println(" Other apps may configure themselves differently in Test Harness Mode by");
+ pw.println(" checking ActivityManager.isRunningInUserTestHarness()");
+ pw.println();
+ pw.println("Test Harness Mode commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println();
+ pw.println(" enable|restore");
+ pw.println(" Erase all data from this device and enable Test Harness Mode,");
+ pw.println(" preserving the stored ADB keys currently on the device and toggling");
+ pw.println(" settings in a way that are conducive to Instrumentation testing.");
+ }
+ }
+
+ /**
+ * The object that will serialize/deserialize the Test Harness Mode data to and from the
+ * persistent data block.
+ */
+ public static class PersistentData {
+ static final byte VERSION_1 = 1;
+
+ final int mVersion;
+ final boolean mEnabled;
+ final byte[] mAdbKeys;
+
+ PersistentData(boolean enabled, byte[] adbKeys) {
+ this(VERSION_1, enabled, adbKeys);
+ }
+
+ PersistentData(int version, boolean enabled, byte[] adbKeys) {
+ this.mVersion = version;
+ this.mEnabled = enabled;
+ this.mAdbKeys = adbKeys;
+ }
+
+ static PersistentData fromBytes(byte[] bytes) {
+ try {
+ DataInputStream is = new DataInputStream(new ByteArrayInputStream(bytes));
+ int version = is.readInt();
+ boolean enabled = is.readBoolean();
+ int adbKeysLength = is.readInt();
+ byte[] adbKeys = new byte[adbKeysLength];
+ is.readFully(adbKeys);
+ return new PersistentData(version, enabled, adbKeys);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ byte[] toBytes() {
+ try {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(os);
+ dos.writeInt(VERSION_1);
+ dos.writeBoolean(mEnabled);
+ dos.writeInt(mAdbKeys.length);
+ dos.write(mAdbKeys);
+ dos.close();
+ return os.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/vr/VrManagerService.java b/services/core/java/com/android/server/vr/VrManagerService.java
index b3eafa4..45689ce 100644
--- a/services/core/java/com/android/server/vr/VrManagerService.java
+++ b/services/core/java/com/android/server/vr/VrManagerService.java
@@ -65,7 +65,6 @@
import com.android.server.LocalServices;
import com.android.server.SystemConfig;
import com.android.server.SystemService;
-import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.utils.ManagedApplicationService;
import com.android.server.utils.ManagedApplicationService.BinderChecker;
import com.android.server.utils.ManagedApplicationService.LogEvent;
@@ -623,14 +622,6 @@
}
@Override
- public void setVrInputMethod(ComponentName componentName) {
- enforceCallerPermissionAnyOf(Manifest.permission.RESTRICTED_VR_ACCESS);
- InputMethodManagerInternal imm =
- LocalServices.getService(InputMethodManagerInternal.class);
- imm.startVrInputMethodNoCheck(componentName);
- }
-
- @Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
diff --git a/services/core/java/com/android/server/wm/ActivityDisplay.java b/services/core/java/com/android/server/wm/ActivityDisplay.java
index e817dd4..65d66f4 100644
--- a/services/core/java/com/android/server/wm/ActivityDisplay.java
+++ b/services/core/java/com/android/server/wm/ActivityDisplay.java
@@ -558,26 +558,22 @@
}
/**
- * Pause all activities in either all of the stacks or just the back stacks. This is done before
- * resuming a new activity and to make sure that previously active activities are
- * paused in stacks that are no longer visible or in pinned windowing mode. This does not
- * pause activities in visible stacks, so if an activity is launched within the same stack/task,
- * then we should explicitly pause that stack's top activity.
+ * Pause all activities in either all of the stacks or just the back stacks.
* @param userLeaving Passed to pauseActivity() to indicate whether to call onUserLeaving().
* @param resuming The resuming activity.
* @param dontWait The resuming activity isn't going to wait for all activities to be paused
* before resuming.
- * @return {@code true} if any activity was paused as a result of this call.
+ * @return true if any activity was paused as a result of this call.
*/
boolean pauseBackStacks(boolean userLeaving, ActivityRecord resuming, boolean dontWait) {
boolean someActivityPaused = false;
for (int stackNdx = mStacks.size() - 1; stackNdx >= 0; --stackNdx) {
final ActivityStack stack = mStacks.get(stackNdx);
- final ActivityRecord resumedActivity = stack.getResumedActivity();
- if (resumedActivity != null
- && (!stack.shouldBeVisible(resuming) || !stack.isFocusable())) {
+ // TODO(b/111541062): Check if resumed activity on this display instead
+ if (!mRootActivityContainer.isTopDisplayFocusedStack(stack)
+ && stack.getResumedActivity() != null) {
if (DEBUG_STATES) Slog.d(TAG_STATES, "pauseBackStacks: stack=" + stack +
- " mResumedActivity=" + resumedActivity);
+ " mResumedActivity=" + stack.getResumedActivity());
someActivityPaused |= stack.startPausingLocked(userLeaving, false, resuming,
dontWait);
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 6213fa0..b8634d8 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -1946,84 +1946,30 @@
try {
mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
WindowVisibilityItem.obtain(true /* showWindow */));
- makeActiveIfNeeded(null /* activeActivity*/);
+ if (shouldPauseWhenBecomingVisible()) {
+ // An activity must be in the {@link PAUSING} state for the system to validate
+ // the move to {@link PAUSED}.
+ setState(PAUSING, "makeVisibleIfNeeded");
+ mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
+ PauseActivityItem.obtain(finishing, false /* userLeaving */,
+ configChangeFlags, false /* dontReport */));
+ }
} catch (Exception e) {
Slog.w(TAG, "Exception thrown sending visibility update: " + intent.getComponent(), e);
}
}
- /**
- * Make activity resumed or paused if needed.
- * @param activeActivity an activity that is resumed or just completed pause action.
- * We won't change the state of this activity.
- */
- boolean makeActiveIfNeeded(ActivityRecord activeActivity) {
- if (shouldResumeActivity(activeActivity)) {
- if (DEBUG_VISIBILITY) {
- Slog.v("TAG_VISIBILITY", "Resume visible activity, " + this);
- }
- return getActivityStack().resumeTopActivityUncheckedLocked(activeActivity /* prev */,
- null /* options */);
- } else if (shouldPauseActivity(activeActivity)) {
- if (DEBUG_VISIBILITY) {
- Slog.v("TAG_VISIBILITY", "Pause visible activity, " + this);
- }
- // An activity must be in the {@link PAUSING} state for the system to validate
- // the move to {@link PAUSED}.
- setState(PAUSING, "makeVisibleIfNeeded");
- try {
- mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
- PauseActivityItem.obtain(finishing, false /* userLeaving */,
- configChangeFlags, false /* dontReport */));
- } catch (Exception e) {
- Slog.w(TAG, "Exception thrown sending pause: " + intent.getComponent(), e);
- }
- }
- return false;
- }
-
- /**
- * Check if activity should be moved to PAUSED state. The activity:
- * - should be eligible to be made active (see {@link #shouldMakeActive(ActivityRecord)})
- * - should be non-focusable
- * - should not be currently pausing or paused
- * @param activeActivity the activity that is active or just completed pause action. We won't
- * resume if this activity is active.
- */
- private boolean shouldPauseActivity(ActivityRecord activeActivity) {
- return shouldMakeActive(activeActivity) && !isFocusable() && !isState(PAUSING, PAUSED);
- }
-
- /**
- * Check if activity should be moved to RESUMED state. The activity:
- * - should be eligible to be made active (see {@link #shouldMakeActive(ActivityRecord)})
- * - should be focusable
- * @param activeActivity the activity that is active or just completed pause action. We won't
- * resume if this activity is active.
- */
- private boolean shouldResumeActivity(ActivityRecord activeActivity) {
- return shouldMakeActive(activeActivity) && isFocusable() && !isState(RESUMED);
- }
-
- /**
- * Check if activity is eligible to be made active (resumed of paused). The activity:
- * - should be paused, stopped or stopping
- * - should not be the currently active one
- * - should be either the topmost in task, or right below the top activity that is finishing
- * If all of these conditions are not met at the same time, the activity cannot be made active.
- */
- private boolean shouldMakeActive(ActivityRecord activeActivity) {
- // If the activity is stopped, stopping, cycle to an active state. We avoid doing
+ /** Check if activity should be moved to PAUSED state when it becomes visible. */
+ private boolean shouldPauseWhenBecomingVisible() {
+ // If the activity is stopped or stopping, cycle to the paused state. We avoid doing
// this when there is an activity waiting to become translucent as the extra binder
// calls will lead to noticeable jank. A later call to
- // ActivityStack#ensureActivitiesVisibleLocked will bring the activity to a proper
- // active state.
- if (!isState(RESUMED, PAUSED, STOPPED, STOPPING)
- || getActivityStack().mTranslucentActivityWaiting != null) {
- return false;
- }
-
- if (this == activeActivity) {
+ // ActivityStack#ensureActivitiesVisibleLocked will bring the activity to the proper
+ // paused state. We also avoid doing this for the activity the stack supervisor
+ // considers the resumed activity, as normal means will bring the activity from STOPPED
+ // to RESUMED. Adding PAUSING in this scenario will lead to double lifecycles.
+ if (!isState(STOPPED, STOPPING) || getActivityStack().mTranslucentActivityWaiting != null
+ || isResumedActivityOnDisplay()) {
return false;
}
@@ -2033,14 +1979,14 @@
throw new IllegalStateException("Activity not found in its task");
}
if (positionInTask == task.mActivities.size() - 1) {
- // It's the topmost activity in the task - should become resumed now
+ // It's the topmost activity in the task - should become paused now
return true;
}
// Check if activity above is finishing now and this one becomes the topmost in task.
final ActivityRecord activityAbove = task.mActivities.get(positionInTask + 1);
if (activityAbove.finishing && results == null) {
- // We will only allow making active if activity above wasn't launched for result.
- // Otherwise it will cause this activity to resume before getting result.
+ // We will only allow pausing if activity above wasn't launched for result. Otherwise it
+ // will cause this activity to resume before getting result.
return true;
}
return false;
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index 3aef8e1f..891c3da 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -357,11 +357,6 @@
*/
boolean mForceHidden = false;
- /**
- * Used to keep resumeTopActivityUncheckedLocked() from being entered recursively
- */
- boolean mInResumeTopActivity = false;
-
private boolean mUpdateBoundsDeferred;
private boolean mUpdateBoundsDeferredCalled;
private boolean mUpdateDisplayedBoundsDeferredCalled;
@@ -1737,7 +1732,6 @@
"Activity paused: token=" + token + ", timeout=" + timeout);
final ActivityRecord r = isInStackLocked(token);
-
if (r != null) {
mHandler.removeMessages(PAUSE_TIMEOUT_MSG, r);
if (mPausingActivity == r) {
@@ -2094,7 +2088,8 @@
boolean aboveTop = top != null;
final boolean stackShouldBeVisible = shouldBeVisible(starting);
boolean behindFullscreenActivity = !stackShouldBeVisible;
- boolean resumeNextActivity = isFocusable() && isInStackLocked(starting) == null;
+ boolean resumeNextActivity = mRootActivityContainer.isTopDisplayFocusedStack(this)
+ && (isInStackLocked(starting) == null);
final boolean isTopNotPinnedStack =
isAttached() && getDisplay().isTopNotPinnedStack(this);
for (int taskNdx = mTaskHistory.size() - 1; taskNdx >= 0; --taskNdx) {
@@ -2155,10 +2150,6 @@
if (r.handleAlreadyVisible()) {
resumeNextActivity = false;
}
-
- if (notifyClients) {
- r.makeActiveIfNeeded(starting);
- }
} else {
r.makeVisibleIfNeeded(starting, notifyClients);
}
@@ -2336,7 +2327,7 @@
r.setVisible(true);
}
if (r != starting) {
- mStackSupervisor.startSpecificActivityLocked(r, andResume, true /* checkConfig */);
+ mStackSupervisor.startSpecificActivityLocked(r, andResume, false);
return true;
}
}
@@ -2514,7 +2505,7 @@
*/
@GuardedBy("mService")
boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options) {
- if (mInResumeTopActivity) {
+ if (mStackSupervisor.inResumeTopActivity) {
// Don't even start recursing.
return false;
}
@@ -2522,7 +2513,7 @@
boolean result = false;
try {
// Protect against recursion.
- mInResumeTopActivity = true;
+ mStackSupervisor.inResumeTopActivity = true;
result = resumeTopActivityInnerLocked(prev, options);
// When resuming the top activity, it may be necessary to pause the top activity (for
@@ -2537,7 +2528,7 @@
checkReadyForSleep();
}
} finally {
- mInResumeTopActivity = false;
+ mStackSupervisor.inResumeTopActivity = false;
}
return result;
@@ -2570,7 +2561,7 @@
// Find the next top-most activity to resume in this stack that is not finishing and is
// focusable. If it is not focusable, we will fall into the case below to resume the
// top activity in the next focusable task.
- ActivityRecord next = topRunningActivityLocked(true /* focusableOnly */);
+ final ActivityRecord next = topRunningActivityLocked(true /* focusableOnly */);
final boolean hasRunningActivity = next != null;
@@ -2658,12 +2649,6 @@
if (!mRootActivityContainer.allPausedActivitiesComplete()) {
if (DEBUG_SWITCH || DEBUG_PAUSE || DEBUG_STATES) Slog.v(TAG_PAUSE,
"resumeTopActivityLocked: Skip resume: some activity pausing.");
-
- // Adding previous activity to the waiting visible list, or it would be stopped
- // before top activity being visible.
- if (prev != null && !next.nowVisible) {
- mStackSupervisor.mActivitiesWaitingForVisibleActivity.add(prev);
- }
return false;
}
@@ -2873,9 +2858,7 @@
// the screen based on the new activity order.
boolean notUpdated = true;
- // Activity should also be visible if set mLaunchTaskBehind to true (see
- // ActivityRecord#shouldBeVisibleIgnoringKeyguard()).
- if (shouldBeVisible(next)) {
+ if (isFocusedStackOnDisplay()) {
// We have special rotation behavior when here is some active activity that
// requests specific orientation or Keyguard is locked. Make sure all activity
// visibilities are set correctly as well as the transition is updated if needed
@@ -4104,12 +4087,6 @@
mStackSupervisor.mFinishingActivities.add(r);
r.resumeKeyDispatchingLocked();
mRootActivityContainer.resumeFocusedStacksTopActivities();
- // If activity was not paused at this point - explicitly pause it to start finishing
- // process. Finishing will be completed once it reports pause back.
- if (r.isState(RESUMED) && mPausingActivity != null) {
- startPausingLocked(false /* userLeaving */, false /* uiSleeping */, next /* resuming */,
- false /* dontWait */);
- }
return r;
}
diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
index a83ef34..3a288ca 100644
--- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
@@ -327,6 +327,9 @@
*/
PowerManager.WakeLock mGoingToSleep;
+ /** Used to keep resumeTopActivityUncheckedLocked() from being entered recursively */
+ boolean inResumeTopActivity;
+
/**
* Temporary rect used during docked stack resize calculation so we don't need to create a new
* object each time.
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 43c1206..2807094 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -179,10 +179,7 @@
.setActivityOptions(options.toBundle())
.execute();
mLastHomeActivityStartRecord = tmpOutRecord[0];
- final ActivityDisplay display =
- mService.mRootActivityContainer.getActivityDisplay(displayId);
- final ActivityStack homeStack = display != null ? display.getHomeStack() : null;
- if (homeStack != null && homeStack.mInResumeTopActivity) {
+ if (mSupervisor.inResumeTopActivity) {
// If we are in resume section already, home activity will be initialized, but not
// resumed (to avoid recursive resume) and will stay that way until something pokes it
// again. We need to schedule another resume.
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 4e2dffc..d36e545 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1629,7 +1629,7 @@
// Also, we don't want to resume activities in a task that currently has an overlay
// as the starting activity just needs to be in the visible paused state until the
// over is removed.
- mTargetStack.ensureActivitiesVisibleLocked(mStartActivity, 0, !PRESERVE_WINDOWS);
+ mTargetStack.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
// Go ahead and tell window manager to execute app transition for this activity
// since the app transition will not be triggered through the resume channel.
mTargetStack.getDisplay().mDisplayContent.executeAppTransition();
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index 9361e7f..750c5ca 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -2404,7 +2404,7 @@
@Override
protected void reparentSurfaceControl(Transaction t, SurfaceControl newParent) {
if (!mSurfaceAnimator.hasLeash()) {
- t.reparent(mSurfaceControl, newParent.getHandle());
+ t.reparent(mSurfaceControl, newParent);
}
}
@@ -2450,7 +2450,7 @@
t.setWindowCrop(mAnimationBoundsLayer, mTmpRect);
// Reparent leash to animation bounds layer.
- t.reparent(leash, mAnimationBoundsLayer.getHandle());
+ t.reparent(leash, mAnimationBoundsLayer);
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index ac1159a..8fefd35 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -4588,7 +4588,7 @@
* Reparents the given surface to mOverlayLayer.
*/
void reparentToOverlay(Transaction transaction, SurfaceControl surface) {
- transaction.reparent(surface, mOverlayLayer.getHandle());
+ transaction.reparent(surface, mOverlayLayer);
}
void applyMagnificationSpec(MagnificationSpec spec) {
@@ -4831,11 +4831,11 @@
* Re-parent the DisplayContent's top surfaces, {@link #mWindowingLayer} and
* {@link #mOverlayLayer} to the specified surfaceControl.
*
- * @param surfaceControlHandle The handle for the new SurfaceControl, where the DisplayContent's
+ * @param surfaceControlHandle The new SurfaceControl, where the DisplayContent's
* surfaces will be re-parented to.
*/
- void reparentDisplayContent(IBinder surfaceControlHandle) {
- mPendingTransaction.reparent(mWindowingLayer, surfaceControlHandle)
- .reparent(mOverlayLayer, surfaceControlHandle);
+ void reparentDisplayContent(SurfaceControl sc) {
+ mPendingTransaction.reparent(mWindowingLayer, sc)
+ .reparent(mOverlayLayer, sc);
}
}
diff --git a/services/core/java/com/android/server/wm/RootActivityContainer.java b/services/core/java/com/android/server/wm/RootActivityContainer.java
index c4a853d..9b72141 100644
--- a/services/core/java/com/android/server/wm/RootActivityContainer.java
+++ b/services/core/java/com/android/server/wm/RootActivityContainer.java
@@ -1108,41 +1108,28 @@
return false;
}
- boolean result = false;
if (targetStack != null && (targetStack.isTopStackOnDisplay()
|| getTopDisplayFocusedStack() == targetStack)) {
- result = targetStack.resumeTopActivityUncheckedLocked(target, targetOptions);
+ return targetStack.resumeTopActivityUncheckedLocked(target, targetOptions);
}
+ // Resume all top activities in focused stacks on all displays.
for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {
- boolean resumedOnDisplay = false;
final ActivityDisplay display = mActivityDisplays.get(displayNdx);
- for (int stackNdx = display.getChildCount() - 1; stackNdx >= 0; --stackNdx) {
- final ActivityStack stack = display.getChildAt(stackNdx);
- final ActivityRecord topRunningActivity = stack.topRunningActivityLocked();
- if (!stack.isFocusableAndVisible() || topRunningActivity == null) {
- continue;
- }
- if (topRunningActivity.isState(RESUMED)) {
- // Kick off any lingering app transitions form the MoveTaskToFront operation.
- stack.executeAppTransition(targetOptions);
- } else {
- resumedOnDisplay |= topRunningActivity.makeActiveIfNeeded(target);
- }
+ final ActivityStack focusedStack = display.getFocusedStack();
+ if (focusedStack == null) {
+ continue;
}
- if (!resumedOnDisplay) {
- // In cases when there are no valid activities (e.g. device just booted or launcher
- // crashed) it's possible that nothing was resumed on a display. Requesting resume
- // of top activity in focused stack explicitly will make sure that at least home
- // activity is started and resumed, and no recursion occurs.
- final ActivityStack focusedStack = display.getFocusedStack();
- if (focusedStack != null) {
- focusedStack.resumeTopActivityUncheckedLocked(target, targetOptions);
- }
+ final ActivityRecord r = focusedStack.topRunningActivityLocked();
+ if (r == null || !r.isState(RESUMED)) {
+ focusedStack.resumeTopActivityUncheckedLocked(null, null);
+ } else if (r.isState(RESUMED)) {
+ // Kick off any lingering app transitions form the MoveTaskToFront operation.
+ focusedStack.executeAppTransition(targetOptions);
}
}
- return result;
+ return false;
}
void applySleepTokens(boolean applyToStacks) {
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index 9d9b48a..1a8a911 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -199,7 +199,7 @@
* @see #setLayer
*/
void reparent(Transaction t, SurfaceControl newParent) {
- t.reparent(mLeash != null ? mLeash : mAnimatable.getSurfaceControl(), newParent.getHandle());
+ t.reparent(mLeash != null ? mLeash : mAnimatable.getSurfaceControl(), newParent);
}
/**
@@ -228,8 +228,8 @@
// Cancel source animation, but don't let animation runner cancel the animation.
from.cancelAnimation(t, false /* restarting */, false /* forwardCancel */);
- t.reparent(surface, mLeash.getHandle());
- t.reparent(mLeash, parent.getHandle());
+ t.reparent(surface, mLeash);
+ t.reparent(mLeash, parent);
mAnimatable.onAnimationLeashCreated(t, mLeash);
mService.mAnimationTransferMap.put(mAnimation, this);
}
@@ -275,7 +275,7 @@
final boolean destroy = mLeash != null && surface != null && parent != null;
if (destroy) {
if (DEBUG_ANIM) Slog.i(TAG, "Reparenting to original parent");
- t.reparent(surface, parent.getHandle());
+ t.reparent(surface, parent);
scheduleAnim = true;
}
mService.mAnimationTransferMap.remove(mAnimation);
@@ -308,7 +308,7 @@
if (!hidden) {
t.show(leash);
}
- t.reparent(surface, leash.getHandle());
+ t.reparent(surface, leash);
return leash;
}
diff --git a/services/core/java/com/android/server/wm/TaskRecord.java b/services/core/java/com/android/server/wm/TaskRecord.java
index 0529ed1..69dcaf4 100644
--- a/services/core/java/com/android/server/wm/TaskRecord.java
+++ b/services/core/java/com/android/server/wm/TaskRecord.java
@@ -698,14 +698,6 @@
return false;
}
- final boolean toTopOfStack = position == MAX_VALUE;
- if (toTopOfStack && toStack.getResumedActivity() != null
- && toStack.topRunningActivityLocked() != null) {
- // Pause the resumed activity on the target stack while re-parenting task on top of it.
- toStack.startPausingLocked(false /* userLeaving */, false /* uiSleeping */,
- null /* resuming */, false /* pauseImmediately */);
- }
-
final int toStackWindowingMode = toStack.getWindowingMode();
final ActivityRecord topActivity = getTopActivity();
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index c8c834f..c6679a9 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -7334,7 +7334,7 @@
}
@Override
- public void reparentDisplayContent(int displayId, IBinder surfaceControlHandle) {
+ public void reparentDisplayContent(int displayId, SurfaceControl sc) {
final Display display = mDisplayManager.getDisplay(displayId);
if (display == null) {
throw new IllegalArgumentException(
@@ -7351,7 +7351,7 @@
long token = Binder.clearCallingIdentity();
try {
DisplayContent displayContent = getDisplayContentOrCreate(displayId, null);
- displayContent.reparentDisplayContent(surfaceControlHandle);
+ displayContent.reparentDisplayContent(sc);
} finally {
Binder.restoreCallingIdentity(token);
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 0977323..fb7e47d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -4223,7 +4223,7 @@
@Override
public void setPasswordHistoryLength(ComponentName who, int length, boolean parent) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return;
}
Preconditions.checkNotNull(who, "ComponentName is null");
@@ -4246,13 +4246,16 @@
@Override
public int getPasswordHistoryLength(ComponentName who, int userHandle, boolean parent) {
+ if (!mLockPatternUtils.hasSecureLockScreen()) {
+ return 0;
+ }
return getStrictestPasswordRequirement(who, userHandle, parent,
admin -> admin.passwordHistoryLength, PASSWORD_QUALITY_UNSPECIFIED);
}
@Override
public void setPasswordExpirationTimeout(ComponentName who, long timeout, boolean parent) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return;
}
Preconditions.checkNotNull(who, "ComponentName is null");
@@ -4288,7 +4291,7 @@
*/
@Override
public long getPasswordExpirationTimeout(ComponentName who, int userHandle, boolean parent) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return 0L;
}
enforceFullCrossUsersPermission(userHandle);
@@ -4423,7 +4426,7 @@
@Override
public long getPasswordExpiration(ComponentName who, int userHandle, boolean parent) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return 0L;
}
enforceFullCrossUsersPermission(userHandle);
@@ -4770,6 +4773,9 @@
@Override
public int getCurrentFailedPasswordAttempts(int userHandle, boolean parent) {
+ if (!mLockPatternUtils.hasSecureLockScreen()) {
+ return 0;
+ }
enforceFullCrossUsersPermission(userHandle);
synchronized (getLockObject()) {
if (!isCallerWithSystemUid()) {
@@ -4789,7 +4795,7 @@
@Override
public void setMaximumFailedPasswordsForWipe(ComponentName who, int num, boolean parent) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return;
}
Preconditions.checkNotNull(who, "ComponentName is null");
@@ -4815,7 +4821,7 @@
@Override
public int getMaximumFailedPasswordsForWipe(ComponentName who, int userHandle, boolean parent) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return 0;
}
enforceFullCrossUsersPermission(userHandle);
@@ -4829,7 +4835,7 @@
@Override
public int getProfileWithMinimumFailedPasswordsForWipe(int userHandle, boolean parent) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return UserHandle.USER_NULL;
}
enforceFullCrossUsersPermission(userHandle);
@@ -4910,6 +4916,11 @@
}
@Override
public boolean resetPassword(String passwordOrNull, int flags) throws RemoteException {
+ if (!mLockPatternUtils.hasSecureLockScreen()) {
+ Slog.w(LOG_TAG, "Cannot reset password when the device has no lock screen");
+ return false;
+ }
+
final int callingUid = mInjector.binderGetCallingUid();
final int userHandle = mInjector.userHandleGetCallingUserId();
@@ -5252,7 +5263,7 @@
@Override
public void setRequiredStrongAuthTimeout(ComponentName who, long timeoutMs,
boolean parent) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return;
}
Preconditions.checkNotNull(who, "ComponentName is null");
@@ -5285,7 +5296,7 @@
*/
@Override
public long getRequiredStrongAuthTimeout(ComponentName who, int userId, boolean parent) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return DevicePolicyManager.DEFAULT_STRONG_AUTH_TIMEOUT_MS;
}
enforceFullCrossUsersPermission(userId);
@@ -6494,7 +6505,7 @@
*/
@Override
public void setActivePasswordState(PasswordMetrics metrics, int userHandle) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return;
}
enforceFullCrossUsersPermission(userHandle);
@@ -6514,7 +6525,7 @@
@Override
public void reportPasswordChanged(@UserIdInt int userId) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return;
}
enforceFullCrossUsersPermission(userId);
@@ -8800,7 +8811,7 @@
@Override
public void setTrustAgentConfiguration(ComponentName admin, ComponentName agent,
PersistableBundle args, boolean parent) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return;
}
Preconditions.checkNotNull(admin, "admin is null");
@@ -8817,7 +8828,7 @@
@Override
public List<PersistableBundle> getTrustAgentConfiguration(ComponentName admin,
ComponentName agent, int userHandle, boolean parent) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return null;
}
Preconditions.checkNotNull(agent, "agent null");
@@ -13215,7 +13226,7 @@
@Override
public boolean setResetPasswordToken(ComponentName admin, byte[] token) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return false;
}
if (token == null || token.length < 32) {
@@ -13243,7 +13254,7 @@
@Override
public boolean clearResetPasswordToken(ComponentName admin) {
- if (!mHasFeature) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
return false;
}
synchronized (getLockObject()) {
@@ -13269,6 +13280,9 @@
@Override
public boolean isResetPasswordTokenActive(ComponentName admin) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
+ return false;
+ }
synchronized (getLockObject()) {
final int userHandle = mInjector.userHandleGetCallingUserId();
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -13290,6 +13304,9 @@
@Override
public boolean resetPasswordWithToken(ComponentName admin, String passwordOrNull, byte[] token,
int flags) {
+ if (!mHasFeature || !mLockPatternUtils.hasSecureLockScreen()) {
+ return false;
+ }
Preconditions.checkNotNull(token);
synchronized (getLockObject()) {
final int userHandle = mInjector.userHandleGetCallingUserId();
diff --git a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java
index 32513c1..e99dd4f 100644
--- a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java
+++ b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java
@@ -21,9 +21,12 @@
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
+import android.database.sqlite.SQLiteCursor;
+import android.database.sqlite.SQLiteCursorDriver;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQuery;
import android.net.NetworkUtils;
import android.net.ipmemorystore.NetworkAttributes;
import android.net.ipmemorystore.Status;
@@ -35,6 +38,7 @@
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
+import java.util.StringJoiner;
/**
* Encapsulating class for using the SQLite database backing the memory store.
@@ -46,6 +50,9 @@
*/
public class IpMemoryStoreDatabase {
private static final String TAG = IpMemoryStoreDatabase.class.getSimpleName();
+ // A pair of NetworkAttributes objects is group-close if the confidence that they are
+ // the same is above this cutoff. See NetworkAttributes and SameL3NetworkResponse.
+ private static final float GROUPCLOSE_CONFIDENCE = 0.5f;
/**
* Contract class for the Network Attributes table.
@@ -187,30 +194,35 @@
return addresses;
}
+ @NonNull
+ private static ContentValues toContentValues(@Nullable final NetworkAttributes attributes) {
+ final ContentValues values = new ContentValues();
+ if (null == attributes) return values;
+ if (null != attributes.assignedV4Address) {
+ values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS,
+ NetworkUtils.inet4AddressToIntHTH(attributes.assignedV4Address));
+ }
+ if (null != attributes.groupHint) {
+ values.put(NetworkAttributesContract.COLNAME_GROUPHINT, attributes.groupHint);
+ }
+ if (null != attributes.dnsAddresses) {
+ values.put(NetworkAttributesContract.COLNAME_DNSADDRESSES,
+ encodeAddressList(attributes.dnsAddresses));
+ }
+ if (null != attributes.mtu) {
+ values.put(NetworkAttributesContract.COLNAME_MTU, attributes.mtu);
+ }
+ return values;
+ }
+
// Convert a NetworkAttributes object to content values to store them in a table compliant
// with the contract defined in NetworkAttributesContract.
@NonNull
private static ContentValues toContentValues(@NonNull final String key,
@Nullable final NetworkAttributes attributes, final long expiry) {
- final ContentValues values = new ContentValues();
+ final ContentValues values = toContentValues(attributes);
values.put(NetworkAttributesContract.COLNAME_L2KEY, key);
values.put(NetworkAttributesContract.COLNAME_EXPIRYDATE, expiry);
- if (null != attributes) {
- if (null != attributes.assignedV4Address) {
- values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS,
- NetworkUtils.inet4AddressToIntHTH(attributes.assignedV4Address));
- }
- if (null != attributes.groupHint) {
- values.put(NetworkAttributesContract.COLNAME_GROUPHINT, attributes.groupHint);
- }
- if (null != attributes.dnsAddresses) {
- values.put(NetworkAttributesContract.COLNAME_DNSADDRESSES,
- encodeAddressList(attributes.dnsAddresses));
- }
- if (null != attributes.mtu) {
- values.put(NetworkAttributesContract.COLNAME_MTU, attributes.mtu);
- }
- }
return values;
}
@@ -228,6 +240,32 @@
return values;
}
+ @Nullable
+ private static NetworkAttributes readNetworkAttributesLine(@NonNull final Cursor cursor) {
+ // Make sure the data hasn't expired
+ final long expiry = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, -1L);
+ if (expiry < System.currentTimeMillis()) return null;
+
+ final NetworkAttributes.Builder builder = new NetworkAttributes.Builder();
+ final int assignedV4AddressInt = getInt(cursor,
+ NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0);
+ final String groupHint = getString(cursor, NetworkAttributesContract.COLNAME_GROUPHINT);
+ final byte[] dnsAddressesBlob =
+ getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES);
+ final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1);
+ if (0 != assignedV4AddressInt) {
+ builder.setAssignedV4Address(NetworkUtils.intToInet4AddressHTH(assignedV4AddressInt));
+ }
+ builder.setGroupHint(groupHint);
+ if (null != dnsAddressesBlob) {
+ builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob));
+ }
+ if (mtu >= 0) {
+ builder.setMtu(mtu);
+ }
+ return builder.build();
+ }
+
private static final String[] EXPIRY_COLUMN = new String[] {
NetworkAttributesContract.COLNAME_EXPIRYDATE
};
@@ -313,32 +351,9 @@
// result here. 0 results means the key was not found.
if (cursor.getCount() != 1) return null;
cursor.moveToFirst();
-
- // Make sure the data hasn't expired
- final long expiry = cursor.getLong(
- cursor.getColumnIndexOrThrow(NetworkAttributesContract.COLNAME_EXPIRYDATE));
- if (expiry < System.currentTimeMillis()) return null;
-
- final NetworkAttributes.Builder builder = new NetworkAttributes.Builder();
- final int assignedV4AddressInt = getInt(cursor,
- NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0);
- final String groupHint = getString(cursor, NetworkAttributesContract.COLNAME_GROUPHINT);
- final byte[] dnsAddressesBlob =
- getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES);
- final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1);
+ final NetworkAttributes attributes = readNetworkAttributesLine(cursor);
cursor.close();
-
- if (0 != assignedV4AddressInt) {
- builder.setAssignedV4Address(NetworkUtils.intToInet4AddressHTH(assignedV4AddressInt));
- }
- builder.setGroupHint(groupHint);
- if (null != dnsAddressesBlob) {
- builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob));
- }
- if (mtu >= 0) {
- builder.setMtu(mtu);
- }
- return builder.build();
+ return attributes;
}
private static final String[] DATA_COLUMN = new String[] {
@@ -365,17 +380,134 @@
return result;
}
+ /**
+ * The following is a horrible hack that is necessary because the Android SQLite API does not
+ * have a way to query a binary blob. This, almost certainly, is an overlook.
+ *
+ * The Android SQLite API has two family of methods : one for query that returns data, and
+ * one for more general SQL statements that can execute any statement but may not return
+ * anything. All the query methods, however, take only String[] for the arguments.
+ *
+ * In principle it is simple to write a function that will encode the binary blob in the
+ * way SQLite expects it. However, because the API forces the argument to be coerced into a
+ * String, the SQLiteQuery object generated by the default query methods will bind all
+ * arguments as Strings and SQL will *sanitize* them. This works okay for numeric types,
+ * but the format for blobs is x'<hex string>'. Note the presence of quotes, which will
+ * be sanitized, changing the contents of the field, and the query will fail to match the
+ * blob.
+ *
+ * As far as I can tell, there are two possible ways around this problem. The first one
+ * is to put the data in the query string and eschew it being an argument. This would
+ * require doing the sanitizing by hand. The other is to call bindBlob directly on the
+ * generated SQLiteQuery object, which not only is a lot less dangerous than rolling out
+ * sanitizing, but also will do the right thing if the underlying format ever changes.
+ *
+ * But none of the methods that take an SQLiteQuery object can return data ; this *must*
+ * be called with SQLiteDatabase#query. This object is not accessible from outside.
+ * However, there is a #query version that accepts a CursorFactory and this is pretty
+ * straightforward to implement as all the arguments are coming in and the SQLiteCursor
+ * class is public API.
+ * With this, it's possible to intercept the SQLiteQuery object, and assuming the args
+ * are available, to bind them directly and work around the API's oblivious coercion into
+ * Strings.
+ *
+ * This is really sad, but I don't see another way of having this work than this or the
+ * hand-rolled sanitizing, and this is the lesser evil.
+ */
+ private static class CustomCursorFactory implements SQLiteDatabase.CursorFactory {
+ @NonNull
+ private final ArrayList<Object> mArgs;
+ CustomCursorFactory(@NonNull final ArrayList<Object> args) {
+ mArgs = args;
+ }
+ @Override
+ public Cursor newCursor(final SQLiteDatabase db, final SQLiteCursorDriver masterQuery,
+ final String editTable,
+ final SQLiteQuery query) {
+ int index = 1; // bind is 1-indexed
+ for (final Object arg : mArgs) {
+ if (arg instanceof String) {
+ query.bindString(index++, (String) arg);
+ } else if (arg instanceof Long) {
+ query.bindLong(index++, (Long) arg);
+ } else if (arg instanceof Integer) {
+ query.bindLong(index++, Long.valueOf((Integer) arg));
+ } else if (arg instanceof byte[]) {
+ query.bindBlob(index++, (byte[]) arg);
+ } else {
+ throw new IllegalStateException("Unsupported type CustomCursorFactory "
+ + arg.getClass().toString());
+ }
+ }
+ return new SQLiteCursor(masterQuery, editTable, query);
+ }
+ }
+
+ // Returns the l2key of the closest match, if and only if it matches
+ // closely enough (as determined by group-closeness).
+ @Nullable
+ static String findClosestAttributes(@NonNull final SQLiteDatabase db,
+ @NonNull final NetworkAttributes attr) {
+ if (attr.isEmpty()) return null;
+ final ContentValues values = toContentValues(attr);
+
+ // Build the selection and args. To cut down on the number of lines to search, limit
+ // the search to those with at least one argument equals to the requested attributes.
+ // This works only because null attributes match only will not result in group-closeness.
+ final StringJoiner sj = new StringJoiner(" OR ");
+ final ArrayList<Object> args = new ArrayList<>();
+ args.add(System.currentTimeMillis());
+ for (final String field : values.keySet()) {
+ sj.add(field + " = ?");
+ args.add(values.get(field));
+ }
+
+ final String selection = NetworkAttributesContract.COLNAME_EXPIRYDATE + " > ? AND ("
+ + sj.toString() + ")";
+ final Cursor cursor = db.queryWithFactory(new CustomCursorFactory(args),
+ false, // distinct
+ NetworkAttributesContract.TABLENAME,
+ null, // columns, null means everything
+ selection, // selection
+ null, // selectionArgs, horrendously passed to the cursor factory instead
+ null, // groupBy
+ null, // having
+ null, // orderBy
+ null); // limit
+ if (cursor.getCount() <= 0) return null;
+ cursor.moveToFirst();
+ String bestKey = null;
+ float bestMatchConfidence = GROUPCLOSE_CONFIDENCE; // Never return a match worse than this.
+ while (!cursor.isAfterLast()) {
+ final NetworkAttributes read = readNetworkAttributesLine(cursor);
+ final float confidence = read.getNetworkGroupSamenessConfidence(attr);
+ if (confidence > bestMatchConfidence) {
+ bestKey = getString(cursor, NetworkAttributesContract.COLNAME_L2KEY);
+ bestMatchConfidence = confidence;
+ }
+ cursor.moveToNext();
+ }
+ cursor.close();
+ return bestKey;
+ }
+
// Helper methods
- static String getString(final Cursor cursor, final String columnName) {
+ private static String getString(final Cursor cursor, final String columnName) {
final int columnIndex = cursor.getColumnIndex(columnName);
return (columnIndex >= 0) ? cursor.getString(columnIndex) : null;
}
- static byte[] getBlob(final Cursor cursor, final String columnName) {
+ private static byte[] getBlob(final Cursor cursor, final String columnName) {
final int columnIndex = cursor.getColumnIndex(columnName);
return (columnIndex >= 0) ? cursor.getBlob(columnIndex) : null;
}
- static int getInt(final Cursor cursor, final String columnName, final int defaultValue) {
+ private static int getInt(final Cursor cursor, final String columnName,
+ final int defaultValue) {
final int columnIndex = cursor.getColumnIndex(columnName);
return (columnIndex >= 0) ? cursor.getInt(columnIndex) : defaultValue;
}
+ private static long getLong(final Cursor cursor, final String columnName,
+ final long defaultValue) {
+ final int columnIndex = cursor.getColumnIndex(columnName);
+ return (columnIndex >= 0) ? cursor.getLong(columnIndex) : defaultValue;
+ }
}
diff --git a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java
index 8b521f4..d43dc6a 100644
--- a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java
+++ b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java
@@ -250,9 +250,26 @@
* Through the listener, returns the L2 key if one matched, or null.
*/
@Override
- public void findL2Key(@NonNull final NetworkAttributesParcelable attributes,
- @NonNull final IOnL2KeyResponseListener listener) {
- // TODO : implement this.
+ public void findL2Key(@Nullable final NetworkAttributesParcelable attributes,
+ @Nullable final IOnL2KeyResponseListener listener) {
+ if (null == listener) return;
+ mExecutor.execute(() -> {
+ try {
+ if (null == attributes) {
+ listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null);
+ return;
+ }
+ if (null == mDb) {
+ listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null);
+ return;
+ }
+ final String key = IpMemoryStoreDatabase.findClosestAttributes(mDb,
+ new NetworkAttributes(attributes));
+ listener.onL2KeyResponse(makeStatus(SUCCESS), key);
+ } catch (final RemoteException e) {
+ // Client at the other end died
+ }
+ });
}
/**
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 98385c9..623990b 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -136,6 +136,7 @@
import com.android.server.statusbar.StatusBarManagerService;
import com.android.server.storage.DeviceStorageMonitorService;
import com.android.server.telecom.TelecomLoaderService;
+import com.android.server.testharness.TestHarnessModeService;
import com.android.server.textclassifier.TextClassificationManagerService;
import com.android.server.textservices.TextServicesManagerService;
import com.android.server.trust.TrustManagerService;
@@ -1157,6 +1158,10 @@
traceBeginAndSlog("StartPersistentDataBlock");
mSystemServiceManager.startService(PersistentDataBlockService.class);
traceEnd();
+
+ traceBeginAndSlog("StartTestHarnessMode");
+ mSystemServiceManager.startService(TestHarnessModeService.class);
+ traceEnd();
}
if (hasPdb || OemLockService.isHalPresent()) {
diff --git a/services/net/java/android/net/shared/NetworkMonitorUtils.java b/services/net/java/android/net/shared/NetworkMonitorUtils.java
index 463cf2a..3d2a2de 100644
--- a/services/net/java/android/net/shared/NetworkMonitorUtils.java
+++ b/services/net/java/android/net/shared/NetworkMonitorUtils.java
@@ -16,6 +16,11 @@
package android.net.shared;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+
import android.content.Context;
import android.net.NetworkCapabilities;
import android.provider.Settings;
@@ -58,9 +63,12 @@
* @param dfltNetCap Default requested network capabilities.
* @param nc Network capabilities of the network to test.
*/
- public static boolean isValidationRequired(
- NetworkCapabilities dfltNetCap, NetworkCapabilities nc) {
+ public static boolean isValidationRequired(NetworkCapabilities nc) {
// TODO: Consider requiring validation for DUN networks.
- return dfltNetCap.satisfiedByNetworkCapabilities(nc);
+ return nc != null
+ && nc.hasCapability(NET_CAPABILITY_INTERNET)
+ && nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ && nc.hasCapability(NET_CAPABILITY_TRUSTED)
+ && nc.hasCapability(NET_CAPABILITY_NOT_VPN);
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java
index 1a16e56..53d72bb 100644
--- a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java
@@ -1376,6 +1376,45 @@
verifyLightStateConditions(LIGHT_STATE_ACTIVE);
}
+ @Test
+ public void testStepToIdleMode() {
+ float delta = mDeviceIdleController.MIN_PRE_IDLE_FACTOR_CHANGE;
+ for (int mode = PowerManager.PRE_IDLE_TIMEOUT_MODE_NORMAL;
+ mode <= PowerManager.PRE_IDLE_TIMEOUT_MODE_LONG;
+ mode++) {
+ int ret = mDeviceIdleController.setPreIdleTimeoutMode(mode);
+ if (mode == PowerManager.PRE_IDLE_TIMEOUT_MODE_NORMAL) {
+ assertEquals("setPreIdleTimeoutMode: " + mode + " failed.",
+ mDeviceIdleController.SET_IDLE_FACTOR_RESULT_IGNORED, ret);
+ } else {
+ assertEquals("setPreIdleTimeoutMode: " + mode + " failed.",
+ mDeviceIdleController.SET_IDLE_FACTOR_RESULT_OK, ret);
+ }
+ //TODO(b/123045185): Mocked Handler of DeviceIdleController to make message loop
+ //workable in this test class
+ mDeviceIdleController.updatePreIdleFactor();
+ float expectedfactor = mDeviceIdleController.getPreIdleTimeoutByMode(mode);
+ float curfactor = mDeviceIdleController.getPreIdleTimeoutFactor();
+ assertEquals("Pre idle time factor of mode [" + mode + "].",
+ expectedfactor, curfactor, delta);
+ mDeviceIdleController.resetPreIdleTimeoutMode();
+ mDeviceIdleController.updatePreIdleFactor();
+
+ checkNextAlarmTimeWithNewPreIdleFactor(expectedfactor, STATE_INACTIVE);
+ checkNextAlarmTimeWithNewPreIdleFactor(expectedfactor, STATE_IDLE_PENDING);
+
+ checkNextAlarmTimeWithNewPreIdleFactor(expectedfactor, STATE_SENSING);
+ checkNextAlarmTimeWithNewPreIdleFactor(expectedfactor, STATE_LOCATING);
+ checkNextAlarmTimeWithNewPreIdleFactor(expectedfactor, STATE_QUICK_DOZE_DELAY);
+ checkNextAlarmTimeWithNewPreIdleFactor(expectedfactor, STATE_IDLE_MAINTENANCE);
+ checkNextAlarmTimeWithNewPreIdleFactor(expectedfactor, STATE_IDLE);
+ checkMaybeDoAnImmediateMaintenance(expectedfactor);
+ }
+ float curfactor = mDeviceIdleController.getPreIdleTimeoutFactor();
+ assertEquals("Pre idle time factor of mode default.",
+ 1.0f, curfactor, delta);
+ }
+
private void enterDeepState(int state) {
switch (state) {
case STATE_ACTIVE:
@@ -1599,4 +1638,84 @@
fail("Conditions for " + lightStateToString(expectedLightState) + " unknown.");
}
}
+
+ private void checkNextAlarmTimeWithNewPreIdleFactor(float factor, int state) {
+ final long errorTolerance = 1000;
+ enterDeepState(state);
+ long now = SystemClock.elapsedRealtime();
+ long alarm = mDeviceIdleController.getNextAlarmTime();
+ if (state == STATE_INACTIVE || state == STATE_IDLE_PENDING) {
+ int ret = mDeviceIdleController.setPreIdleTimeoutFactor(factor);
+ if (Float.compare(factor, 1.0f) == 0) {
+ assertEquals("setPreIdleTimeoutMode: " + factor + " failed.",
+ mDeviceIdleController.SET_IDLE_FACTOR_RESULT_IGNORED, ret);
+ } else {
+ assertEquals("setPreIdleTimeoutMode: " + factor + " failed.",
+ mDeviceIdleController.SET_IDLE_FACTOR_RESULT_OK, ret);
+ }
+ if (ret == mDeviceIdleController.SET_IDLE_FACTOR_RESULT_OK) {
+ mDeviceIdleController.updatePreIdleFactor();
+ long newAlarm = mDeviceIdleController.getNextAlarmTime();
+ long newDelay = (long) ((alarm - now) * factor);
+ assertTrue("setPreIdleTimeoutFactor: " + factor,
+ Math.abs(newDelay - (newAlarm - now)) < errorTolerance);
+ mDeviceIdleController.resetPreIdleTimeoutMode();
+ mDeviceIdleController.updatePreIdleFactor();
+ mDeviceIdleController.maybeDoImmediateMaintenance();
+ newAlarm = mDeviceIdleController.getNextAlarmTime();
+ assertTrue("resetPreIdleTimeoutMode from: " + factor,
+ Math.abs(newAlarm - alarm) < errorTolerance);
+ mDeviceIdleController.setPreIdleTimeoutFactor(factor);
+ now = SystemClock.elapsedRealtime();
+ enterDeepState(state);
+ newAlarm = mDeviceIdleController.getNextAlarmTime();
+ assertTrue("setPreIdleTimeoutFactor: " + factor + " before step to idle",
+ Math.abs(newDelay - (newAlarm - now)) < errorTolerance);
+ mDeviceIdleController.resetPreIdleTimeoutMode();
+ mDeviceIdleController.updatePreIdleFactor();
+ mDeviceIdleController.maybeDoImmediateMaintenance();
+ }
+ } else {
+ mDeviceIdleController.setPreIdleTimeoutFactor(factor);
+ mDeviceIdleController.updatePreIdleFactor();
+ long newAlarm = mDeviceIdleController.getNextAlarmTime();
+ assertTrue("setPreIdleTimeoutFactor: " + factor
+ + " shounld not change next alarm" ,
+ (newAlarm == alarm));
+ mDeviceIdleController.resetPreIdleTimeoutMode();
+ mDeviceIdleController.updatePreIdleFactor();
+ mDeviceIdleController.maybeDoImmediateMaintenance();
+ }
+ }
+
+ private void checkMaybeDoAnImmediateMaintenance(float factor) {
+ int ret = mDeviceIdleController.setPreIdleTimeoutFactor(factor);
+ final long minuteInMillis = 60 * 1000;
+ if (Float.compare(factor, 1.0f) == 0) {
+ assertEquals("setPreIdleTimeoutMode: " + factor + " failed.",
+ mDeviceIdleController.SET_IDLE_FACTOR_RESULT_IGNORED, ret);
+ } else {
+ assertEquals("setPreIdleTimeoutMode: " + factor + " failed.",
+ mDeviceIdleController.SET_IDLE_FACTOR_RESULT_OK, ret);
+ }
+ if (ret == mDeviceIdleController.SET_IDLE_FACTOR_RESULT_OK) {
+ enterDeepState(STATE_IDLE);
+ long now = SystemClock.elapsedRealtime();
+ long alarm = mDeviceIdleController.getNextAlarmTime();
+ mDeviceIdleController.setIdleStartTimeForTest(
+ now - (long) (mConstants.IDLE_TIMEOUT * 0.6));
+ mDeviceIdleController.maybeDoImmediateMaintenance();
+ long newAlarm = mDeviceIdleController.getNextAlarmTime();
+ assertTrue("maintenance not reschedule IDLE_TIMEOUT * 0.6",
+ newAlarm == alarm);
+ mDeviceIdleController.setIdleStartTimeForTest(
+ now - (long) (mConstants.IDLE_TIMEOUT * 1.2));
+ mDeviceIdleController.maybeDoImmediateMaintenance();
+ newAlarm = mDeviceIdleController.getNextAlarmTime();
+ assertTrue("maintenance not reschedule IDLE_TIMEOUT * 1.2",
+ (newAlarm - now) < minuteInMillis);
+ mDeviceIdleController.resetPreIdleTimeoutMode();
+ mDeviceIdleController.updatePreIdleFactor();
+ }
+ }
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 57ee6dc..cad71a2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -25,6 +25,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
+import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
import static com.android.server.job.JobSchedulerService.RARE_INDEX;
import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
@@ -370,16 +371,19 @@
mQuotaController.saveTimingSession(0, "com.android.test.stay", one);
ExecutionStats expectedStats = new ExecutionStats();
- expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
- mQuotaController.onAppRemovedLocked("com.android.test.remove", 10001);
+ final int uid = 10001;
+ mQuotaController.onAppRemovedLocked("com.android.test.remove", uid);
assertNull(mQuotaController.getTimingSessions(0, "com.android.test.remove"));
assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test.stay"));
assertEquals(expectedStats,
mQuotaController.getExecutionStatsLocked(0, "com.android.test.remove", RARE_INDEX));
assertNotEquals(expectedStats,
mQuotaController.getExecutionStatsLocked(0, "com.android.test.stay", RARE_INDEX));
+
+ assertFalse(mQuotaController.getForegroundUids().get(uid));
}
@Test
@@ -405,7 +409,7 @@
mQuotaController.saveTimingSession(10, "com.android.test", one);
ExecutionStats expectedStats = new ExecutionStats();
- expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
mQuotaController.onUserRemovedLocked(0);
@@ -440,14 +444,14 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 12 * HOUR_IN_MILLIS;
// Invalid time is now +24 hours since there are no sessions at all for the app.
- expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
mQuotaController.updateExecutionStatsLocked(0, "com.android.test.not.run", inputStats);
assertEquals(expectedStats, inputStats);
inputStats.windowSizeMs = expectedStats.windowSizeMs = MINUTE_IN_MILLIS;
// Invalid time is now +18 hours since there are no sessions in the window but the earliest
// session is 6 hours ago.
- expectedStats.invalidTimeElapsed = now + 18 * HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 18 * HOUR_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 0;
expectedStats.bgJobCountInWindow = 0;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -457,7 +461,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * MINUTE_IN_MILLIS;
// Invalid time is now since the session straddles the window cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 2 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 3;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -468,7 +472,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 5 * MINUTE_IN_MILLIS;
// Invalid time is now since the start of the session is at the very edge of the window
// cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 4 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 3;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -479,7 +483,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 49 * MINUTE_IN_MILLIS;
// Invalid time is now +44 minutes since the earliest session in the window is now-5
// minutes.
- expectedStats.invalidTimeElapsed = now + 44 * MINUTE_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 44 * MINUTE_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 4 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 3;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -489,7 +493,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 50 * MINUTE_IN_MILLIS;
// Invalid time is now since the session is at the very edge of the window cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 5 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 4;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -500,7 +504,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS;
// Invalid time is now since the start of the session is at the very edge of the window
// cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 6 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 5;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -510,7 +514,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
// Invalid time is now since the session straddles the window cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 11 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 10;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -523,7 +527,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * HOUR_IN_MILLIS;
// Invalid time is now +59 minutes since the earliest session in the window is now-121
// minutes.
- expectedStats.invalidTimeElapsed = now + 59 * MINUTE_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 59 * MINUTE_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 12 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 10;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -536,7 +540,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 6 * HOUR_IN_MILLIS;
// Invalid time is now since the start of the session is at the very edge of the window
// cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 15;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -546,14 +550,14 @@
mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
assertEquals(expectedStats, inputStats);
- // Make sure invalidTimeElapsed is set correctly when it's dependent on the max period.
+ // Make sure expirationTimeElapsed is set correctly when it's dependent on the max period.
mQuotaController.getTimingSessions(0, "com.android.test")
.add(0,
createTimingSession(now - (23 * HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 3));
inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
// Invalid time is now +1 hour since the earliest session in the max period is 1 hour
// before the end of the max period cutoff time.
- expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 15;
expectedStats.executionTimeInMaxPeriodMs = 23 * MINUTE_IN_MILLIS;
@@ -569,7 +573,7 @@
2 * MINUTE_IN_MILLIS, 2));
inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
// Invalid time is now since the earlist session straddles the max period cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 15;
expectedStats.executionTimeInMaxPeriodMs = 24 * MINUTE_IN_MILLIS;
@@ -599,7 +603,7 @@
// Active
expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS;
- expectedStats.invalidTimeElapsed = now + 4 * MINUTE_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 4 * MINUTE_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 3 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 5;
expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
@@ -609,7 +613,7 @@
// Working
expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 13 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 10;
expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
@@ -621,7 +625,7 @@
// Frequent
expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
- expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 23 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 15;
expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
@@ -633,7 +637,7 @@
// Rare
expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
- expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 33 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 20;
expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
@@ -675,7 +679,7 @@
ExecutionStats expectedStats = new ExecutionStats();
expectedStats.windowSizeMs = originalStatsActive.windowSizeMs;
- expectedStats.invalidTimeElapsed = originalStatsActive.invalidTimeElapsed;
+ expectedStats.expirationTimeElapsed = originalStatsActive.expirationTimeElapsed;
expectedStats.executionTimeInWindowMs = originalStatsActive.executionTimeInWindowMs;
expectedStats.bgJobCountInWindow = originalStatsActive.bgJobCountInWindow;
expectedStats.executionTimeInMaxPeriodMs = originalStatsActive.executionTimeInMaxPeriodMs;
@@ -688,7 +692,7 @@
assertEquals(expectedStats, newStatsActive);
expectedStats.windowSizeMs = originalStatsWorking.windowSizeMs;
- expectedStats.invalidTimeElapsed = originalStatsWorking.invalidTimeElapsed;
+ expectedStats.expirationTimeElapsed = originalStatsWorking.expirationTimeElapsed;
expectedStats.executionTimeInWindowMs = originalStatsWorking.executionTimeInWindowMs;
expectedStats.bgJobCountInWindow = originalStatsWorking.bgJobCountInWindow;
expectedStats.quotaCutoffTimeElapsed = originalStatsWorking.quotaCutoffTimeElapsed;
@@ -698,7 +702,7 @@
assertNotEquals(expectedStats, newStatsWorking);
expectedStats.windowSizeMs = originalStatsFrequent.windowSizeMs;
- expectedStats.invalidTimeElapsed = originalStatsFrequent.invalidTimeElapsed;
+ expectedStats.expirationTimeElapsed = originalStatsFrequent.expirationTimeElapsed;
expectedStats.executionTimeInWindowMs = originalStatsFrequent.executionTimeInWindowMs;
expectedStats.bgJobCountInWindow = originalStatsFrequent.bgJobCountInWindow;
expectedStats.quotaCutoffTimeElapsed = originalStatsFrequent.quotaCutoffTimeElapsed;
@@ -708,7 +712,7 @@
assertNotEquals(expectedStats, newStatsFrequent);
expectedStats.windowSizeMs = originalStatsRare.windowSizeMs;
- expectedStats.invalidTimeElapsed = originalStatsRare.invalidTimeElapsed;
+ expectedStats.expirationTimeElapsed = originalStatsRare.expirationTimeElapsed;
expectedStats.executionTimeInWindowMs = originalStatsRare.executionTimeInWindowMs;
expectedStats.bgJobCountInWindow = originalStatsRare.bgJobCountInWindow;
expectedStats.quotaCutoffTimeElapsed = originalStatsRare.quotaCutoffTimeElapsed;
@@ -719,6 +723,77 @@
}
@Test
+ public void testIsWithinQuotaLocked_NeverApp() {
+ assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test.never", NEVER_INDEX));
+ }
+
+ @Test
+ public void testIsWithinQuotaLocked_Charging() {
+ setCharging();
+ assertTrue(mQuotaController.isWithinQuotaLocked(0, "com.android.test", RARE_INDEX));
+ }
+
+ @Test
+ public void testIsWithinQuotaLocked_UnderDuration_UnderJobCount() {
+ setDischarging();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.incrementJobCount(0, "com.android.test", 5);
+ assertTrue(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX));
+ }
+
+ @Test
+ public void testIsWithinQuotaLocked_UnderDuration_OverJobCount() {
+ setDischarging();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ final int jobCount = mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME;
+ mQuotaController.saveTimingSession(0, "com.android.test.spam",
+ createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25));
+ mQuotaController.saveTimingSession(0, "com.android.test.spam",
+ createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, jobCount));
+ mQuotaController.incrementJobCount(0, "com.android.test.spam", jobCount);
+ assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test.spam",
+ WORKING_INDEX));
+
+ mQuotaController.saveTimingSession(0, "com.android.test.frequent",
+ createTimingSession(now - (2 * HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 2000));
+ mQuotaController.saveTimingSession(0, "com.android.test.frequent",
+ createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 500));
+ assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test.frequent",
+ FREQUENT_INDEX));
+ }
+
+ @Test
+ public void testIsWithinQuotaLocked_OverDuration_UnderJobCount() {
+ setDischarging();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (30 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (5 * MINUTE_IN_MILLIS), 4 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.incrementJobCount(0, "com.android.test", 5);
+ assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX));
+ }
+
+ @Test
+ public void testIsWithinQuotaLocked_OverDuration_OverJobCount() {
+ setDischarging();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ final int jobCount = mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME;
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, jobCount));
+ mQuotaController.incrementJobCount(0, "com.android.test", jobCount);
+ assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX));
+ }
+
+ @Test
public void testMaybeScheduleCleanupAlarmLocked() {
// No sessions saved yet.
mQuotaController.maybeScheduleCleanupAlarmLocked();
@@ -752,6 +827,7 @@
// Active window size is 10 minutes.
final int standbyBucket = ACTIVE_INDEX;
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND);
// No sessions saved yet.
mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -1016,11 +1092,37 @@
.set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", ACTIVE_INDEX);
- inOrder.verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
- any());
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
inOrder.verify(mAlarmManager, times(1)).cancel(any(AlarmManager.OnAlarmListener.class));
}
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_JobCount_AllowedTime() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ final int standbyBucket = WORKING_INDEX;
+ ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
+ SOURCE_PACKAGE, standbyBucket);
+ stats.jobCountInAllowedTime =
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME + 2;
+
+ // Invalid time in the past, so the count shouldn't be used.
+ stats.jobCountExpirationTimeElapsed =
+ now - mQuotaController.getAllowedTimePerPeriodMs() / 2;
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+ verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+ // Invalid time in the future, so the count should be used.
+ stats.jobCountExpirationTimeElapsed =
+ now + mQuotaController.getAllowedTimePerPeriodMs() / 2;
+ final long expectedWorkingAlarmTime =
+ stats.jobCountExpirationTimeElapsed + mQuotaController.getAllowedTimePerPeriodMs();
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+ verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+ }
/**
* Tests that the start alarm is properly rescheduled if the earliest session that contributes
@@ -1172,6 +1274,11 @@
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 45 * MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 60 * MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = 3 * HOUR_IN_MILLIS;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE = 5000;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING = 4000;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT = 3000;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE = 2000;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME = 500;
mQuotaController.onConstantsUpdatedLocked();
@@ -1183,11 +1290,16 @@
mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
assertEquals(60 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
assertEquals(3 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
+ assertEquals(500, mQuotaController.getMaxJobCountPerAllowedTime());
+ assertEquals(5000, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]);
+ assertEquals(4000, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]);
+ assertEquals(3000, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]);
+ assertEquals(2000, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]);
}
@Test
public void testConstantsUpdating_InvalidValues() {
- // Test negatives
+ // Test negatives/too low.
mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = -MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS = -MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_ACTIVE_MS = -MINUTE_IN_MILLIS;
@@ -1195,6 +1307,11 @@
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = -MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = -MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = -MINUTE_IN_MILLIS;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE = -1;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING = 1;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT = 1;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE = 1;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME = 0;
mQuotaController.onConstantsUpdatedLocked();
@@ -1205,6 +1322,11 @@
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
assertEquals(HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
+ assertEquals(10, mQuotaController.getMaxJobCountPerAllowedTime());
+ assertEquals(100, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]);
+ assertEquals(100, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]);
+ assertEquals(100, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]);
+ assertEquals(100, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]);
// Test larger than a day. Controller should cap at one day.
mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = 25 * HOUR_IN_MILLIS;
@@ -1246,6 +1368,7 @@
@Test
public void testTimerTracking_Discharging() {
setDischarging();
+ setProcessState(ActivityManager.PROCESS_STATE_BACKUP);
JobStatus jobStatus = createJobStatus("testTimerTracking_Discharging", 1);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
@@ -1293,6 +1416,8 @@
*/
@Test
public void testTimerTracking_ChargingAndDischarging() {
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+
JobStatus jobStatus = createJobStatus("testTimerTracking_ChargingAndDischarging", 1);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
JobStatus jobStatus2 = createJobStatus("testTimerTracking_ChargingAndDischarging", 2);
@@ -1363,6 +1488,7 @@
@Test
public void testTimerTracking_AllBackground() {
setDischarging();
+ setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
JobStatus jobStatus = createJobStatus("testTimerTracking_AllBackground", 1);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
@@ -1503,6 +1629,64 @@
}
/**
+ * Tests that Timers don't track job counts while in the foreground.
+ */
+ @Test
+ public void testTimerTracking_JobCount_Foreground() {
+ setDischarging();
+
+ final int standbyBucket = ACTIVE_INDEX;
+ JobStatus jobFg1 = createJobStatus("testTimerTracking_JobCount_Foreground", 1);
+ JobStatus jobFg2 = createJobStatus("testTimerTracking_JobCount_Foreground", 2);
+
+ mQuotaController.maybeStartTrackingJobLocked(jobFg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobFg2, null);
+ assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
+ SOURCE_PACKAGE, standbyBucket);
+ assertEquals(0, stats.jobCountInAllowedTime);
+
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ mQuotaController.prepareForExecutionLocked(jobFg1);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.prepareForExecutionLocked(jobFg2);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobFg1, null, false);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobFg2, null, false);
+ assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ assertEquals(0, stats.jobCountInAllowedTime);
+ }
+
+ /**
+ * Tests that Timers properly track job counts while in the background.
+ */
+ @Test
+ public void testTimerTracking_JobCount_Background() {
+ final int standbyBucket = WORKING_INDEX;
+ JobStatus jobBg1 = createJobStatus("testTimerTracking_JobCount_Background", 1);
+ JobStatus jobBg2 = createJobStatus("testTimerTracking_JobCount_Background", 2);
+ mQuotaController.maybeStartTrackingJobLocked(jobBg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
+
+ ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
+ SOURCE_PACKAGE, standbyBucket);
+ assertEquals(0, stats.jobCountInAllowedTime);
+
+ setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING);
+ mQuotaController.prepareForExecutionLocked(jobBg1);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.prepareForExecutionLocked(jobBg2);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobBg1, null, false);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+
+ assertEquals(2, stats.jobCountInAllowedTime);
+ }
+
+ /**
* Tests that Timers properly track overlapping top and background jobs.
*/
@Test
@@ -1680,6 +1864,7 @@
JobStatus jobStatus = createJobStatus("testTracking_OutOfQuota", 1);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
setStandbyBucket(WORKING_INDEX, jobStatus); // 2 hour window
+ setProcessState(ActivityManager.PROCESS_STATE_HOME);
// Now the package only has two seconds to run.
final long remainingTimeMs = 2 * SECOND_IN_MILLIS;
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -1707,6 +1892,7 @@
JobStatus jobStatus = createJobStatus("testTracking_OutOfQuota", 1);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
setStandbyBucket(WORKING_INDEX, jobStatus); // 2 hour window
+ setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
Handler handler = mQuotaController.getHandler();
spyOn(handler);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/MagnificationGestureHandlerTest.java
index 5083110..d91ce39 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/MagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MagnificationGestureHandlerTest.java
@@ -308,6 +308,24 @@
assertZoomsImmediatelyOnSwipeFrom(STATE_SHORTCUT_TRIGGERED);
}
+ @Test
+ public void testMultiTap_outOfDistanceSlop_shouldInIdle() {
+ // All delay motion events should be sent, if multi-tap with out of distance slop.
+ // STATE_IDLE will check if tapCount() < 2.
+ allowEventDelegation();
+ assertStaysIn(STATE_IDLE, () -> {
+ tap();
+ tap(DEFAULT_X * 2, DEFAULT_Y * 2);
+ });
+ assertStaysIn(STATE_IDLE, () -> {
+ tap();
+ tap(DEFAULT_X * 2, DEFAULT_Y * 2);
+ tap();
+ tap(DEFAULT_X * 2, DEFAULT_Y * 2);
+ tap();
+ });
+ }
+
private void assertZoomsImmediatelyOnSwipeFrom(int state) {
goFromStateIdleTo(state);
swipeAndHold();
@@ -531,6 +549,11 @@
send(upEvent());
}
+ private void tap(float x, float y) {
+ send(downEvent(x, y));
+ send(upEvent(x, y));
+ }
+
private void swipe() {
swipeAndHold();
send(upEvent());
@@ -572,18 +595,26 @@
}
private MotionEvent downEvent() {
+ return downEvent(DEFAULT_X, DEFAULT_Y);
+ }
+
+ private MotionEvent downEvent(float x, float y) {
mLastDownTime = mClock.now();
return fromTouchscreen(MotionEvent.obtain(mLastDownTime, mLastDownTime,
- ACTION_DOWN, DEFAULT_X, DEFAULT_Y, 0));
+ ACTION_DOWN, x, y, 0));
}
private MotionEvent upEvent() {
- return upEvent(mLastDownTime);
+ return upEvent(DEFAULT_X, DEFAULT_Y, mLastDownTime);
}
- private MotionEvent upEvent(long downTime) {
+ private MotionEvent upEvent(float x, float y) {
+ return upEvent(x, y, mLastDownTime);
+ }
+
+ private MotionEvent upEvent(float x, float y, long downTime) {
return fromTouchscreen(MotionEvent.obtain(downTime, mClock.now(),
- MotionEvent.ACTION_UP, DEFAULT_X, DEFAULT_Y, 0));
+ MotionEvent.ACTION_UP, x, y, 0));
}
private MotionEvent pointerEvent(int action, float x, float y) {
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 38e8ac2..0813e6fa 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -206,6 +206,8 @@
mAdmin1Context.binder.callingUid = DpmMockContext.CALLER_UID;
setUpUserManager();
+
+ when(getServices().lockPatternUtils.hasSecureLockScreen()).thenReturn(true);
}
private TransferOwnershipMetadataManager getMockTransferMetadataManager() {
@@ -836,6 +838,7 @@
MockUtils.checkIntent(intent),
MockUtils.checkUserHandle(MANAGED_PROFILE_USER_ID));
}
+
/**
* Test for: {@link DevicePolicyManager#setDeviceOwner} DO on system user installs successfully.
*/
@@ -2618,6 +2621,7 @@
when(getServices().lockPatternUtils
.isSeparateProfileChallengeEnabled(eq(PROFILE_USER))).thenReturn(false);
dpmi.reportSeparateProfileChallengeChanged(PROFILE_USER);
+ when(getServices().lockPatternUtils.hasSecureLockScreen()).thenReturn(true);
verifyScreenTimeoutCall(Long.MAX_VALUE, PROFILE_USER);
verifyScreenTimeoutCall(10L , UserHandle.USER_SYSTEM);
@@ -4233,6 +4237,41 @@
assertTrue(dpm.isActivePasswordSufficient());
}
+ public void testIsActivePasswordSufficient_noLockScreen() throws Exception {
+ // If there is no lock screen, the password is considered empty no matter what, because
+ // it provides no security.
+ when(getServices().lockPatternUtils.hasSecureLockScreen()).thenReturn(false);
+
+ mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
+ mContext.packageName = admin1.getPackageName();
+ setupDeviceOwner();
+
+ // If no password requirements are set, isActivePasswordSufficient should succeed.
+ assertTrue(dpm.isActivePasswordSufficient());
+
+ // Now set some password quality requirements.
+ dpm.setPasswordQuality(admin1, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING);
+
+ reset(mContext.spiedContext);
+ final int userHandle = UserHandle.getUserId(mContext.binder.callingUid);
+ PasswordMetrics passwordMetricsNoSymbols = new PasswordMetrics(
+ DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, 9,
+ 8, 2,
+ 6, 1,
+ 0, 1);
+ // This should be ignored, as there is no lock screen.
+ dpm.setActivePasswordState(passwordMetricsNoSymbols, userHandle);
+ dpm.reportPasswordChanged(userHandle);
+
+ // No broadcast should be sent.
+ verify(mContext.spiedContext, times(0)).sendBroadcastAsUser(
+ MockUtils.checkIntentAction(DeviceAdminReceiver.ACTION_PASSWORD_CHANGED),
+ MockUtils.checkUserHandle(userHandle));
+
+ // The active (nonexistent) password doesn't comply with the requirements.
+ assertFalse(dpm.isActivePasswordSufficient());
+ }
+
private void setActivePasswordState(PasswordMetrics passwordMetrics)
throws Exception {
final int userHandle = UserHandle.getUserId(mContext.binder.callingUid);
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
index 0328621..8afc3d3 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
@@ -22,7 +22,6 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
-import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -58,8 +57,7 @@
// Make sure that there is a short-circuit for DEFAULT_DISPLAY.
assertEquals(DEFAULT_DISPLAY,
InputMethodManagerService.computeImeDisplayIdForTarget(
- DEFAULT_DISPLAY, false /* isVrImeStarted */,
- sMustNotBeCalledChecker));
+ DEFAULT_DISPLAY, sMustNotBeCalledChecker));
}
@Test
@@ -67,17 +65,7 @@
// Make sure that there is a short-circuit for INVALID_DISPLAY.
assertEquals(DEFAULT_DISPLAY,
InputMethodManagerService.computeImeDisplayIdForTarget(
- INVALID_DISPLAY, false /* isVrImeStarted */,
- sMustNotBeCalledChecker));
- }
-
- @Test
- public void testComputeImeDisplayId_VrIme() {
- // Make sure that there is a short-circuit for VR IME.
- assertEquals(DEFAULT_DISPLAY,
- InputMethodManagerService.computeImeDisplayIdForTarget(
- SYSTEM_DECORATION_SUPPORT_DISPLAY_ID, true /* isVrImeStarted */,
- sMustNotBeCalledChecker));
+ INVALID_DISPLAY, sMustNotBeCalledChecker));
}
@Test
@@ -86,8 +74,7 @@
// Make sure IME displayId is DEFAULT_DISPLAY.
assertEquals(DEFAULT_DISPLAY,
InputMethodManagerService.computeImeDisplayIdForTarget(
- NO_SYSTEM_DECORATION_SUPPORT_DISPLAY_ID, false /* isVrImeStarted */,
- sChecker));
+ NO_SYSTEM_DECORATION_SUPPORT_DISPLAY_ID, sChecker));
}
@Test
@@ -96,7 +83,6 @@
// Make sure IME displayId is the same display.
assertEquals(SYSTEM_DECORATION_SUPPORT_DISPLAY_ID,
InputMethodManagerService.computeImeDisplayIdForTarget(
- SYSTEM_DECORATION_SUPPORT_DISPLAY_ID, false /* isVrImeStarted */,
- sChecker));
+ SYSTEM_DECORATION_SUPPORT_DISPLAY_ID, sChecker));
}
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index 2dc3510..cf89cb8 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -37,8 +37,8 @@
import android.os.IProgressListener;
import android.os.RemoteException;
import android.os.UserManager;
-import android.os.storage.StorageManager;
import android.os.storage.IStorageManager;
+import android.os.storage.StorageManager;
import android.security.KeyStore;
import android.test.AndroidTestCase;
@@ -46,6 +46,7 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockSettingsInternal;
import com.android.server.LocalServices;
+import com.android.server.wm.WindowManagerInternal;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@@ -85,6 +86,8 @@
KeyStore mKeyStore;
MockSyntheticPasswordManager mSpManager;
IAuthSecret mAuthSecretService;
+ WindowManagerInternal mMockWindowManager;
+ protected boolean mHasSecureLockScreen;
@Override
protected void setUp() throws Exception {
@@ -97,10 +100,13 @@
mActivityManager = mock(IActivityManager.class);
mDevicePolicyManager = mock(DevicePolicyManager.class);
mDevicePolicyManagerInternal = mock(DevicePolicyManagerInternal.class);
+ mMockWindowManager = mock(WindowManagerInternal.class);
LocalServices.removeServiceForTest(LockSettingsInternal.class);
LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class);
+ LocalServices.removeServiceForTest(WindowManagerInternal.class);
LocalServices.addService(DevicePolicyManagerInternal.class, mDevicePolicyManagerInternal);
+ LocalServices.addService(WindowManagerInternal.class, mMockWindowManager);
mContext = new MockLockSettingsContext(getContext(), mUserManager, mNotificationManager,
mDevicePolicyManager, mock(StorageManager.class), mock(TrustManager.class),
@@ -114,11 +120,17 @@
storageDir.mkdirs();
}
+ mHasSecureLockScreen = true;
mLockPatternUtils = new LockPatternUtils(mContext) {
@Override
public ILockSettings getLockSettings() {
return mService;
}
+
+ @Override
+ public boolean hasSecureLockScreen() {
+ return mHasSecureLockScreen;
+ }
};
mSpManager = new MockSyntheticPasswordManager(mContext, mStorage, mGateKeeperService,
mUserManager);
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
index e12f6d3..5124803 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
@@ -26,13 +26,12 @@
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
import android.os.RemoteException;
-import android.os.UserHandle;
import android.service.gatekeeper.GateKeeperResponse;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.VerifyCredentialResponse;
-import com.android.server.locksettings.LockSettingsStorage.CredentialHash;
import com.android.server.locksettings.FakeGateKeeperService.VerifyHandle;
+import com.android.server.locksettings.LockSettingsStorage.CredentialHash;
/**
* runtest frameworks-services -c com.android.server.locksettings.LockSettingsServiceTests
@@ -54,11 +53,21 @@
PASSWORD_QUALITY_ALPHABETIC);
}
+ public void testCreatePasswordFailsWithoutLockScreen() throws RemoteException {
+ testCreateCredentialFailsWithoutLockScreen(PRIMARY_USER_ID, "password",
+ CREDENTIAL_TYPE_PASSWORD, PASSWORD_QUALITY_ALPHABETIC);
+ }
+
public void testCreatePatternPrimaryUser() throws RemoteException {
testCreateCredential(PRIMARY_USER_ID, "123456789", CREDENTIAL_TYPE_PATTERN,
PASSWORD_QUALITY_SOMETHING);
}
+ public void testCreatePatternFailsWithoutLockScreen() throws RemoteException {
+ testCreateCredentialFailsWithoutLockScreen(PRIMARY_USER_ID, "123456789",
+ CREDENTIAL_TYPE_PATTERN, PASSWORD_QUALITY_SOMETHING);
+ }
+
public void testChangePasswordPrimaryUser() throws RemoteException {
testChangeCredentials(PRIMARY_USER_ID, "78963214", CREDENTIAL_TYPE_PATTERN,
"asdfghjk", CREDENTIAL_TYPE_PASSWORD, PASSWORD_QUALITY_ALPHABETIC);
@@ -198,6 +207,21 @@
assertVerifyCredentials(userId, credential, type, -1);
}
+ private void testCreateCredentialFailsWithoutLockScreen(
+ int userId, String credential, int type, int quality) throws RemoteException {
+ mHasSecureLockScreen = false;
+
+ try {
+ mService.setLockCredential(credential, type, null, quality, userId);
+ fail("An exception should have been thrown.");
+ } catch (UnsupportedOperationException e) {
+ // Success - the exception was expected.
+ }
+
+ assertFalse(mService.havePassword(userId));
+ assertFalse(mService.havePattern(userId));
+ }
+
private void testChangeCredentials(int userId, String newCredential, int newType,
String oldCredential, int oldType, int quality) throws RemoteException {
final long sid = 1234;
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsShellCommandTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsShellCommandTest.java
index a28a5a1..929c3b5 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsShellCommandTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsShellCommandTest.java
@@ -27,6 +27,7 @@
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
@@ -77,6 +78,7 @@
final Context context = InstrumentationRegistry.getTargetContext();
mUserId = ActivityManager.getCurrentUser();
mCommand = new LockSettingsShellCommand(mLockPatternUtils);
+ when(mLockPatternUtils.hasSecureLockScreen()).thenReturn(true);
}
@Test
@@ -103,6 +105,16 @@
}
@Test
+ public void testChangePin_noLockScreen() throws Exception {
+ when(mLockPatternUtils.hasSecureLockScreen()).thenReturn(false);
+ assertEquals(-1, mCommand.exec(new Binder(), in, out, err,
+ new String[] { "set-pin", "--old", "1234", "4321" },
+ mShellCallback, mResultReceiver));
+ verify(mLockPatternUtils).hasSecureLockScreen();
+ verifyNoMoreInteractions(mLockPatternUtils);
+ }
+
+ @Test
public void testChangePassword() throws Exception {
when(mLockPatternUtils.isLockPatternEnabled(mUserId)).thenReturn(false);
when(mLockPatternUtils.isLockPasswordEnabled(mUserId)).thenReturn(true);
@@ -115,6 +127,16 @@
}
@Test
+ public void testChangePassword_noLockScreen() throws Exception {
+ when(mLockPatternUtils.hasSecureLockScreen()).thenReturn(false);
+ assertEquals(-1, mCommand.exec(new Binder(), in, out, err,
+ new String[] { "set-password", "--old", "1234", "4321" },
+ mShellCallback, mResultReceiver));
+ verify(mLockPatternUtils).hasSecureLockScreen();
+ verifyNoMoreInteractions(mLockPatternUtils);
+ }
+
+ @Test
public void testChangePattern() throws Exception {
when(mLockPatternUtils.isLockPatternEnabled(mUserId)).thenReturn(true);
when(mLockPatternUtils.isLockPasswordEnabled(mUserId)).thenReturn(false);
@@ -126,6 +148,16 @@
}
@Test
+ public void testChangePattern_noLockScreen() throws Exception {
+ when(mLockPatternUtils.hasSecureLockScreen()).thenReturn(false);
+ assertEquals(-1, mCommand.exec(new Binder(), in, out, err,
+ new String[] { "set-pattern", "--old", "1234", "4321" },
+ mShellCallback, mResultReceiver));
+ verify(mLockPatternUtils).hasSecureLockScreen();
+ verifyNoMoreInteractions(mLockPatternUtils);
+ }
+
+ @Test
public void testClear() throws Exception {
when(mLockPatternUtils.isLockPatternEnabled(mUserId)).thenReturn(true);
when(mLockPatternUtils.isLockPasswordEnabled(mUserId)).thenReturn(false);
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 94e02bc..0595a5b 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -40,10 +40,10 @@
import com.android.server.locksettings.SyntheticPasswordManager.AuthenticationToken;
import com.android.server.locksettings.SyntheticPasswordManager.PasswordData;
-import java.util.ArrayList;
-
import org.mockito.ArgumentCaptor;
+import java.util.ArrayList;
+
/**
* runtest frameworks-services -c com.android.server.locksettings.SyntheticPasswordTests
@@ -448,6 +448,37 @@
assertTrue(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
}
+ public void testSetLockCredentialWithTokenFailsWithoutLockScreen() throws Exception {
+ final String password = "password";
+ final String pattern = "123654";
+ final String token = "some-high-entropy-secure-token";
+
+ mHasSecureLockScreen = false;
+ enableSyntheticPassword();
+ long handle = mLocalService.addEscrowToken(token.getBytes(), PRIMARY_USER_ID);
+ assertTrue(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+ try {
+ mLocalService.setLockCredentialWithToken(password,
+ LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, token.getBytes(),
+ PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
+ fail("An exception should have been thrown.");
+ } catch (UnsupportedOperationException e) {
+ // Success - the exception was expected.
+ }
+ assertFalse(mService.havePassword(PRIMARY_USER_ID));
+
+ try {
+ mLocalService.setLockCredentialWithToken(pattern,
+ LockPatternUtils.CREDENTIAL_TYPE_PATTERN, handle, token.getBytes(),
+ PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
+ fail("An exception should have been thrown.");
+ } catch (UnsupportedOperationException e) {
+ // Success - the exception was expected.
+ }
+ assertFalse(mService.havePattern(PRIMARY_USER_ID));
+ }
+
public void testgetHashFactorPrimaryUser() throws RemoteException {
final String password = "password";
mService.setLockCredential(password, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java
index f817e8e..6da202b 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java
@@ -16,8 +16,6 @@
package com.android.server.pm.dex;
-import static com.android.server.pm.dex.PackageDynamicCodeLoading.FILE_TYPE_DEX;
-
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.atMost;
@@ -26,10 +24,12 @@
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
+import android.os.UserHandle;
import android.os.storage.StorageManager;
import androidx.test.filters.SmallTest;
@@ -56,40 +56,44 @@
public class DexLoggerTests {
private static final String OWNING_PACKAGE_NAME = "package.name";
private static final String VOLUME_UUID = "volUuid";
- private static final String DEX_PATH = "/bar/foo.jar";
+ private static final String FILE_PATH = "/bar/foo.jar";
private static final int STORAGE_FLAGS = StorageManager.FLAG_STORAGE_DE;
private static final int OWNER_UID = 43;
private static final int OWNER_USER_ID = 44;
// Obtained via: echo -n "foo.jar" | sha256sum
- private static final String DEX_FILENAME_HASH =
+ private static final String FILENAME_HASH =
"91D7B844D7CC9673748FF057D8DC83972280FC28537D381AA42015A9CF214B9F";
- private static final byte[] CONTENT_HASH_BYTES = new byte[] {
- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
- 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32
+ private static final byte[] CONTENT_HASH_BYTES = new byte[]{
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+ 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32
};
private static final String CONTENT_HASH =
"0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20";
private static final byte[] EMPTY_BYTES = {};
- @Rule public MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+ private static final String EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH =
+ "dcl:" + FILENAME_HASH;
+ private static final String EXPECTED_MESSAGE_WITH_CONTENT_HASH =
+ EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH + " " + CONTENT_HASH;
+ private static final String EXPECTED_MESSAGE_NATIVE_WITH_CONTENT_HASH =
+ "dcln:" + FILENAME_HASH + " " + CONTENT_HASH;
+
+ @Rule public MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.LENIENT);
@Mock IPackageManager mPM;
@Mock Installer mInstaller;
- private PackageDynamicCodeLoading mPackageDynamicCodeLoading;
private DexLogger mDexLogger;
private final ListMultimap<Integer, String> mMessagesForUid = ArrayListMultimap.create();
private boolean mWriteTriggered = false;
- private static final String EXPECTED_MESSAGE_WITH_CONTENT_HASH =
- DEX_FILENAME_HASH + " " + CONTENT_HASH;
@Before
public void setup() throws Exception {
// Disable actually attempting to do file writes.
- mPackageDynamicCodeLoading = new PackageDynamicCodeLoading() {
+ PackageDynamicCodeLoading packageDynamicCodeLoading = new PackageDynamicCodeLoading() {
@Override
void maybeWriteAsync() {
mWriteTriggered = true;
@@ -102,13 +106,13 @@
};
// For test purposes capture log messages as well as sending to the event log.
- mDexLogger = new DexLogger(mPM, mInstaller, mPackageDynamicCodeLoading) {
+ mDexLogger = new DexLogger(mPM, mInstaller, packageDynamicCodeLoading) {
@Override
- void writeDclEvent(int uid, String message) {
- super.writeDclEvent(uid, message);
- mMessagesForUid.put(uid, message);
- }
- };
+ void writeDclEvent(String subtag, int uid, String message) {
+ super.writeDclEvent(subtag, uid, message);
+ mMessagesForUid.put(uid, subtag + ":" + message);
+ }
+ };
// Make the owning package exist in our mock PackageManager.
ApplicationInfo appInfo = new ApplicationInfo();
@@ -124,9 +128,9 @@
@Test
public void testOneLoader_ownFile_withFileHash() throws Exception {
- whenFileIsHashed(DEX_PATH, doReturn(CONTENT_HASH_BYTES));
+ whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES));
- recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+ recordLoad(OWNING_PACKAGE_NAME, FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID);
@@ -139,13 +143,13 @@
@Test
public void testOneLoader_ownFile_noFileHash() throws Exception {
- whenFileIsHashed(DEX_PATH, doReturn(EMPTY_BYTES));
+ whenFileIsHashed(FILE_PATH, doReturn(EMPTY_BYTES));
- recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+ recordLoad(OWNING_PACKAGE_NAME, FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID);
- assertThat(mMessagesForUid).containsEntry(OWNER_UID, DEX_FILENAME_HASH);
+ assertThat(mMessagesForUid).containsEntry(OWNER_UID, EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH);
// File should be removed from the DCL list, since we can't hash it.
assertThat(mWriteTriggered).isTrue();
@@ -154,13 +158,14 @@
@Test
public void testOneLoader_ownFile_hashingFails() throws Exception {
- whenFileIsHashed(DEX_PATH, doThrow(new InstallerException("Intentional failure for test")));
+ whenFileIsHashed(FILE_PATH,
+ doThrow(new InstallerException("Intentional failure for test")));
- recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+ recordLoad(OWNING_PACKAGE_NAME, FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID);
- assertThat(mMessagesForUid).containsEntry(OWNER_UID, DEX_FILENAME_HASH);
+ assertThat(mMessagesForUid).containsEntry(OWNER_UID, EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH);
// File should be removed from the DCL list, since we can't hash it.
assertThat(mWriteTriggered).isTrue();
@@ -178,11 +183,23 @@
}
@Test
+ public void testOneLoader_pathTraversal() throws Exception {
+ String filePath = "/bar/../secret/foo.jar";
+ whenFileIsHashed(filePath, doReturn(CONTENT_HASH_BYTES));
+ setPackageUid(OWNING_PACKAGE_NAME, -1);
+
+ recordLoad(OWNING_PACKAGE_NAME, filePath);
+ mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
+
+ assertThat(mMessagesForUid).isEmpty();
+ }
+
+ @Test
public void testOneLoader_differentOwner() throws Exception {
- whenFileIsHashed(DEX_PATH, doReturn(CONTENT_HASH_BYTES));
+ whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES));
setPackageUid("other.package.name", 1001);
- recordLoad("other.package.name", DEX_PATH);
+ recordLoad("other.package.name", FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid.keys()).containsExactly(1001);
@@ -192,10 +209,10 @@
@Test
public void testOneLoader_differentOwner_uninstalled() throws Exception {
- whenFileIsHashed(DEX_PATH, doReturn(CONTENT_HASH_BYTES));
+ whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES));
setPackageUid("other.package.name", -1);
- recordLoad("other.package.name", DEX_PATH);
+ recordLoad("other.package.name", FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid).isEmpty();
@@ -203,22 +220,38 @@
}
@Test
+ public void testNativeCodeLoad() throws Exception {
+ whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES));
+
+ recordLoadNative(FILE_PATH);
+ mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
+
+ assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID);
+ assertThat(mMessagesForUid)
+ .containsEntry(OWNER_UID, EXPECTED_MESSAGE_NATIVE_WITH_CONTENT_HASH);
+
+ assertThat(mWriteTriggered).isFalse();
+ assertThat(mDexLogger.getAllPackagesWithDynamicCodeLoading())
+ .containsExactly(OWNING_PACKAGE_NAME);
+ }
+
+ @Test
public void testMultipleLoadersAndFiles() throws Exception {
String otherDexPath = "/bar/nosuchdir/foo.jar";
- whenFileIsHashed(DEX_PATH, doReturn(CONTENT_HASH_BYTES));
+ whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES));
whenFileIsHashed(otherDexPath, doReturn(EMPTY_BYTES));
setPackageUid("other.package.name1", 1001);
setPackageUid("other.package.name2", 1002);
- recordLoad("other.package.name1", DEX_PATH);
+ recordLoad("other.package.name1", FILE_PATH);
recordLoad("other.package.name1", otherDexPath);
- recordLoad("other.package.name2", DEX_PATH);
- recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+ recordLoad("other.package.name2", FILE_PATH);
+ recordLoad(OWNING_PACKAGE_NAME, FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid.keys()).containsExactly(1001, 1001, 1002, OWNER_UID);
assertThat(mMessagesForUid).containsEntry(1001, EXPECTED_MESSAGE_WITH_CONTENT_HASH);
- assertThat(mMessagesForUid).containsEntry(1001, DEX_FILENAME_HASH);
+ assertThat(mMessagesForUid).containsEntry(1001, EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH);
assertThat(mMessagesForUid).containsEntry(1002, EXPECTED_MESSAGE_WITH_CONTENT_HASH);
assertThat(mMessagesForUid).containsEntry(OWNER_UID, EXPECTED_MESSAGE_WITH_CONTENT_HASH);
@@ -233,7 +266,7 @@
@Test
public void testUnknownOwner() {
reset(mPM);
- recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+ recordLoad(OWNING_PACKAGE_NAME, FILE_PATH);
mDexLogger.logDynamicCodeLoading("other.package.name");
assertThat(mMessagesForUid).isEmpty();
@@ -244,7 +277,7 @@
@Test
public void testUninstalledPackage() {
reset(mPM);
- recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+ recordLoad(OWNING_PACKAGE_NAME, FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid).isEmpty();
@@ -262,7 +295,16 @@
}
private void recordLoad(String loadingPackageName, String dexPath) {
- mPackageDynamicCodeLoading.record(
- OWNING_PACKAGE_NAME, dexPath, FILE_TYPE_DEX, OWNER_USER_ID, loadingPackageName);
+ mDexLogger.recordDex(OWNER_USER_ID, dexPath, OWNING_PACKAGE_NAME, loadingPackageName);
+ mWriteTriggered = false;
+ }
+
+ private void recordLoadNative(String nativePath) throws Exception {
+ int loadingUid = UserHandle.getUid(OWNER_USER_ID, OWNER_UID);
+ String[] packageNames = { OWNING_PACKAGE_NAME };
+ when(mPM.getPackagesForUid(loadingUid)).thenReturn(packageNames);
+
+ mDexLogger.recordNative(loadingUid, nativePath);
+ mWriteTriggered = false;
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 319ffed..8be63fc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -31,7 +31,6 @@
import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_RIGHT;
import static com.android.server.wm.ActivityStack.ActivityState.INITIALIZING;
import static com.android.server.wm.ActivityStack.ActivityState.PAUSING;
-import static com.android.server.wm.ActivityStack.ActivityState.RESUMED;
import static com.android.server.wm.ActivityStack.ActivityState.STOPPED;
import static com.android.server.wm.ActivityStack.REMOVE_TASK_MODE_MOVING;
@@ -76,9 +75,6 @@
mStack = (TestActivityStack) new StackBuilder(mRootActivityContainer).build();
mTask = mStack.getChildAt(0);
mActivity = mTask.getTopActivity();
-
- doReturn(false).when(mService).isBooting();
- doReturn(true).when(mService).isBooted();
}
@Test
@@ -121,23 +117,22 @@
mActivity.setState(STOPPED, "testPausingWhenVisibleFromStopped");
- // The activity is in the focused stack so it should be resumed.
+ // The activity is in the focused stack so it should not move to paused.
mActivity.makeVisibleIfNeeded(null /* starting */, true /* reportToClient */);
- assertTrue(mActivity.isState(RESUMED));
+ assertTrue(mActivity.isState(STOPPED));
assertFalse(pauseFound.value);
- // Make the activity non focusable
- mActivity.setState(STOPPED, "testPausingWhenVisibleFromStopped");
- doReturn(false).when(mActivity).isFocusable();
+ // Clear focused stack
+ final ActivityDisplay display = mRootActivityContainer.getDefaultDisplay();
+ when(display.getFocusedStack()).thenReturn(null);
- // If the activity is not focusable, it should move to paused.
+ // In the unfocused stack, the activity should move to paused.
mActivity.makeVisibleIfNeeded(null /* starting */, true /* reportToClient */);
assertTrue(mActivity.isState(PAUSING));
assertTrue(pauseFound.value);
// Make sure that the state does not change for current non-stopping states.
mActivity.setState(INITIALIZING, "testPausingWhenVisibleFromStopped");
- doReturn(true).when(mActivity).isFocusable();
mActivity.makeVisibleIfNeeded(null /* starting */, true /* reportToClient */);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
index ea8f33f..68df87e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
@@ -55,7 +55,6 @@
import android.hardware.display.DisplayManagerGlobal;
import android.os.Handler;
import android.os.Looper;
-import android.os.PowerManager;
import android.os.Process;
import android.os.UserHandle;
import android.service.voice.IVoiceInteractionSession;
@@ -426,7 +425,6 @@
doReturn(mock(IPackageManager.class)).when(this).getPackageManager();
// allow background activity starts by default
doReturn(true).when(this).isBackgroundActivityStartsEnabled();
- doNothing().when(this).updateCpuStats();
}
void setup(IntentFirewall intentFirewall, PendingIntentController intentController,
@@ -582,8 +580,6 @@
doNothing().when(this).acquireLaunchWakelock();
doReturn(mKeyguardController).when(this).getKeyguardController();
- mLaunchingActivity = mock(PowerManager.WakeLock.class);
-
initialize();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
index d0b9225..ea5ab7b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
@@ -70,9 +70,9 @@
mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
verify(mTransaction).reparent(eq(mToken.getSurfaceControl()),
- eq(mToken.mSurfaceAnimator.mLeash.getHandle()));
+ eq(mToken.mSurfaceAnimator.mLeash));
verify(mTransaction).reparent(eq(mToken.mSurfaceAnimator.mLeash),
- eq(mToken.mAnimationBoundsLayer.getHandle()));
+ eq(mToken.mAnimationBoundsLayer));
}
@Test
@@ -111,7 +111,7 @@
mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
verify(mTransaction).reparent(eq(mToken.getSurfaceControl()),
- eq(mToken.mSurfaceAnimator.mLeash.getHandle()));
+ eq(mToken.mSurfaceAnimator.mLeash));
assertThat(mToken.mAnimationBoundsLayer).isNull();
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
index ad80cd6..9b84215 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
@@ -90,7 +90,7 @@
final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass(
OnAnimationFinishedCallback.class);
assertAnimating(mAnimatable);
- verify(mTransaction).reparent(eq(mAnimatable.mSurface), eq(mAnimatable.mLeash.getHandle()));
+ verify(mTransaction).reparent(eq(mAnimatable.mSurface), eq(mAnimatable.mLeash));
verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture());
callbackCaptor.getValue().onAnimationFinished(mSpec);
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index 6c4b1af8..0fe5e08 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -793,15 +793,17 @@
* <p>
* Apps must be prepared for this method to return {@code null}, indicating that there currently
* exists no user-chosen default {@code PhoneAccount}.
+ * <p>
+ * The default dialer has access to use this method.
*
* @return The user outgoing phone account selected by the user.
- * @hide
*/
- @UnsupportedAppUsage
+ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
public PhoneAccountHandle getUserSelectedOutgoingPhoneAccount() {
try {
if (isServiceConnected()) {
- return getTelecomService().getUserSelectedOutgoingPhoneAccount();
+ return getTelecomService().getUserSelectedOutgoingPhoneAccount(
+ mContext.getOpPackageName());
}
} catch (RemoteException e) {
Log.e(TAG, "Error calling ITelecomService#getUserSelectedOutgoingPhoneAccount", e);
@@ -810,10 +812,14 @@
}
/**
- * Sets the user-chosen default for making outgoing phone calls.
+ * Sets the user-chosen default {@link PhoneAccountHandle} for making outgoing phone calls.
+ *
+ * @param accountHandle The {@link PhoneAccountHandle} which will be used by default for making
+ * outgoing voice calls.
* @hide
*/
- @UnsupportedAppUsage
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ @SystemApi
public void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle accountHandle) {
try {
if (isServiceConnected()) {
diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
index 954a709..e1d5c17 100644
--- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
+++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
@@ -45,7 +45,7 @@
/**
* @see TelecomServiceImpl#getUserSelectedOutgoingPhoneAccount
*/
- PhoneAccountHandle getUserSelectedOutgoingPhoneAccount();
+ PhoneAccountHandle getUserSelectedOutgoingPhoneAccount(String callingPackage);
/**
* @see TelecomServiceImpl#setUserSelectedOutgoingPhoneAccount
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index 0e5c71d..0fa1b41 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -129,6 +129,66 @@
"android.telephony.euicc.action.RESOLVE_ERROR";
/**
+ * Intent action sent by system apps (such as the Settings app) to the Telephony framework to
+ * enable or disable a subscription. Must be accompanied with {@link #EXTRA_SUBSCRIPTION_ID} and
+ * {@link #EXTRA_ENABLE_SUBSCRIPTION}.
+ *
+ * <p>Unlike {@link #switchToSubscription(int, PendingIntent)}, using this action allows the
+ * underlying eUICC service (i.e. the LPA app) to control the UI experience during this
+ * operation. The action is received by the Telephony framework, which in turn selects and
+ * launches an appropriate LPA activity to present UI to the user. For example, the activity may
+ * show a confirmation dialog, a progress dialog, or an error dialog when necessary.
+ *
+ * <p>The launched activity will immediately finish with
+ * {@link android.app.Activity#RESULT_CANCELED} if {@link #isEnabled} is false.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String ACTION_TOGGLE_SUBSCRIPTION_PRIVILEGED =
+ "android.telephony.euicc.action.TOGGLE_SUBSCRIPTION_PRIVILEGED";
+
+ /**
+ * Intent action sent by system apps (such as the Settings app) to the Telephony framework to
+ * delete a subscription. Must be accompanied with {@link #EXTRA_SUBSCRIPTION_ID}.
+ *
+ * <p>Unlike {@link #deleteSubscription(int, PendingIntent)}, using this action allows the
+ * underlying eUICC service (i.e. the LPA app) to control the UI experience during this
+ * operation. The action is received by the Telephony framework, which in turn selects and
+ * launches an appropriate LPA activity to present UI to the user. For example, the activity may
+ * show a confirmation dialog, a progress dialog, or an error dialog when necessary.
+ *
+ * <p>The launched activity will immediately finish with
+ * {@link android.app.Activity#RESULT_CANCELED} if {@link #isEnabled} is false.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String ACTION_DELETE_SUBSCRIPTION_PRIVILEGED =
+ "android.telephony.euicc.action.DELETE_SUBSCRIPTION_PRIVILEGED";
+
+ /**
+ * Intent action sent by system apps (such as the Settings app) to the Telephony framework to
+ * rename a subscription. Must be accompanied with {@link #EXTRA_SUBSCRIPTION_ID} and
+ * {@link #EXTRA_SUBSCRIPTION_NICKNAME}.
+ *
+ * <p>Unlike {@link #updateSubscriptionNickname(int, String, PendingIntent)}, using this action
+ * allows the the underlying eUICC service (i.e. the LPA app) to control the UI experience
+ * during this operation. The action is received by the Telephony framework, which in turn
+ * selects and launches an appropriate LPA activity to present UI to the user. For example, the
+ * activity may show a confirmation dialog, a progress dialog, or an error dialog when
+ * necessary.
+ *
+ * <p>The launched activity will immediately finish with
+ * {@link android.app.Activity#RESULT_CANCELED} if {@link #isEnabled} is false.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String ACTION_RENAME_SUBSCRIPTION_PRIVILEGED =
+ "android.telephony.euicc.action.RENAME_SUBSCRIPTION_PRIVILEGED";
+
+ /**
* Result code for an operation indicating that the operation succeeded.
*/
public static final int EMBEDDED_SUBSCRIPTION_RESULT_OK = 0;
@@ -219,6 +279,37 @@
"android.telephony.euicc.extra.FORCE_PROVISION";
/**
+ * Key for an extra set on privileged actions {@link #ACTION_TOGGLE_SUBSCRIPTION_PRIVILEGED},
+ * {@link #ACTION_DELETE_SUBSCRIPTION_PRIVILEGED}, and
+ * {@link #ACTION_RENAME_SUBSCRIPTION_PRIVILEGED} providing the ID of the targeted subscription.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String EXTRA_SUBSCRIPTION_ID =
+ "android.telephony.euicc.extra.SUBSCRIPTION_ID";
+
+ /**
+ * Key for an extra set on {@link #ACTION_TOGGLE_SUBSCRIPTION_PRIVILEGED} providing a boolean
+ * value of whether to enable or disable the targeted subscription.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String EXTRA_ENABLE_SUBSCRIPTION =
+ "android.telephony.euicc.extra.ENABLE_SUBSCRIPTION";
+
+ /**
+ * Key for an extra set on {@link #ACTION_RENAME_SUBSCRIPTION_PRIVILEGED} providing a new
+ * nickname for the targeted subscription.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String EXTRA_SUBSCRIPTION_NICKNAME =
+ "android.telephony.euicc.extra.SUBSCRIPTION_NICKNAME";
+
+ /**
* Optional meta-data attribute for a carrier app providing an icon to use to represent the
* carrier. If not provided, the app's launcher icon will be used as a fallback.
*/
diff --git a/tests/DexLoggerIntegrationTests/Android.mk b/tests/DexLoggerIntegrationTests/Android.mk
index ee2ec0a..979d13a 100644
--- a/tests/DexLoggerIntegrationTests/Android.mk
+++ b/tests/DexLoggerIntegrationTests/Android.mk
@@ -29,6 +29,35 @@
dexloggertest_jar := $(LOCAL_BUILT_MODULE)
+# Also build a native library that the test app can dynamically load
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_MODULE := DexLoggerNativeTestLibrary
+LOCAL_MULTILIB := first
+LOCAL_SRC_FILES := src/cpp/com_android_dcl_Jni.cpp
+LOCAL_C_INCLUDES += \
+ $(JNI_H_INCLUDE)
+LOCAL_SDK_VERSION := 28
+LOCAL_NDK_STL_VARIANT := c++_static
+
+include $(BUILD_SHARED_LIBRARY)
+
+dexloggertest_so := $(LOCAL_BUILT_MODULE)
+
+# And a standalone native executable that we can exec.
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_MODULE := DexLoggerNativeExecutable
+LOCAL_SRC_FILES := src/cpp/test_executable.cpp
+
+include $(BUILD_EXECUTABLE)
+
+dexloggertest_executable := $(LOCAL_BUILT_MODULE)
+
# Build the test app itself
include $(CLEAR_VARS)
@@ -37,14 +66,18 @@
LOCAL_PACKAGE_NAME := DexLoggerIntegrationTests
LOCAL_SDK_VERSION := current
LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_CERTIFICATE := platform
+LOCAL_CERTIFICATE := shared
LOCAL_SRC_FILES := $(call all-java-files-under, src/com/android/server/pm)
LOCAL_STATIC_JAVA_LIBRARIES := \
android-support-test \
truth-prebuilt \
-# This gets us the javalib.jar built by DexLoggerTestLibrary above.
-LOCAL_JAVA_RESOURCE_FILES := $(dexloggertest_jar)
+# This gets us the javalib.jar built by DexLoggerTestLibrary above as well as the various
+# native binaries.
+LOCAL_JAVA_RESOURCE_FILES := \
+ $(dexloggertest_jar) \
+ $(dexloggertest_so) \
+ $(dexloggertest_executable)
include $(BUILD_PACKAGE)
diff --git a/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java b/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java
index 75ee089..d68769b 100644
--- a/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java
+++ b/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java
@@ -17,6 +17,7 @@
package com.android.server.pm.dex;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import android.app.UiAutomation;
import android.content.Context;
@@ -25,6 +26,7 @@
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.util.EventLog;
+import android.util.EventLog.Event;
import dalvik.system.DexClassLoader;
@@ -65,14 +67,13 @@
// Event log tag used for SNET related events
private static final int SNET_TAG = 0x534e4554;
- // Subtag used to distinguish dynamic code loading events
- private static final String DCL_SUBTAG = "dcl";
+ // Subtags used to distinguish dynamic code loading events
+ private static final String DCL_DEX_SUBTAG = "dcl";
+ private static final String DCL_NATIVE_SUBTAG = "dcln";
- // All the tags we care about
- private static final int[] TAG_LIST = new int[] { SNET_TAG };
-
- // This is {@code DynamicCodeLoggingService#JOB_ID}
- private static final int DYNAMIC_CODE_LOGGING_JOB_ID = 2030028;
+ // These are job IDs from DynamicCodeLoggingService
+ private static final int IDLE_LOGGING_JOB_ID = 2030028;
+ private static final int AUDIT_WATCHING_JOB_ID = 203142925;
private static Context sContext;
private static int sMyUid;
@@ -89,15 +90,20 @@
// Without this the first test passes and others don't - we don't see new events in the
// log. The exact reason is unclear.
EventLog.writeEvent(SNET_TAG, "Dummy event");
+
+ // Audit log messages are throttled by the kernel (at the request of logd) to 5 per
+ // second, so running the tests too quickly in sequence means we lose some and get
+ // spurious failures. Sigh.
+ SystemClock.sleep(1000);
}
@Test
- public void testDexLoggerGeneratesEvents() throws Exception {
- File privateCopyFile = fileForJar("copied.jar");
+ public void testDexLoggerGeneratesEvents_standardClassLoader() throws Exception {
+ File privateCopyFile = privateFile("copied.jar");
// Obtained via "echo -n copied.jar | sha256sum"
String expectedNameHash =
"1B6C71DB26F36582867432CCA12FB6A517470C9F9AABE9198DD4C5C030D6DC0C";
- String expectedContentHash = copyAndHashJar(privateCopyFile);
+ String expectedContentHash = copyAndHashResource("/javalib.jar", privateCopyFile);
// Feed the jar to a class loader and make sure it contains what we expect.
ClassLoader parentClassLoader = sContext.getClass().getClassLoader();
@@ -107,18 +113,18 @@
// And make sure we log events about it
long previousEventNanos = mostRecentEventTimeNanos();
- runDexLogger();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
- assertDclLoggedSince(previousEventNanos, expectedNameHash, expectedContentHash);
+ assertDclLoggedSince(previousEventNanos, DCL_DEX_SUBTAG,
+ expectedNameHash, expectedContentHash);
}
@Test
-
public void testDexLoggerGeneratesEvents_unknownClassLoader() throws Exception {
- File privateCopyFile = fileForJar("copied2.jar");
+ File privateCopyFile = privateFile("copied2.jar");
String expectedNameHash =
"202158B6A3169D78F1722487205A6B036B3F2F5653FDCFB4E74710611AC7EB93";
- String expectedContentHash = copyAndHashJar(privateCopyFile);
+ String expectedContentHash = copyAndHashResource("/javalib.jar", privateCopyFile);
// This time make sure an unknown class loader is an ancestor of the class loader we use.
ClassLoader knownClassLoader = sContext.getClass().getClassLoader();
@@ -129,22 +135,185 @@
// And make sure we log events about it
long previousEventNanos = mostRecentEventTimeNanos();
- runDexLogger();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
- assertDclLoggedSince(previousEventNanos, expectedNameHash, expectedContentHash);
+ assertDclLoggedSince(previousEventNanos, DCL_DEX_SUBTAG,
+ expectedNameHash, expectedContentHash);
}
- private static File fileForJar(String name) {
- return new File(sContext.getDir("jars", Context.MODE_PRIVATE), name);
+ @Test
+ public void testDexLoggerGeneratesEvents_nativeLibrary() throws Exception {
+ File privateCopyFile = privateFile("copied.so");
+ String expectedNameHash =
+ "996223BAD4B4FE75C57A3DEC61DB9C0B38E0A7AD479FC95F33494F4BC55A0F0E";
+ String expectedContentHash =
+ copyAndHashResource("/DexLoggerNativeTestLibrary.so", privateCopyFile);
+
+ System.load(privateCopyFile.toString());
+
+ // Run the job to scan generated audit log entries
+ runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID);
+
+ // And then make sure we log events about it
+ long previousEventNanos = mostRecentEventTimeNanos();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
+
+ assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG,
+ expectedNameHash, expectedContentHash);
}
- private static String copyAndHashJar(File copyTo) throws Exception {
+ @Test
+ public void testDexLoggerGeneratesEvents_nativeLibrary_escapedName() throws Exception {
+ // A file name with a space will be escaped in the audit log; verify we un-escape it
+ // correctly.
+ File privateCopyFile = privateFile("second copy.so");
+ String expectedNameHash =
+ "8C39990C560B4F36F83E208E279F678746FE23A790E4C50F92686584EA2041CA";
+ String expectedContentHash =
+ copyAndHashResource("/DexLoggerNativeTestLibrary.so", privateCopyFile);
+
+ System.load(privateCopyFile.toString());
+
+ // Run the job to scan generated audit log entries
+ runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID);
+
+ // And then make sure we log events about it
+ long previousEventNanos = mostRecentEventTimeNanos();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
+
+ assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG,
+ expectedNameHash, expectedContentHash);
+ }
+
+ @Test
+ public void testDexLoggerGeneratesEvents_nativeExecutable() throws Exception {
+ File privateCopyFile = privateFile("test_executable");
+ String expectedNameHash =
+ "3FBEC3F925A132D18F347F11AE9A5BB8DE1238828F8B4E064AA86EB68BD46DCF";
+ String expectedContentHash =
+ copyAndHashResource("/DexLoggerNativeExecutable", privateCopyFile);
+ assertThat(privateCopyFile.setExecutable(true)).isTrue();
+
+ Process process = Runtime.getRuntime().exec(privateCopyFile.toString());
+ int exitCode = process.waitFor();
+ assertThat(exitCode).isEqualTo(0);
+
+ // Run the job to scan generated audit log entries
+ runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID);
+
+ // And then make sure we log events about it
+ long previousEventNanos = mostRecentEventTimeNanos();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
+
+ assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG,
+ expectedNameHash, expectedContentHash);
+ }
+
+ @Test
+ public void testDexLoggerGeneratesEvents_spoofed_validFile() throws Exception {
+ File privateCopyFile = privateFile("spoofed");
+
+ String expectedContentHash =
+ copyAndHashResource("/DexLoggerNativeExecutable", privateCopyFile);
+
+ EventLog.writeEvent(EventLog.getTagCode("auditd"),
+ "type=1400 avc: granted { execute_no_trans } "
+ + "path=\"" + privateCopyFile + "\" "
+ + "scontext=u:r:untrusted_app_27: "
+ + "tcontext=u:object_r:app_data_file: "
+ + "tclass=file ");
+
+ String expectedNameHash =
+ "1CF36F503A02877BB775DC23C1C5A47A95F2684B6A1A83B11795B856D88861E3";
+
+ // Run the job to scan generated audit log entries
+ runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID);
+
+ // And then make sure we log events about it
+ long previousEventNanos = mostRecentEventTimeNanos();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
+
+ assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG,
+ expectedNameHash, expectedContentHash);
+ }
+
+ @Test
+ public void testDexLoggerGeneratesEvents_spoofed_pathTraversal() throws Exception {
+ File privateDir = privateFile("x").getParentFile();
+
+ // Transform /a/b/c -> /a/b/c/../../.. so we get back to the root
+ File pathTraversalToRoot = privateDir;
+ File root = new File("/");
+ while (!privateDir.equals(root)) {
+ pathTraversalToRoot = new File(pathTraversalToRoot, "..");
+ privateDir = privateDir.getParentFile();
+ }
+
+ File spoofedFile = new File(pathTraversalToRoot, "dev/urandom");
+
+ assertWithMessage("Expected " + spoofedFile + " to be readable")
+ .that(spoofedFile.canRead()).isTrue();
+
+ EventLog.writeEvent(EventLog.getTagCode("auditd"),
+ "type=1400 avc: granted { execute_no_trans } "
+ + "path=\"" + spoofedFile + "\" "
+ + "scontext=u:r:untrusted_app_27: "
+ + "tcontext=u:object_r:app_data_file: "
+ + "tclass=file ");
+
+ String expectedNameHash =
+ "65528FE876BD676B0DFCC9A8ACA8988E026766F99EEC1E1FB48F46B2F635E225";
+
+ // Run the job to scan generated audit log entries
+ runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID);
+
+ // And then trigger generating DCL events
+ long previousEventNanos = mostRecentEventTimeNanos();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
+
+ assertNoDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, expectedNameHash);
+ }
+
+ @Test
+ public void testDexLoggerGeneratesEvents_spoofed_otherAppFile() throws Exception {
+ File ourPath = sContext.getDatabasePath("android_pay");
+ File targetPath = new File(ourPath.toString()
+ .replace("com.android.frameworks.dexloggertest", "com.google.android.gms"));
+
+ assertWithMessage("Expected " + targetPath + " to not be readable")
+ .that(targetPath.canRead()).isFalse();
+
+ EventLog.writeEvent(EventLog.getTagCode("auditd"),
+ "type=1400 avc: granted { execute_no_trans } "
+ + "path=\"" + targetPath + "\" "
+ + "scontext=u:r:untrusted_app_27: "
+ + "tcontext=u:object_r:app_data_file: "
+ + "tclass=file ");
+
+ String expectedNameHash =
+ "CBE04E8AB9E7199FC19CBAAF9C774B88E56B3B19E823F2251693380AD6F515E6";
+
+ // Run the job to scan generated audit log entries
+ runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID);
+
+ // And then trigger generating DCL events
+ long previousEventNanos = mostRecentEventTimeNanos();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
+
+ assertNoDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, expectedNameHash);
+ }
+
+ private static File privateFile(String name) {
+ return new File(sContext.getDir("dcl", Context.MODE_PRIVATE), name);
+ }
+
+ private static String copyAndHashResource(String resourcePath, File copyTo) throws Exception {
MessageDigest hasher = MessageDigest.getInstance("SHA-256");
// Copy the jar from our Java resources to a private data directory
Class<?> thisClass = DexLoggerIntegrationTests.class;
- try (InputStream input = thisClass.getResourceAsStream("/javalib.jar");
- OutputStream output = new FileOutputStream(copyTo)) {
+ try (InputStream input = thisClass.getResourceAsStream(resourcePath);
+ OutputStream output = new FileOutputStream(copyTo)) {
byte[] buffer = new byte[1024];
while (true) {
int numRead = input.read(buffer);
@@ -166,24 +335,18 @@
return formatter.toString();
}
- private static long mostRecentEventTimeNanos() throws Exception {
- List<EventLog.Event> events = new ArrayList<>();
-
- EventLog.readEvents(TAG_LIST, events);
- return events.isEmpty() ? 0 : events.get(events.size() - 1).getTimeNanos();
- }
-
- private static void runDexLogger() throws Exception {
- // This forces {@code DynamicCodeLoggingService} to start now.
- runCommand("cmd jobscheduler run -f android " + DYNAMIC_CODE_LOGGING_JOB_ID);
+ private static void runDynamicCodeLoggingJob(int jobId) throws Exception {
+ // This forces the DynamicCodeLoggingService job to start now.
+ runCommand("cmd jobscheduler run -f android " + jobId);
// Wait for the job to have run.
long startTime = SystemClock.elapsedRealtime();
while (true) {
String response = runCommand(
- "cmd jobscheduler get-job-state android " + DYNAMIC_CODE_LOGGING_JOB_ID);
+ "cmd jobscheduler get-job-state android " + jobId);
if (!response.contains("pending") && !response.contains("active")) {
break;
}
+ // Don't wait forever - if it's taken > 10s then something is very wrong.
if (SystemClock.elapsedRealtime() - startTime > TimeUnit.SECONDS.toMillis(10)) {
throw new AssertionError("Job has not completed: " + response);
}
@@ -208,37 +371,68 @@
return response.toString("UTF-8");
}
- private static void assertDclLoggedSince(long previousEventNanos, String expectedNameHash,
- String expectedContentHash) throws Exception {
- List<EventLog.Event> events = new ArrayList<>();
- EventLog.readEvents(TAG_LIST, events);
- int found = 0;
- for (EventLog.Event event : events) {
+ private static long mostRecentEventTimeNanos() throws Exception {
+ List<Event> events = readSnetEvents();
+ return events.isEmpty() ? 0 : events.get(events.size() - 1).getTimeNanos();
+ }
+
+ private static void assertDclLoggedSince(long previousEventNanos, String expectedSubTag,
+ String expectedNameHash, String expectedContentHash) throws Exception {
+ List<String> messages =
+ findMatchingEvents(previousEventNanos, expectedSubTag, expectedNameHash);
+
+ assertWithMessage("Expected exactly one matching log entry").that(messages).hasSize(1);
+ assertThat(messages.get(0)).endsWith(expectedContentHash);
+ }
+
+ private static void assertNoDclLoggedSince(long previousEventNanos, String expectedSubTag,
+ String expectedNameHash) throws Exception {
+ List<String> messages =
+ findMatchingEvents(previousEventNanos, expectedSubTag, expectedNameHash);
+
+ assertWithMessage("Expected no matching log entries").that(messages).isEmpty();
+ }
+
+ private static List<String> findMatchingEvents(long previousEventNanos, String expectedSubTag,
+ String expectedNameHash) throws Exception {
+ List<String> messages = new ArrayList<>();
+
+ for (Event event : readSnetEvents()) {
if (event.getTimeNanos() <= previousEventNanos) {
continue;
}
- Object[] data = (Object[]) event.getData();
- // We only care about DCL events that we generated.
- String subTag = (String) data[0];
- if (!DCL_SUBTAG.equals(subTag)) {
+ Object data = event.getData();
+ if (!(data instanceof Object[])) {
continue;
}
- int uid = (int) data[1];
+ Object[] fields = (Object[]) data;
+
+ // We only care about DCL events that we generated.
+ String subTag = (String) fields[0];
+ if (!expectedSubTag.equals(subTag)) {
+ continue;
+ }
+ int uid = (int) fields[1];
if (uid != sMyUid) {
continue;
}
- String message = (String) data[2];
+ String message = (String) fields[2];
if (!message.startsWith(expectedNameHash)) {
continue;
}
- assertThat(message).endsWith(expectedContentHash);
- ++found;
+ messages.add(message);
+ //assertThat(message).endsWith(expectedContentHash);
}
+ return messages;
+ }
- assertThat(found).isEqualTo(1);
+ private static List<Event> readSnetEvents() throws Exception {
+ List<Event> events = new ArrayList<>();
+ EventLog.readEvents(new int[] { SNET_TAG }, events);
+ return events;
}
/**
diff --git a/tests/DexLoggerIntegrationTests/src/cpp/com_android_dcl_Jni.cpp b/tests/DexLoggerIntegrationTests/src/cpp/com_android_dcl_Jni.cpp
new file mode 100644
index 0000000..0608883
--- /dev/null
+++ b/tests/DexLoggerIntegrationTests/src/cpp/com_android_dcl_Jni.cpp
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "jni.h"
+
+extern "C" jint JNI_OnLoad(JavaVM* /* vm */, void* /* reserved */)
+{
+ return JNI_VERSION_1_6;
+}
diff --git a/tests/DexLoggerIntegrationTests/src/cpp/test_executable.cpp b/tests/DexLoggerIntegrationTests/src/cpp/test_executable.cpp
new file mode 100644
index 0000000..ad025e6
--- /dev/null
+++ b/tests/DexLoggerIntegrationTests/src/cpp/test_executable.cpp
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+int main() {
+ // This program just has to run, it doesn't need to do anything. So we don't.
+ return 0;
+}
diff --git a/tests/RollbackTest/src/com/android/tests/rollback/RollbackBroadcastReceiver.java b/tests/RollbackTest/src/com/android/tests/rollback/RollbackBroadcastReceiver.java
index d3c39f0..030641b 100644
--- a/tests/RollbackTest/src/com/android/tests/rollback/RollbackBroadcastReceiver.java
+++ b/tests/RollbackTest/src/com/android/tests/rollback/RollbackBroadcastReceiver.java
@@ -44,7 +44,6 @@
RollbackBroadcastReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_ROLLBACK_EXECUTED);
- filter.addDataScheme("package");
InstrumentationRegistry.getContext().registerReceiver(this, filter);
}
diff --git a/tests/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
index c2e735e..9d67cea 100644
--- a/tests/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
+++ b/tests/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
@@ -22,9 +22,9 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.rollback.PackageRollbackInfo;
import android.content.rollback.RollbackInfo;
import android.content.rollback.RollbackManager;
-import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.test.InstrumentationRegistry;
@@ -38,7 +38,6 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -99,7 +98,7 @@
// so that's not the case!
for (int i = 0; i < 5; ++i) {
for (RollbackInfo info : rm.getRecentlyExecutedRollbacks()) {
- if (TEST_APP_A.equals(info.targetPackage.packageName)) {
+ if (TEST_APP_A.equals(info.targetPackage.getPackageName())) {
Log.i(TAG, "Sleeping 1 second to wait for uninstall to take effect.");
Thread.sleep(1000);
break;
@@ -117,7 +116,7 @@
// There should be no recently executed rollbacks for this package.
for (RollbackInfo info : rm.getRecentlyExecutedRollbacks()) {
- assertNotEquals(TEST_APP_A, info.targetPackage.packageName);
+ assertNotEquals(TEST_APP_A, info.targetPackage.getPackageName());
}
// Install v1 of the app (without rollbacks enabled).
@@ -136,9 +135,7 @@
assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_A));
RollbackInfo rollback = rm.getAvailableRollback(TEST_APP_A);
assertNotNull(rollback);
- assertEquals(TEST_APP_A, rollback.targetPackage.packageName);
- assertEquals(2, rollback.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollback.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollback.targetPackage);
// We should not have received any rollback requests yet.
// TODO: Possibly flaky if, by chance, some other app on device
@@ -154,21 +151,18 @@
// received could lead to test flakiness.
Intent broadcast = broadcastReceiver.poll(5, TimeUnit.SECONDS);
assertNotNull(broadcast);
- assertEquals(TEST_APP_A, broadcast.getData().getSchemeSpecificPart());
assertNull(broadcastReceiver.poll(0, TimeUnit.SECONDS));
// Verify the recent rollback has been recorded.
rollback = null;
for (RollbackInfo r : rm.getRecentlyExecutedRollbacks()) {
- if (TEST_APP_A.equals(r.targetPackage.packageName)) {
+ if (TEST_APP_A.equals(r.targetPackage.getPackageName())) {
assertNull(rollback);
rollback = r;
}
}
assertNotNull(rollback);
- assertEquals(TEST_APP_A, rollback.targetPackage.packageName);
- assertEquals(2, rollback.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollback.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollback.targetPackage);
broadcastReceiver.unregister();
context.unregisterReceiver(enableRollbackReceiver);
@@ -209,16 +203,12 @@
assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_A));
RollbackInfo rollbackA = rm.getAvailableRollback(TEST_APP_A);
assertNotNull(rollbackA);
- assertEquals(TEST_APP_A, rollbackA.targetPackage.packageName);
- assertEquals(2, rollbackA.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollbackA.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollbackA.targetPackage);
assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_B));
RollbackInfo rollbackB = rm.getAvailableRollback(TEST_APP_B);
assertNotNull(rollbackB);
- assertEquals(TEST_APP_B, rollbackB.targetPackage.packageName);
- assertEquals(2, rollbackB.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollbackB.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_B, 2, 1, rollbackB.targetPackage);
// Reload the persisted data.
rm.reloadPersistedData();
@@ -226,16 +216,12 @@
// The apps should still be available for rollback.
rollbackA = rm.getAvailableRollback(TEST_APP_A);
assertNotNull(rollbackA);
- assertEquals(TEST_APP_A, rollbackA.targetPackage.packageName);
- assertEquals(2, rollbackA.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollbackA.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollbackA.targetPackage);
assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_B));
rollbackB = rm.getAvailableRollback(TEST_APP_B);
assertNotNull(rollbackB);
- assertEquals(TEST_APP_B, rollbackB.targetPackage.packageName);
- assertEquals(2, rollbackB.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollbackB.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_B, 2, 1, rollbackB.targetPackage);
// Rollback of B should not rollback A
RollbackTestUtils.rollback(rollbackB);
@@ -279,16 +265,12 @@
assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_A));
RollbackInfo rollbackA = rm.getAvailableRollback(TEST_APP_A);
assertNotNull(rollbackA);
- assertEquals(TEST_APP_A, rollbackA.targetPackage.packageName);
- assertEquals(2, rollbackA.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollbackA.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollbackA.targetPackage);
assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_B));
RollbackInfo rollbackB = rm.getAvailableRollback(TEST_APP_B);
assertNotNull(rollbackB);
- assertEquals(TEST_APP_B, rollbackB.targetPackage.packageName);
- assertEquals(2, rollbackB.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollbackB.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_B, 2, 1, rollbackB.targetPackage);
// Reload the persisted data.
rm.reloadPersistedData();
@@ -296,16 +278,12 @@
// The apps should still be available for rollback.
rollbackA = rm.getAvailableRollback(TEST_APP_A);
assertNotNull(rollbackA);
- assertEquals(TEST_APP_A, rollbackA.targetPackage.packageName);
- assertEquals(2, rollbackA.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollbackA.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollbackA.targetPackage);
assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_B));
rollbackB = rm.getAvailableRollback(TEST_APP_B);
assertNotNull(rollbackB);
- assertEquals(TEST_APP_B, rollbackB.targetPackage.packageName);
- assertEquals(2, rollbackB.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollbackB.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_B, 2, 1, rollbackB.targetPackage);
// Rollback of B should rollback A as well
RollbackTestUtils.rollback(rollbackB);
@@ -349,15 +327,13 @@
// Verify the recent rollback has been recorded.
rollback = null;
for (RollbackInfo r : rm.getRecentlyExecutedRollbacks()) {
- if (TEST_APP_A.equals(r.targetPackage.packageName)) {
+ if (TEST_APP_A.equals(r.targetPackage.getPackageName())) {
assertNull(rollback);
rollback = r;
}
}
assertNotNull(rollback);
- assertEquals(TEST_APP_A, rollback.targetPackage.packageName);
- assertEquals(2, rollback.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollback.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollback.targetPackage);
// Reload the persisted data.
rm.reloadPersistedData();
@@ -365,15 +341,13 @@
// Verify the recent rollback is still recorded.
rollback = null;
for (RollbackInfo r : rm.getRecentlyExecutedRollbacks()) {
- if (TEST_APP_A.equals(r.targetPackage.packageName)) {
+ if (TEST_APP_A.equals(r.targetPackage.getPackageName())) {
assertNull(rollback);
rollback = r;
}
}
assertNotNull(rollback);
- assertEquals(TEST_APP_A, rollback.targetPackage.packageName);
- assertEquals(2, rollback.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollback.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollback.targetPackage);
} finally {
RollbackTestUtils.dropShellPermissionIdentity();
}
@@ -405,9 +379,7 @@
assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_A));
RollbackInfo rollback = rm.getAvailableRollback(TEST_APP_A);
assertNotNull(rollback);
- assertEquals(TEST_APP_A, rollback.targetPackage.packageName);
- assertEquals(2, rollback.targetPackage.higherVersion.versionCode);
- assertEquals(1, rollback.targetPackage.lowerVersion.versionCode);
+ assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollback.targetPackage);
// Expire the rollback.
rm.expireRollbackForPackage(TEST_APP_A);
@@ -470,7 +442,7 @@
* Test that app user data is rolled back.
* TODO: Stop ignoring this test once user data rollback is supported.
*/
- @Ignore @Test
+ @Test
public void testUserDataRollback() throws Exception {
try {
RollbackTestUtils.adoptShellPermissionIdentity(
@@ -479,9 +451,9 @@
Manifest.permission.MANAGE_ROLLBACKS);
RollbackTestUtils.uninstall(TEST_APP_A);
- RollbackTestUtils.install("RollbackTestAppV1.apk", false);
+ RollbackTestUtils.install("RollbackTestAppAv1.apk", false);
processUserData(TEST_APP_A);
- RollbackTestUtils.install("RollbackTestAppV2.apk", true);
+ RollbackTestUtils.install("RollbackTestAppAv2.apk", true);
processUserData(TEST_APP_A);
RollbackManager rm = RollbackTestUtils.getRollbackManager();
@@ -500,8 +472,7 @@
@Test
public void testRollbackBroadcastRestrictions() throws Exception {
RollbackBroadcastReceiver broadcastReceiver = new RollbackBroadcastReceiver();
- Intent broadcast = new Intent(Intent.ACTION_PACKAGE_ROLLBACK_EXECUTED,
- Uri.fromParts("package", "com.android.tests.rollback.bogus", null));
+ Intent broadcast = new Intent(Intent.ACTION_PACKAGE_ROLLBACK_EXECUTED);
try {
InstrumentationRegistry.getContext().sendBroadcast(broadcast);
fail("Succeeded in sending restricted broadcast from app context.");
@@ -550,11 +521,11 @@
Thread.sleep(1000);
RollbackInfo rollbackA = rm.getAvailableRollback(TEST_APP_A);
assertNotNull(rollbackA);
- assertEquals(TEST_APP_A, rollbackA.targetPackage.packageName);
+ assertEquals(TEST_APP_A, rollbackA.targetPackage.getPackageName());
RollbackInfo rollbackB = rm.getAvailableRollback(TEST_APP_B);
assertNotNull(rollbackB);
- assertEquals(TEST_APP_B, rollbackB.targetPackage.packageName);
+ assertEquals(TEST_APP_B, rollbackB.targetPackage.getPackageName());
// Executing rollback should roll back the correct package.
RollbackTestUtils.rollback(rollbackA);
@@ -671,7 +642,7 @@
// We should not see a recent rollback listed for TEST_APP_B
for (RollbackInfo r : rm.getRecentlyExecutedRollbacks()) {
- assertNotEquals(TEST_APP_B, r.targetPackage.packageName);
+ assertNotEquals(TEST_APP_B, r.targetPackage.getPackageName());
}
// TODO: Test the listed dependent apps for the recently executed
@@ -681,4 +652,15 @@
RollbackTestUtils.dropShellPermissionIdentity();
}
}
+
+ // Helper function to test the value of a PackageRollbackInfo
+ private void assertPackageRollbackInfoEquals(String packageName,
+ long versionRolledBackFrom, long versionRolledBackTo,
+ PackageRollbackInfo info) {
+ assertEquals(packageName, info.getPackageName());
+ assertEquals(packageName, info.getVersionRolledBackFrom().getPackageName());
+ assertEquals(versionRolledBackFrom, info.getVersionRolledBackFrom().getLongVersionCode());
+ assertEquals(packageName, info.getVersionRolledBackTo().getPackageName());
+ assertEquals(versionRolledBackTo, info.getVersionRolledBackTo().getLongVersionCode());
+ }
}
diff --git a/tests/net/java/android/net/DnsPacketTest.java b/tests/net/java/android/net/DnsPacketTest.java
new file mode 100644
index 0000000..032e526
--- /dev/null
+++ b/tests/net/java/android/net/DnsPacketTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DnsPacketTest {
+ private void assertHeaderParses(DnsPacket.DnsHeader header, int id, int flag,
+ int qCount, int aCount, int nsCount, int arCount) {
+ assertEquals(header.id, id);
+ assertEquals(header.flags, flag);
+ assertEquals(header.getSectionCount(DnsPacket.QDSECTION), qCount);
+ assertEquals(header.getSectionCount(DnsPacket.ANSECTION), aCount);
+ assertEquals(header.getSectionCount(DnsPacket.NSSECTION), nsCount);
+ assertEquals(header.getSectionCount(DnsPacket.ARSECTION), arCount);
+ }
+
+ private void assertSectionParses(DnsPacket.DnsSection section, String dname,
+ int dtype, int dclass, int ttl, byte[] rr) {
+ assertEquals(section.dName, dname);
+ assertEquals(section.nsType, dtype);
+ assertEquals(section.nsClass, dclass);
+ assertEquals(section.ttl, ttl);
+ assertTrue(Arrays.equals(section.getRR(), rr));
+ }
+
+ class TestDnsPacket extends DnsPacket {
+ TestDnsPacket(byte[] data) throws ParseException {
+ super(data);
+ }
+
+ public DnsHeader getHeader() {
+ return mHeader;
+ }
+ public List<DnsSection> getSectionList(int secType) {
+ return mSections[secType];
+ }
+ }
+
+ @Test
+ public void testNullDisallowed() {
+ try {
+ new TestDnsPacket(null);
+ fail("Exception not thrown for null byte array");
+ } catch (DnsPacket.ParseException e) {
+ }
+ }
+
+ @Test
+ public void testV4Answer() throws Exception {
+ final byte[] v4blob = new byte[] {
+ /* Header */
+ 0x55, 0x66, /* Transaction ID */
+ (byte) 0x81, (byte) 0x80, /* Flags */
+ 0x00, 0x01, /* Questions */
+ 0x00, 0x01, /* Answer RRs */
+ 0x00, 0x00, /* Authority RRs */
+ 0x00, 0x00, /* Additional RRs */
+ /* Queries */
+ 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01, /* Class */
+ /* Answers */
+ (byte) 0xc0, 0x0c, /* Name */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01, /* Class */
+ 0x00, 0x00, 0x01, 0x2b, /* TTL */
+ 0x00, 0x04, /* Data length */
+ (byte) 0xac, (byte) 0xd9, (byte) 0xa1, (byte) 0x84 /* Address */
+ };
+ TestDnsPacket packet = new TestDnsPacket(v4blob);
+
+ // Header part
+ assertHeaderParses(packet.getHeader(), 0x5566, 0x8180, 1, 1, 0, 0);
+
+ // Section part
+ List<DnsPacket.DnsSection> qdSectionList =
+ packet.getSectionList(DnsPacket.QDSECTION);
+ assertEquals(qdSectionList.size(), 1);
+ assertSectionParses(qdSectionList.get(0), "www.google.com", 1, 1, 0, null);
+
+ List<DnsPacket.DnsSection> anSectionList =
+ packet.getSectionList(DnsPacket.ANSECTION);
+ assertEquals(anSectionList.size(), 1);
+ assertSectionParses(anSectionList.get(0), "www.google.com", 1, 1, 0x12b,
+ new byte[]{ (byte) 0xac, (byte) 0xd9, (byte) 0xa1, (byte) 0x84 });
+ }
+
+ @Test
+ public void testV6Answer() throws Exception {
+ final byte[] v6blob = new byte[] {
+ /* Header */
+ 0x77, 0x22, /* Transaction ID */
+ (byte) 0x81, (byte) 0x80, /* Flags */
+ 0x00, 0x01, /* Questions */
+ 0x00, 0x01, /* Answer RRs */
+ 0x00, 0x00, /* Authority RRs */
+ 0x00, 0x00, /* Additional RRs */
+ /* Queries */
+ 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+ 0x00, 0x1c, /* Type */
+ 0x00, 0x01, /* Class */
+ /* Answers */
+ (byte) 0xc0, 0x0c, /* Name */
+ 0x00, 0x1c, /* Type */
+ 0x00, 0x01, /* Class */
+ 0x00, 0x00, 0x00, 0x37, /* TTL */
+ 0x00, 0x10, /* Data length */
+ 0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x04 /* Address */
+ };
+ TestDnsPacket packet = new TestDnsPacket(v6blob);
+
+ // Header part
+ assertHeaderParses(packet.getHeader(), 0x7722, 0x8180, 1, 1, 0, 0);
+
+ // Section part
+ List<DnsPacket.DnsSection> qdSectionList =
+ packet.getSectionList(DnsPacket.QDSECTION);
+ assertEquals(qdSectionList.size(), 1);
+ assertSectionParses(qdSectionList.get(0), "www.google.com", 28, 1, 0, null);
+
+ List<DnsPacket.DnsSection> anSectionList =
+ packet.getSectionList(DnsPacket.ANSECTION);
+ assertEquals(anSectionList.size(), 1);
+ assertSectionParses(anSectionList.get(0), "www.google.com", 28, 1, 0x37,
+ new byte[]{ 0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x04 });
+ }
+}
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 1c26418..dda4481 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -1508,6 +1508,12 @@
verifyActiveNetwork(TRANSPORT_WIFI);
}
+ @Test
+ public void testRequiresValidation() {
+ assertTrue(NetworkMonitorUtils.isValidationRequired(
+ mCm.getDefaultRequest().networkCapabilities));
+ }
+
enum CallbackState {
NONE,
AVAILABLE,
@@ -4404,8 +4410,7 @@
mMockVpn.setUids(ranges);
// VPN networks do not satisfy the default request and are automatically validated
// by NetworkMonitor
- assertFalse(NetworkMonitorUtils.isValidationRequired(
- mCm.getDefaultRequest().networkCapabilities, vpnNetworkAgent.mNetworkCapabilities));
+ assertFalse(NetworkMonitorUtils.isValidationRequired(vpnNetworkAgent.mNetworkCapabilities));
vpnNetworkAgent.setNetworkValid();
vpnNetworkAgent.connect(false);
diff --git a/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java b/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java
index c748d0f..f2ecef9 100644
--- a/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java
+++ b/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java
@@ -27,6 +27,7 @@
import android.content.Context;
import android.net.ipmemorystore.Blob;
import android.net.ipmemorystore.IOnBlobRetrievedListener;
+import android.net.ipmemorystore.IOnL2KeyResponseListener;
import android.net.ipmemorystore.IOnNetworkAttributesRetrieved;
import android.net.ipmemorystore.IOnSameNetworkResponseListener;
import android.net.ipmemorystore.IOnStatusListener;
@@ -67,7 +68,14 @@
private static final String TEST_CLIENT_ID = "testClientId";
private static final String TEST_DATA_NAME = "testData";
- private static final String[] FAKE_KEYS = { "fakeKey1", "fakeKey2", "fakeKey3", "fakeKey4" };
+ private static final int FAKE_KEY_COUNT = 20;
+ private static final String[] FAKE_KEYS;
+ static {
+ FAKE_KEYS = new String[FAKE_KEY_COUNT];
+ for (int i = 0; i < FAKE_KEYS.length; ++i) {
+ FAKE_KEYS[i] = "fakeKey" + i;
+ }
+ }
@Mock
private Context mMockContext;
@@ -170,6 +178,25 @@
};
}
+ /** Helper method to make an IOnL2KeyResponseListener */
+ private interface OnL2KeyResponseListener {
+ void onL2KeyResponse(Status status, String key);
+ }
+ private IOnL2KeyResponseListener onL2KeyResponse(final OnL2KeyResponseListener functor) {
+ return new IOnL2KeyResponseListener() {
+ @Override
+ public void onL2KeyResponse(final StatusParcelable status, final String key)
+ throws RemoteException {
+ functor.onL2KeyResponse(new Status(status), key);
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+ };
+ }
+
// Helper method to factorize some boilerplate
private void doLatched(final String timeoutMessage, final Consumer<CountDownLatch> functor) {
final CountDownLatch latch = new CountDownLatch(1);
@@ -195,12 +222,9 @@
}
@Test
- public void testNetworkAttributes() {
+ public void testNetworkAttributes() throws UnknownHostException {
final NetworkAttributes.Builder na = new NetworkAttributes.Builder();
- try {
- na.setAssignedV4Address(
- (Inet4Address) Inet4Address.getByAddress(new byte[]{1, 2, 3, 4}));
- } catch (UnknownHostException e) { /* Can't happen */ }
+ na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4"));
na.setGroupHint("hint1");
na.setMtu(219);
final String l2Key = FAKE_KEYS[0];
@@ -218,10 +242,8 @@
})));
final NetworkAttributes.Builder na2 = new NetworkAttributes.Builder();
- try {
- na.setDnsAddresses(Arrays.asList(
- new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")}));
- } catch (UnknownHostException e) { /* Still can't happen */ }
+ na.setDnsAddresses(Arrays.asList(
+ new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")}));
final NetworkAttributes attributes2 = na2.build();
storeAttributes("Did not complete storing attributes 2", l2Key, attributes2);
@@ -333,8 +355,93 @@
}
@Test
- public void testFindL2Key() {
- // TODO : implement this
+ public void testFindL2Key() throws UnknownHostException {
+ final NetworkAttributes.Builder na = new NetworkAttributes.Builder();
+ na.setGroupHint("hint0");
+ storeAttributes(FAKE_KEYS[0], na.build());
+
+ na.setDnsAddresses(Arrays.asList(
+ new InetAddress[] {Inet6Address.getByName("8D56:9AF1::08EE:20F1")}));
+ na.setMtu(219);
+ storeAttributes(FAKE_KEYS[1], na.build());
+ na.setMtu(null);
+ na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4"));
+ na.setDnsAddresses(Arrays.asList(
+ new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")}));
+ na.setGroupHint("hint1");
+ storeAttributes(FAKE_KEYS[2], na.build());
+ na.setMtu(219);
+ storeAttributes(FAKE_KEYS[3], na.build());
+ na.setMtu(240);
+ storeAttributes(FAKE_KEYS[4], na.build());
+ na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("5.6.7.8"));
+ storeAttributes(FAKE_KEYS[5], na.build());
+
+ // Matches key 5 exactly
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertEquals(FAKE_KEYS[5], key);
+ })));
+
+ // MTU matches key 4 but v4 address matches key 5. The latter is stronger.
+ na.setMtu(240);
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertEquals(FAKE_KEYS[5], key);
+ })));
+
+ // Closest to key 3 (indeed, identical)
+ na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4"));
+ na.setMtu(219);
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertEquals(FAKE_KEYS[3], key);
+ })));
+
+ // Group hint alone must not be strong enough to override the rest
+ na.setGroupHint("hint0");
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertEquals(FAKE_KEYS[3], key);
+ })));
+
+ // Still closest to key 3, though confidence is lower
+ na.setGroupHint("hint1");
+ na.setDnsAddresses(null);
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertEquals(FAKE_KEYS[3], key);
+ })));
+
+ // But changing the MTU makes this closer to key 4
+ na.setMtu(240);
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertEquals(FAKE_KEYS[4], key);
+ })));
+
+ // MTU alone not strong enough to make this group-close
+ na.setGroupHint(null);
+ na.setDnsAddresses(null);
+ na.setAssignedV4Address(null);
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertNull(key);
+ })));
}
private void assertNetworksSameness(final String key1, final String key2, final int sameness) {
@@ -349,7 +456,7 @@
@Test
public void testIsSameNetwork() throws UnknownHostException {
final NetworkAttributes.Builder na = new NetworkAttributes.Builder();
- na.setAssignedV4Address((Inet4Address) Inet4Address.getByAddress(new byte[]{1, 2, 3, 4}));
+ na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4"));
na.setGroupHint("hint1");
na.setMtu(219);
na.setDnsAddresses(Arrays.asList(Inet6Address.getByName("0A1C:2E40:480A::1CA6")));
diff --git a/tests/utils/testutils/java/com/android/test/filters/SelectTest.java b/tests/utils/testutils/java/com/android/test/filters/SelectTest.java
new file mode 100644
index 0000000..d0350af
--- /dev/null
+++ b/tests/utils/testutils/java/com/android/test/filters/SelectTest.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.filters;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringJoiner;
+
+/**
+ * JUnit filter to select tests.
+ *
+ * <p>This filter selects tests specified by package name, class name, and method name. With this
+ * filter, the package and the class options of AndroidJUnitRunner can be superseded. Also the
+ * restriction that prevents using the package and the class options can be mitigated.
+ *
+ * <p><b>Select out tests from Java packages:</b> this option supersedes {@code -e package} option.
+ * <pre>
+ * adb shell am instrument -w \
+ * -e filter com.android.test.filters.SelectTest \
+ * -e selectTest package1.,package2. \
+ * com.tests.pkg/androidx.test.runner.AndroidJUnitRunner
+ * </pre>
+ * Note that the ending {@code .} in package name is mandatory.
+ *
+ * <p><b>Select out test classes:</b> this option supersedes {@code -e class} option.
+ * <pre>
+ * adb shell am instrument -w \
+ * -e filter com.android.test.filters.SelectTest \
+ * -e selectTest package1.ClassA,package2.ClassB \
+ * com.tests.pkg/androidx.test.runner.AndroidJUnitRunner
+ * </pre>
+ *
+ * <p><b>Select out test methods from Java classes:</b>
+ * <pre>
+ * adb shell am instrument -w \
+ * -e filter com.android.test.filters.SelectTest \
+ * -e selectTest package1.ClassA#methodX,package2.ClassB#methodY \
+ * com.tests.pkg/androidx.test.runner.AndroidJUnitRunner
+ * </pre>
+ *
+ * Those options can be used simultaneously. For example
+ * <pre>
+ * adb shell am instrument -w \
+ * -e filter com.android.test.filters.SelectTest \
+ * -e selectTest package1.,package2.classA,package3.ClassB#methodZ \
+ * com.tests.pkg/androidx.test.runner.AndroidJUnitRunner
+ * </pre>
+ * will select out all tests in package1, all tests in classA, and ClassB#methodZ test.
+ *
+ * <p>Note that when this option is specified with either {@code -e package} or {@code -e class}
+ * option, filtering behaves as logically conjunction. Other options, such as {@code -e notPackage},
+ * {@code -e notClass}, {@code -e annotation}, and {@code -e notAnnotation}, should work as expected
+ * with this SelectTest option.
+ *
+ * <p>When specified with {@code -e selectTest_verbose true} option, {@link SelectTest} verbosely
+ * logs to logcat while parsing {@code -e selectTest} option.
+ */
+public class SelectTest extends Filter {
+
+ private static final String TAG = SelectTest.class.getSimpleName();
+
+ @VisibleForTesting
+ static final String OPTION_SELECT_TEST = "selectTest";
+ @VisibleForTesting
+ static final String OPTION_SELECT_TEST_VERBOSE = OPTION_SELECT_TEST + "_verbose";
+
+ private static final String ARGUMENT_ITEM_SEPARATOR = ",";
+ private static final String PACKAGE_NAME_SEPARATOR = ".";
+ private static final String METHOD_SEPARATOR = "#";
+
+ @Nullable
+ private final PackageSet mPackageSet;
+
+ /**
+ * Construct {@link SelectTest} filter from instrumentation arguments in {@link Bundle}.
+ *
+ * @param testArgs instrumentation test arguments.
+ */
+ public SelectTest(@NonNull Bundle testArgs) {
+ mPackageSet = parseSelectTest(testArgs);
+ }
+
+ @Override
+ public boolean shouldRun(Description description) {
+ if (mPackageSet == null) {
+ // Accept all tests because this filter is disabled.
+ return true;
+ }
+ String testClassName = description.getClassName();
+ String testMethodName = description.getMethodName();
+ return mPackageSet.accept(testClassName, testMethodName);
+ }
+
+ @Override
+ public String describe() {
+ return OPTION_SELECT_TEST + "=" + mPackageSet;
+ }
+
+ /**
+ * Create {@link #OPTION_SELECT_TEST} argument and add it to {@code testArgs}.
+ *
+ * <p>This method is intended to be used at constructor of extended {@link Filter} class.
+ *
+ * @param testArgs instrumentation test arguments.
+ * @param selectTests array of class name to be selected to run.
+ * @return modified instrumentation test arguments.
+ */
+ @NonNull
+ protected static Bundle addSelectTest(
+ @NonNull Bundle testArgs, @NonNull String... selectTests) {
+ if (selectTests.length == 0) {
+ return testArgs;
+ }
+ testArgs.putString(OPTION_SELECT_TEST, join(Arrays.asList(selectTests)));
+ return testArgs;
+ }
+
+ /**
+ * Parse {@code -e selectTest} argument.
+ * @param testArgs instrumentation test arguments.
+ * @return {@link PackageSet} that will filter tests. Returns {@code null} when no
+ * {@code -e selectTest} option is specified, thus this filter gets disabled.
+ */
+ @Nullable
+ private static PackageSet parseSelectTest(Bundle testArgs) {
+ final String selectTestArgs = testArgs.getString(OPTION_SELECT_TEST);
+ if (selectTestArgs == null) {
+ Log.w(TAG, "Disabled because no " + OPTION_SELECT_TEST + " option specified");
+ return null;
+ }
+
+ final boolean verbose = new Boolean(testArgs.getString(OPTION_SELECT_TEST_VERBOSE));
+ final PackageSet packageSet = new PackageSet(verbose);
+ for (String selectTestArg : selectTestArgs.split(ARGUMENT_ITEM_SEPARATOR)) {
+ packageSet.add(selectTestArg);
+ }
+ return packageSet;
+ }
+
+ private static String getPackageName(String selectTestArg) {
+ int endPackagePos = selectTestArg.lastIndexOf(PACKAGE_NAME_SEPARATOR);
+ return (endPackagePos < 0) ? "" : selectTestArg.substring(0, endPackagePos);
+ }
+
+ @Nullable
+ private static String getClassName(String selectTestArg) {
+ if (selectTestArg.endsWith(PACKAGE_NAME_SEPARATOR)) {
+ return null;
+ }
+ int methodSepPos = selectTestArg.indexOf(METHOD_SEPARATOR);
+ return (methodSepPos < 0) ? selectTestArg : selectTestArg.substring(0, methodSepPos);
+ }
+
+ @Nullable
+ private static String getMethodName(String selectTestArg) {
+ int methodSepPos = selectTestArg.indexOf(METHOD_SEPARATOR);
+ return (methodSepPos < 0) ? null : selectTestArg.substring(methodSepPos + 1);
+ }
+
+ /** Package level filter */
+ private static class PackageSet {
+ private final boolean mVerbose;
+ /**
+ * Java package name to {@link ClassSet} map. To represent package filtering, a map value
+ * can be {@code null}.
+ */
+ private final Map<String, ClassSet> mClassSetMap = new LinkedHashMap<>();
+
+ PackageSet(boolean verbose) {
+ mVerbose = verbose;
+ }
+
+ void add(final String selectTestArg) {
+ final String packageName = getPackageName(selectTestArg);
+ final String className = getClassName(selectTestArg);
+
+ if (className == null) {
+ ClassSet classSet = mClassSetMap.put(packageName, null); // package filtering.
+ if (mVerbose) {
+ logging("Select package " + selectTestArg, classSet != null,
+ "; supersede " + classSet);
+ }
+ return;
+ }
+
+ ClassSet classSet = mClassSetMap.get(packageName);
+ if (classSet == null) {
+ if (mClassSetMap.containsKey(packageName)) {
+ if (mVerbose) {
+ logging("Select package " + packageName + PACKAGE_NAME_SEPARATOR, true,
+ " ignore " + selectTestArg);
+ }
+ return;
+ }
+ classSet = new ClassSet(mVerbose);
+ mClassSetMap.put(packageName, classSet);
+ }
+ classSet.add(selectTestArg);
+ }
+
+ boolean accept(String className, @Nullable String methodName) {
+ String packageName = getPackageName(className);
+ if (!mClassSetMap.containsKey(packageName)) {
+ return false;
+ }
+ ClassSet classSet = mClassSetMap.get(packageName);
+ return classSet == null || classSet.accept(className, methodName);
+ }
+
+ @Override
+ public String toString() {
+ StringJoiner joiner = new StringJoiner(ARGUMENT_ITEM_SEPARATOR);
+ for (String packageName : mClassSetMap.keySet()) {
+ ClassSet classSet = mClassSetMap.get(packageName);
+ joiner.add(classSet == null
+ ? packageName + PACKAGE_NAME_SEPARATOR : classSet.toString());
+ }
+ return joiner.toString();
+ }
+ }
+
+ /** Class level filter */
+ private static class ClassSet {
+ private final boolean mVerbose;
+ /**
+ * Java class name to set of method names map. To represent class filtering, a map value
+ * can be {@code null}.
+ */
+ private final Map<String, Set<String>> mMethodSetMap = new LinkedHashMap<>();
+
+ ClassSet(boolean verbose) {
+ mVerbose = verbose;
+ }
+
+ void add(String selectTestArg) {
+ final String className = getClassName(selectTestArg);
+ final String methodName = getMethodName(selectTestArg);
+
+ if (methodName == null) {
+ Set<String> methodSet = mMethodSetMap.put(className, null); // class filtering.
+ if (mVerbose) {
+ logging("Select class " + selectTestArg, methodSet != null,
+ "; supersede " + toString(className, methodSet));
+ }
+ return;
+ }
+
+ Set<String> methodSet = mMethodSetMap.get(className);
+ if (methodSet == null) {
+ if (mMethodSetMap.containsKey(className)) {
+ if (mVerbose) {
+ logging("Select class " + className, true, "; ignore " + selectTestArg);
+ }
+ return;
+ }
+ methodSet = new LinkedHashSet<>();
+ mMethodSetMap.put(className, methodSet);
+ }
+
+ methodSet.add(methodName);
+ if (mVerbose) {
+ logging("Select method " + selectTestArg, false, null);
+ }
+ }
+
+ boolean accept(String className, @Nullable String methodName) {
+ if (!mMethodSetMap.containsKey(className)) {
+ return false;
+ }
+ Set<String> methodSet = mMethodSetMap.get(className);
+ return methodName == null || methodSet == null || methodSet.contains(methodName);
+ }
+
+ @Override
+ public String toString() {
+ StringJoiner joiner = new StringJoiner(ARGUMENT_ITEM_SEPARATOR);
+ for (String className : mMethodSetMap.keySet()) {
+ joiner.add(toString(className, mMethodSetMap.get(className)));
+ }
+ return joiner.toString();
+ }
+
+ private static String toString(String className, @Nullable Set<String> methodSet) {
+ if (methodSet == null) {
+ return className;
+ }
+ StringJoiner joiner = new StringJoiner(ARGUMENT_ITEM_SEPARATOR);
+ for (String methodName : methodSet) {
+ joiner.add(className + METHOD_SEPARATOR + methodName);
+ }
+ return joiner.toString();
+ }
+ }
+
+ private static void logging(String infoLog, boolean isWarning, String warningLog) {
+ if (isWarning) {
+ Log.w(TAG, infoLog + warningLog);
+ } else {
+ Log.i(TAG, infoLog);
+ }
+ }
+
+ private static String join(Collection<String> list) {
+ StringJoiner joiner = new StringJoiner(ARGUMENT_ITEM_SEPARATOR);
+ for (String text : list) {
+ joiner.add(text);
+ }
+ return joiner.toString();
+ }
+}
diff --git a/tests/utils/testutils/java/com/android/test/filters/SelectTestTests.java b/tests/utils/testutils/java/com/android/test/filters/SelectTestTests.java
new file mode 100644
index 0000000..a6b0102
--- /dev/null
+++ b/tests/utils/testutils/java/com/android/test/filters/SelectTestTests.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.filters;
+
+import static com.android.test.filters.SelectTest.OPTION_SELECT_TEST;
+import static com.android.test.filters.SelectTest.OPTION_SELECT_TEST_VERBOSE;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Bundle;
+import android.util.ArraySet;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.StringJoiner;
+
+public class SelectTestTests {
+
+ private static final String PACKAGE_A = "packageA.";
+ private static final String PACKAGE_B = "packageB.";
+ private static final String PACKAGE_C = "packageC.";
+ private static final String CLASS_A1 = PACKAGE_A + "Class1";
+ private static final String CLASS_A2 = PACKAGE_A + "Class2";
+ private static final String CLASS_B3 = PACKAGE_B + "Class3";
+ private static final String CLASS_B4 = PACKAGE_B + "Class4";
+ private static final String CLASS_C5 = PACKAGE_C + "Class5";
+ private static final String CLASS_C6 = PACKAGE_C + "Class6";
+ private static final String METHOD_A1K = CLASS_A1 + "#methodK";
+ private static final String METHOD_A1L = CLASS_A1 + "#methodL";
+ private static final String METHOD_A2M = CLASS_A2 + "#methodM";
+ private static final String METHOD_A2N = CLASS_A2 + "#methodN";
+ private static final String METHOD_B3P = CLASS_B3 + "#methodP";
+ private static final String METHOD_B3Q = CLASS_B3 + "#methodQ";
+ private static final String METHOD_B4R = CLASS_B4 + "#methodR";
+ private static final String METHOD_B4S = CLASS_B4 + "#methodS";
+ private static final String METHOD_C5W = CLASS_C5 + "#methodW";
+ private static final String METHOD_C5X = CLASS_C5 + "#methodX";
+ private static final String METHOD_C6Y = CLASS_C6 + "#methodY";
+ private static final String METHOD_C6Z = CLASS_C6 + "#methodZ";
+
+ private static final Set<Description> TEST_METHOD_A1K = methodTest(METHOD_A1K);
+ private static final Set<Description> TEST_METHOD_A1L = methodTest(METHOD_A1L);
+ private static final Set<Description> TEST_METHOD_A2M = methodTest(METHOD_A2M);
+ private static final Set<Description> TEST_METHOD_A2N = methodTest(METHOD_A2N);
+ private static final Set<Description> TEST_METHOD_B3P = methodTest(METHOD_B3P);
+ private static final Set<Description> TEST_METHOD_B3Q = methodTest(METHOD_B3Q);
+ private static final Set<Description> TEST_METHOD_B4R = methodTest(METHOD_B4R);
+ private static final Set<Description> TEST_METHOD_B4S = methodTest(METHOD_B4S);
+ private static final Set<Description> TEST_METHOD_C5W = methodTest(METHOD_C5W);
+ private static final Set<Description> TEST_METHOD_C5X = methodTest(METHOD_C5X);
+ private static final Set<Description> TEST_METHOD_C6Y = methodTest(METHOD_C6Y);
+ private static final Set<Description> TEST_METHOD_C6Z = methodTest(METHOD_C6Z);
+ private static final Set<Description> TEST_CLASS_A1 = merge(TEST_METHOD_A1K, TEST_METHOD_A1L);
+ private static final Set<Description> TEST_CLASS_A2 = merge(TEST_METHOD_A2M, TEST_METHOD_A2N);
+ private static final Set<Description> TEST_CLASS_B3 = merge(TEST_METHOD_B3P, TEST_METHOD_B3Q);
+ private static final Set<Description> TEST_CLASS_B4 = merge(TEST_METHOD_B4R, TEST_METHOD_B4S);
+ private static final Set<Description> TEST_CLASS_C5 = merge(TEST_METHOD_C5W, TEST_METHOD_C5X);
+ private static final Set<Description> TEST_CLASS_C6 = merge(TEST_METHOD_C6Y, TEST_METHOD_C6Z);
+ private static final Set<Description> TEST_PACKAGE_A = merge(TEST_CLASS_A1, TEST_CLASS_A2);
+ private static final Set<Description> TEST_PACKAGE_B = merge(TEST_CLASS_B3, TEST_CLASS_B4);
+ private static final Set<Description> TEST_PACKAGE_C = merge(TEST_CLASS_C5, TEST_CLASS_C6);
+ private static final Set<Description> TEST_ALL =
+ merge(TEST_PACKAGE_A, TEST_PACKAGE_B, TEST_PACKAGE_C);
+
+ private SelectTestBuilder mBuilder;
+
+ @Before
+ public void setUp() {
+ mBuilder = new SelectTestBuilder();
+ }
+
+ private static class SelectTestBuilder {
+ private final Bundle mTestArgs = new Bundle();
+
+ Filter build() {
+ mTestArgs.putString(OPTION_SELECT_TEST_VERBOSE, Boolean.TRUE.toString());
+ return new SelectTest(mTestArgs);
+ }
+
+ SelectTestBuilder withSelectTest(String... selectTestArgs) {
+ putTestOption(OPTION_SELECT_TEST, selectTestArgs);
+ return this;
+ }
+
+ private void putTestOption(String option, String... args) {
+ if (args.length > 0) {
+ StringJoiner joiner = new StringJoiner(",");
+ for (String arg : args) {
+ joiner.add(arg);
+ }
+ mTestArgs.putString(option, joiner.toString());
+ }
+ }
+ }
+
+ private static Set<Description> methodTest(String testName) {
+ int methodSep = testName.indexOf("#");
+ String className = testName.substring(0, methodSep);
+ String methodName = testName.substring(methodSep + 1);
+ final Set<Description> tests = new ArraySet<>();
+ tests.add(Description.createSuiteDescription(className));
+ tests.add(Description.createTestDescription(className, methodName));
+ return Collections.unmodifiableSet(tests);
+ }
+
+ @SafeVarargs
+ private static Set<Description> merge(Set<Description>... testSpecs) {
+ final Set<Description> merged = new LinkedHashSet<>();
+ for (Set<Description> testSet : testSpecs) {
+ merged.addAll(testSet);
+ }
+ return Collections.unmodifiableSet(merged);
+ }
+
+ @SafeVarargs
+ private static void acceptTests(Filter filter, Set<Description>... testSpecs) {
+ final Set<Description> accepts = merge(testSpecs);
+ for (Description test : TEST_ALL) {
+ if (accepts.contains(test)) {
+ assertTrue("accept " + test, filter.shouldRun(test));
+ } else {
+ assertFalse("reject " + test, filter.shouldRun(test));
+ }
+ }
+ }
+
+ @Test
+ public void testFilterDisabled() {
+ final Filter filter = mBuilder.build();
+ acceptTests(filter, TEST_ALL);
+ }
+
+ @Test
+ public void testSelectPackage() {
+ final Filter filter = mBuilder.withSelectTest(PACKAGE_A, PACKAGE_B).build();
+ acceptTests(filter, TEST_PACKAGE_A, TEST_PACKAGE_B);
+ }
+
+ @Test
+ public void testSelectClass() {
+ final Filter filter = mBuilder.withSelectTest(CLASS_A1, CLASS_A2, CLASS_B3).build();
+ acceptTests(filter, TEST_CLASS_A1, TEST_CLASS_A2, TEST_CLASS_B3);
+ }
+
+ @Test
+ public void testSelectMethod() {
+ final Filter filter = mBuilder
+ .withSelectTest(METHOD_A1K, METHOD_A2M, METHOD_A2N, METHOD_B3P).build();
+ acceptTests(filter, TEST_METHOD_A1K, TEST_METHOD_A2M, TEST_METHOD_A2N, TEST_METHOD_B3P);
+ }
+
+ @Test
+ public void testSelectClassAndPackage() {
+ final Filter filter = mBuilder.withSelectTest(CLASS_A1, PACKAGE_B, CLASS_C5).build();
+ acceptTests(filter, TEST_CLASS_A1, TEST_PACKAGE_B, TEST_CLASS_C5);
+ }
+
+ @Test
+ public void testSelectMethodAndPackage() {
+ final Filter filter = mBuilder.withSelectTest(METHOD_A1K, PACKAGE_B, METHOD_C5W).build();
+ acceptTests(filter, TEST_METHOD_A1K, TEST_PACKAGE_B, TEST_METHOD_C5W);
+ }
+
+ @Test
+ public void testSelectMethodAndClass() {
+ final Filter filter = mBuilder.withSelectTest(METHOD_A1K, CLASS_C5, METHOD_B3P).build();
+ acceptTests(filter, TEST_METHOD_A1K, TEST_CLASS_C5, TEST_METHOD_B3P);
+ }
+
+ @Test
+ public void testSelectClassAndSamePackage() {
+ final Filter filter = mBuilder.withSelectTest(CLASS_A1, CLASS_A2, PACKAGE_A,
+ CLASS_C5, CLASS_C6, PACKAGE_C).build();
+ acceptTests(filter, TEST_PACKAGE_A, TEST_PACKAGE_C);
+ }
+
+ @Test
+ public void testSelectMethodAndSameClass() {
+ final Filter filter = mBuilder.withSelectTest(METHOD_A1K, METHOD_A1L, METHOD_A2M, CLASS_A1,
+ CLASS_B3, METHOD_B3P, METHOD_B3Q, METHOD_B4R).build();
+ acceptTests(filter, TEST_CLASS_A1, TEST_METHOD_A2M, TEST_CLASS_B3, TEST_METHOD_B4R);
+ }
+
+ @Test
+ public void testSelectMethodAndSamePackage() {
+ final Filter filter = mBuilder.withSelectTest(METHOD_A1K, METHOD_A1L, METHOD_A2M, PACKAGE_A,
+ PACKAGE_C, METHOD_C5W, METHOD_C5X, METHOD_C6Y).build();
+ acceptTests(filter, TEST_PACKAGE_A, TEST_PACKAGE_C);
+ }
+
+ @Test
+ public void testSelectMethodAndClassAndPackage() {
+ final Filter filter = mBuilder.withSelectTest(
+ METHOD_A1K, CLASS_A1, METHOD_A1L, METHOD_A2M, PACKAGE_A,
+ PACKAGE_B, METHOD_B3Q, CLASS_B3, METHOD_B4R, METHOD_B3P).build();
+ acceptTests(filter, TEST_PACKAGE_A, TEST_PACKAGE_B);
+ }
+}