Merge "hide the nanos even harder" into nyc-dev
diff --git a/Android.mk b/Android.mk
index 9183771..97dfc1d 100644
--- a/Android.mk
+++ b/Android.mk
@@ -223,6 +223,8 @@
core/java/android/os/IPermissionController.aidl \
core/java/android/os/IProcessInfoService.aidl \
core/java/android/os/IPowerManager.aidl \
+ core/java/android/os/IRecoverySystem.aidl \
+ core/java/android/os/IRecoverySystemProgressListener.aidl \
core/java/android/os/IRemoteCallback.aidl \
core/java/android/os/ISchedulingPolicyService.aidl \
core/java/android/os/IUpdateLock.aidl \
diff --git a/api/current.txt b/api/current.txt
index abc9e9a..66c1d39 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -423,6 +423,7 @@
field public static final int controlX2 = 16843774; // 0x10103fe
field public static final int controlY1 = 16843773; // 0x10103fd
field public static final int controlY2 = 16843775; // 0x10103ff
+ field public static final int countDown = 16844060; // 0x101051c
field public static final int country = 16843962; // 0x10104ba
field public static final int cropToPadding = 16843043; // 0x1010123
field public static final int cursorVisible = 16843090; // 0x1010152
@@ -5829,9 +5830,7 @@
method public void clearPackagePersistentPreferredActivities(android.content.ComponentName, java.lang.String);
method public void clearProfileOwner(android.content.ComponentName);
method public void clearUserRestriction(android.content.ComponentName, java.lang.String);
- method public deprecated android.os.UserHandle createAndInitializeUser(android.content.ComponentName, java.lang.String, java.lang.String, android.content.ComponentName, android.os.Bundle);
method public android.os.UserHandle createAndManageUser(android.content.ComponentName, java.lang.String, android.content.ComponentName, android.os.PersistableBundle, int);
- method public deprecated android.os.UserHandle createUser(android.content.ComponentName, java.lang.String);
method public void enableSystemApp(android.content.ComponentName, java.lang.String);
method public int enableSystemApp(android.content.ComponentName, android.content.Intent);
method public java.lang.String[] getAccountTypesWithManagementDisabled();
@@ -9785,6 +9784,8 @@
field public static final java.lang.String FEATURE_VERIFIED_BOOT = "android.software.verified_boot";
field public static final java.lang.String FEATURE_VR_MODE = "android.software.vr.mode";
field public static final java.lang.String FEATURE_VR_MODE_HIGH_PERFORMANCE = "android.hardware.vr.high_performance";
+ field public static final java.lang.String FEATURE_VULKAN_HARDWARE_LEVEL = "android.hardware.vulkan.level";
+ field public static final java.lang.String FEATURE_VULKAN_HARDWARE_VERSION = "android.hardware.vulkan.version";
field public static final java.lang.String FEATURE_WATCH = "android.hardware.type.watch";
field public static final java.lang.String FEATURE_WEBVIEW = "android.software.webview";
field public static final java.lang.String FEATURE_WIFI = "android.hardware.wifi";
@@ -13201,8 +13202,8 @@
method public final void takePicture(android.hardware.Camera.ShutterCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback);
method public final void takePicture(android.hardware.Camera.ShutterCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback);
method public final void unlock();
- field public static final java.lang.String ACTION_NEW_PICTURE = "android.hardware.action.NEW_PICTURE";
- field public static final java.lang.String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO";
+ field public static final deprecated java.lang.String ACTION_NEW_PICTURE = "android.hardware.action.NEW_PICTURE";
+ field public static final deprecated java.lang.String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO";
field public static final int CAMERA_ERROR_EVICTED = 2; // 0x2
field public static final int CAMERA_ERROR_SERVER_DIED = 100; // 0x64
field public static final int CAMERA_ERROR_UNKNOWN = 1; // 0x1
@@ -19692,6 +19693,7 @@
field public static final int TYPE_BUILTIN_EARPIECE = 1; // 0x1
field public static final int TYPE_BUILTIN_MIC = 15; // 0xf
field public static final int TYPE_BUILTIN_SPEAKER = 2; // 0x2
+ field public static final int TYPE_BUS = 21; // 0x15
field public static final int TYPE_DOCK = 13; // 0xd
field public static final int TYPE_FM = 14; // 0xe
field public static final int TYPE_FM_TUNER = 16; // 0x10
@@ -19709,12 +19711,14 @@
field public static final int TYPE_WIRED_HEADSET = 3; // 0x3
}
- public class AudioFormat {
+ public class AudioFormat implements android.os.Parcelable {
+ method public int describeContents();
method public int getChannelCount();
method public int getChannelIndexMask();
method public int getChannelMask();
method public int getEncoding();
method public int getSampleRate();
+ method public void writeToParcel(android.os.Parcel, int);
field public static final deprecated int CHANNEL_CONFIGURATION_DEFAULT = 1; // 0x1
field public static final deprecated int CHANNEL_CONFIGURATION_INVALID = 0; // 0x0
field public static final deprecated int CHANNEL_CONFIGURATION_MONO = 2; // 0x2
@@ -19756,6 +19760,7 @@
field public static final int CHANNEL_OUT_SIDE_RIGHT = 4096; // 0x1000
field public static final int CHANNEL_OUT_STEREO = 12; // 0xc
field public static final int CHANNEL_OUT_SURROUND = 1052; // 0x41c
+ field public static final android.os.Parcelable.Creator<android.media.AudioFormat> CREATOR;
field public static final int ENCODING_AC3 = 5; // 0x5
field public static final int ENCODING_DEFAULT = 1; // 0x1
field public static final int ENCODING_DTS = 7; // 0x7
@@ -22752,6 +22757,7 @@
method public static final android.net.Uri buildProgramsUriForChannel(android.net.Uri, long, long);
method public static final android.net.Uri buildRecordedProgramUri(long);
field public static final java.lang.String AUTHORITY = "android.media.tv";
+ field public static final java.lang.String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
}
public static abstract interface TvContract.BaseTvColumns implements android.provider.BaseColumns {
@@ -46069,7 +46075,9 @@
method public long getBase();
method public java.lang.String getFormat();
method public android.widget.Chronometer.OnChronometerTickListener getOnChronometerTickListener();
+ method public boolean isCountDown();
method public void setBase(long);
+ method public void setCountDown(boolean);
method public void setFormat(java.lang.String);
method public void setOnChronometerTickListener(android.widget.Chronometer.OnChronometerTickListener);
method public void start();
diff --git a/api/removed.txt b/api/removed.txt
index 0bf6594..50a24f6 100644
--- a/api/removed.txt
+++ b/api/removed.txt
@@ -9,6 +9,8 @@
package android.app.admin {
public class DevicePolicyManager {
+ method public deprecated android.os.UserHandle createAndInitializeUser(android.content.ComponentName, java.lang.String, java.lang.String, android.content.ComponentName, android.os.Bundle);
+ method public deprecated android.os.UserHandle createUser(android.content.ComponentName, java.lang.String);
method public deprecated java.lang.String getDeviceInitializerApp();
method public deprecated android.content.ComponentName getDeviceInitializerComponent();
}
@@ -35,7 +37,7 @@
package android.media {
- public class AudioFormat {
+ public class AudioFormat implements android.os.Parcelable {
ctor public AudioFormat();
}
diff --git a/api/system-current.txt b/api/system-current.txt
index b36d7e8..11e5af5 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -518,6 +518,7 @@
field public static final int controlX2 = 16843774; // 0x10103fe
field public static final int controlY1 = 16843773; // 0x10103fd
field public static final int controlY2 = 16843775; // 0x10103ff
+ field public static final int countDown = 16844060; // 0x101051c
field public static final int country = 16843962; // 0x10104ba
field public static final int cropToPadding = 16843043; // 0x1010123
field public static final int cursorVisible = 16843090; // 0x1010152
@@ -5966,9 +5967,7 @@
method public void clearPackagePersistentPreferredActivities(android.content.ComponentName, java.lang.String);
method public void clearProfileOwner(android.content.ComponentName);
method public void clearUserRestriction(android.content.ComponentName, java.lang.String);
- method public deprecated android.os.UserHandle createAndInitializeUser(android.content.ComponentName, java.lang.String, java.lang.String, android.content.ComponentName, android.os.Bundle);
method public android.os.UserHandle createAndManageUser(android.content.ComponentName, java.lang.String, android.content.ComponentName, android.os.PersistableBundle, int);
- method public deprecated android.os.UserHandle createUser(android.content.ComponentName, java.lang.String);
method public void enableSystemApp(android.content.ComponentName, java.lang.String);
method public int enableSystemApp(android.content.ComponentName, android.content.Intent);
method public java.lang.String[] getAccountTypesWithManagementDisabled();
@@ -6025,6 +6024,7 @@
method public int getStorageEncryptionStatus();
method public android.app.admin.SystemUpdatePolicy getSystemUpdatePolicy();
method public java.util.List<android.os.PersistableBundle> getTrustAgentConfiguration(android.content.ComponentName, android.content.ComponentName);
+ method public int getUserProvisioningState();
method public android.os.Bundle getUserRestrictions(android.content.ComponentName);
method public java.lang.String getWifiMacAddress();
method public boolean hasCaCertInstalled(android.content.ComponentName, byte[]);
@@ -6111,6 +6111,7 @@
field public static final java.lang.String ACTION_ADD_DEVICE_ADMIN = "android.app.action.ADD_DEVICE_ADMIN";
field public static final java.lang.String ACTION_DEVICE_OWNER_CHANGED = "android.app.action.DEVICE_OWNER_CHANGED";
field public static final java.lang.String ACTION_MANAGED_PROFILE_PROVISIONED = "android.app.action.MANAGED_PROFILE_PROVISIONED";
+ field public static final java.lang.String ACTION_PROVISION_FINALIZATION = "android.app.action.PROVISION_FINALIZATION";
field public static final java.lang.String ACTION_PROVISION_MANAGED_DEVICE = "android.app.action.PROVISION_MANAGED_DEVICE";
field public static final java.lang.String ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE = "android.app.action.PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE";
field public static final java.lang.String ACTION_PROVISION_MANAGED_PROFILE = "android.app.action.PROVISION_MANAGED_PROFILE";
@@ -6180,6 +6181,11 @@
field public static final int RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT = 2; // 0x2
field public static final int RESET_PASSWORD_REQUIRE_ENTRY = 1; // 0x1
field public static final int SKIP_SETUP_WIZARD = 1; // 0x1
+ field public static final int STATE_USER_PROFILE_COMPLETE = 4; // 0x4
+ field public static final int STATE_USER_SETUP_COMPLETE = 2; // 0x2
+ field public static final int STATE_USER_SETUP_FINALIZED = 3; // 0x3
+ field public static final int STATE_USER_SETUP_INCOMPLETE = 1; // 0x1
+ field public static final int STATE_USER_UNMANAGED = 0; // 0x0
field public static final int WIPE_EXTERNAL_STORAGE = 1; // 0x1
field public static final int WIPE_RESET_PROTECTION_DATA = 2; // 0x2
}
@@ -10143,6 +10149,8 @@
field public static final java.lang.String FEATURE_VERIFIED_BOOT = "android.software.verified_boot";
field public static final java.lang.String FEATURE_VR_MODE = "android.software.vr.mode";
field public static final java.lang.String FEATURE_VR_MODE_HIGH_PERFORMANCE = "android.hardware.vr.high_performance";
+ field public static final java.lang.String FEATURE_VULKAN_HARDWARE_LEVEL = "android.hardware.vulkan.level";
+ field public static final java.lang.String FEATURE_VULKAN_HARDWARE_VERSION = "android.hardware.vulkan.version";
field public static final java.lang.String FEATURE_WATCH = "android.hardware.type.watch";
field public static final java.lang.String FEATURE_WEBVIEW = "android.software.webview";
field public static final java.lang.String FEATURE_WIFI = "android.hardware.wifi";
@@ -10282,6 +10290,7 @@
field public static final android.os.Parcelable.Creator<android.content.pm.PermissionInfo> CREATOR;
field public static final int FLAG_COSTS_MONEY = 1; // 0x1
field public static final int FLAG_INSTALLED = 1073741824; // 0x40000000
+ field public static final int FLAG_REMOVED = 2; // 0x2
field public static final int PROTECTION_DANGEROUS = 1; // 0x1
field public static final int PROTECTION_FLAG_APPOP = 64; // 0x40
field public static final int PROTECTION_FLAG_DEVELOPMENT = 32; // 0x20
@@ -13603,8 +13612,8 @@
method public final void takePicture(android.hardware.Camera.ShutterCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback);
method public final void takePicture(android.hardware.Camera.ShutterCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback);
method public final void unlock();
- field public static final java.lang.String ACTION_NEW_PICTURE = "android.hardware.action.NEW_PICTURE";
- field public static final java.lang.String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO";
+ field public static final deprecated java.lang.String ACTION_NEW_PICTURE = "android.hardware.action.NEW_PICTURE";
+ field public static final deprecated java.lang.String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO";
field public static final int CAMERA_ERROR_EVICTED = 2; // 0x2
field public static final int CAMERA_ERROR_SERVER_DIED = 100; // 0x64
field public static final int CAMERA_ERROR_UNKNOWN = 1; // 0x1
@@ -21168,6 +21177,7 @@
field public static final int TYPE_BUILTIN_EARPIECE = 1; // 0x1
field public static final int TYPE_BUILTIN_MIC = 15; // 0xf
field public static final int TYPE_BUILTIN_SPEAKER = 2; // 0x2
+ field public static final int TYPE_BUS = 21; // 0x15
field public static final int TYPE_DOCK = 13; // 0xd
field public static final int TYPE_FM = 14; // 0xe
field public static final int TYPE_FM_TUNER = 16; // 0x10
@@ -21197,12 +21207,14 @@
field public static final android.os.Parcelable.Creator<android.media.AudioFocusInfo> CREATOR;
}
- public class AudioFormat {
+ public class AudioFormat implements android.os.Parcelable {
+ method public int describeContents();
method public int getChannelCount();
method public int getChannelIndexMask();
method public int getChannelMask();
method public int getEncoding();
method public int getSampleRate();
+ method public void writeToParcel(android.os.Parcel, int);
field public static final deprecated int CHANNEL_CONFIGURATION_DEFAULT = 1; // 0x1
field public static final deprecated int CHANNEL_CONFIGURATION_INVALID = 0; // 0x0
field public static final deprecated int CHANNEL_CONFIGURATION_MONO = 2; // 0x2
@@ -21244,6 +21256,7 @@
field public static final int CHANNEL_OUT_SIDE_RIGHT = 4096; // 0x1000
field public static final int CHANNEL_OUT_STEREO = 12; // 0xc
field public static final int CHANNEL_OUT_SURROUND = 1052; // 0x41c
+ field public static final android.os.Parcelable.Creator<android.media.AudioFormat> CREATOR;
field public static final int ENCODING_AC3 = 5; // 0x5
field public static final int ENCODING_DEFAULT = 1; // 0x1
field public static final int ENCODING_DTS = 7; // 0x7
@@ -24375,6 +24388,7 @@
method public static final android.net.Uri buildRecordedProgramUri(long);
method public static final boolean isChannelUriForPassthroughInput(android.net.Uri);
field public static final java.lang.String AUTHORITY = "android.media.tv";
+ field public static final java.lang.String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
}
public static abstract interface TvContract.BaseTvColumns implements android.provider.BaseColumns {
@@ -26827,6 +26841,7 @@
method public void stopTrackingWifiChange(android.net.wifi.WifiScanner.WifiChangeListener);
field public static final int MAX_SCAN_PERIOD_MS = 1024000; // 0xfa000
field public static final int MIN_SCAN_PERIOD_MS = 1000; // 0x3e8
+ field public static final int REASON_DUPLICATE_REQEUST = -5; // 0xfffffffb
field public static final int REASON_INVALID_LISTENER = -2; // 0xfffffffe
field public static final int REASON_INVALID_REQUEST = -3; // 0xfffffffd
field public static final int REASON_NOT_AUTHORIZED = -4; // 0xfffffffc
@@ -31418,9 +31433,14 @@
}
public class RecoverySystem {
+ method public static void cancelScheduledUpdate(android.content.Context) throws java.io.IOException;
method public static void installPackage(android.content.Context, java.io.File) throws java.io.IOException;
+ method public static void installPackage(android.content.Context, java.io.File, boolean) throws java.io.IOException;
+ method public static void processPackage(android.content.Context, java.io.File, android.os.RecoverySystem.ProgressListener, android.os.Handler) throws java.io.IOException;
+ method public static void processPackage(android.content.Context, java.io.File, android.os.RecoverySystem.ProgressListener) throws java.io.IOException;
method public static void rebootWipeCache(android.content.Context) throws java.io.IOException;
method public static void rebootWipeUserData(android.content.Context) throws java.io.IOException;
+ method public static void scheduleUpdateOnBoot(android.content.Context, java.io.File) throws java.io.IOException;
method public static void verifyPackage(java.io.File, android.os.RecoverySystem.ProgressListener, java.io.File) throws java.security.GeneralSecurityException, java.io.IOException;
}
@@ -34751,6 +34771,7 @@
field public static final java.lang.String USB_MASS_STORAGE_ENABLED = "usb_mass_storage_enabled";
field public static final java.lang.String USE_GOOGLE_MAIL = "use_google_mail";
field public static final java.lang.String WAIT_FOR_DEBUGGER = "wait_for_debugger";
+ field public static final java.lang.String WEBVIEW_MULTIPROCESS = "webview_multiprocess";
field public static final java.lang.String WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN = "wifi_device_owner_configs_lockdown";
field public static final java.lang.String WIFI_MAX_DHCP_RETRY_COUNT = "wifi_max_dhcp_retry_count";
field public static final java.lang.String WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS = "wifi_mobile_data_transition_wakelock_timeout_ms";
@@ -49165,7 +49186,9 @@
method public long getBase();
method public java.lang.String getFormat();
method public android.widget.Chronometer.OnChronometerTickListener getOnChronometerTickListener();
+ method public boolean isCountDown();
method public void setBase(long);
+ method public void setCountDown(boolean);
method public void setFormat(java.lang.String);
method public void setOnChronometerTickListener(android.widget.Chronometer.OnChronometerTickListener);
method public void start();
diff --git a/api/system-removed.txt b/api/system-removed.txt
index 27de913..7347aa3 100644
--- a/api/system-removed.txt
+++ b/api/system-removed.txt
@@ -6,6 +6,15 @@
}
+package android.app.admin {
+
+ public class DevicePolicyManager {
+ method public deprecated android.os.UserHandle createAndInitializeUser(android.content.ComponentName, java.lang.String, java.lang.String, android.content.ComponentName, android.os.Bundle);
+ method public deprecated android.os.UserHandle createUser(android.content.ComponentName, java.lang.String);
+ }
+
+}
+
package android.content.pm {
public class PackageInfo implements android.os.Parcelable {
@@ -26,7 +35,7 @@
package android.media {
- public class AudioFormat {
+ public class AudioFormat implements android.os.Parcelable {
ctor public AudioFormat();
}
diff --git a/api/test-current.txt b/api/test-current.txt
index 3878fbf..a7bc720 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -423,6 +423,7 @@
field public static final int controlX2 = 16843774; // 0x10103fe
field public static final int controlY1 = 16843773; // 0x10103fd
field public static final int controlY2 = 16843775; // 0x10103ff
+ field public static final int countDown = 16844060; // 0x101051c
field public static final int country = 16843962; // 0x10104ba
field public static final int cropToPadding = 16843043; // 0x1010123
field public static final int cursorVisible = 16843090; // 0x1010152
@@ -5831,9 +5832,7 @@
method public void clearPackagePersistentPreferredActivities(android.content.ComponentName, java.lang.String);
method public void clearProfileOwner(android.content.ComponentName);
method public void clearUserRestriction(android.content.ComponentName, java.lang.String);
- method public deprecated android.os.UserHandle createAndInitializeUser(android.content.ComponentName, java.lang.String, java.lang.String, android.content.ComponentName, android.os.Bundle);
method public android.os.UserHandle createAndManageUser(android.content.ComponentName, java.lang.String, android.content.ComponentName, android.os.PersistableBundle, int);
- method public deprecated android.os.UserHandle createUser(android.content.ComponentName, java.lang.String);
method public void enableSystemApp(android.content.ComponentName, java.lang.String);
method public int enableSystemApp(android.content.ComponentName, android.content.Intent);
method public java.lang.String[] getAccountTypesWithManagementDisabled();
@@ -9793,6 +9792,8 @@
field public static final java.lang.String FEATURE_VERIFIED_BOOT = "android.software.verified_boot";
field public static final java.lang.String FEATURE_VR_MODE = "android.software.vr.mode";
field public static final java.lang.String FEATURE_VR_MODE_HIGH_PERFORMANCE = "android.hardware.vr.high_performance";
+ field public static final java.lang.String FEATURE_VULKAN_HARDWARE_LEVEL = "android.hardware.vulkan.level";
+ field public static final java.lang.String FEATURE_VULKAN_HARDWARE_VERSION = "android.hardware.vulkan.version";
field public static final java.lang.String FEATURE_WATCH = "android.hardware.type.watch";
field public static final java.lang.String FEATURE_WEBVIEW = "android.software.webview";
field public static final java.lang.String FEATURE_WIFI = "android.hardware.wifi";
@@ -13209,8 +13210,8 @@
method public final void takePicture(android.hardware.Camera.ShutterCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback);
method public final void takePicture(android.hardware.Camera.ShutterCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback);
method public final void unlock();
- field public static final java.lang.String ACTION_NEW_PICTURE = "android.hardware.action.NEW_PICTURE";
- field public static final java.lang.String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO";
+ field public static final deprecated java.lang.String ACTION_NEW_PICTURE = "android.hardware.action.NEW_PICTURE";
+ field public static final deprecated java.lang.String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO";
field public static final int CAMERA_ERROR_EVICTED = 2; // 0x2
field public static final int CAMERA_ERROR_SERVER_DIED = 100; // 0x64
field public static final int CAMERA_ERROR_UNKNOWN = 1; // 0x1
@@ -19701,6 +19702,7 @@
field public static final int TYPE_BUILTIN_EARPIECE = 1; // 0x1
field public static final int TYPE_BUILTIN_MIC = 15; // 0xf
field public static final int TYPE_BUILTIN_SPEAKER = 2; // 0x2
+ field public static final int TYPE_BUS = 21; // 0x15
field public static final int TYPE_DOCK = 13; // 0xd
field public static final int TYPE_FM = 14; // 0xe
field public static final int TYPE_FM_TUNER = 16; // 0x10
@@ -19718,12 +19720,14 @@
field public static final int TYPE_WIRED_HEADSET = 3; // 0x3
}
- public class AudioFormat {
+ public class AudioFormat implements android.os.Parcelable {
+ method public int describeContents();
method public int getChannelCount();
method public int getChannelIndexMask();
method public int getChannelMask();
method public int getEncoding();
method public int getSampleRate();
+ method public void writeToParcel(android.os.Parcel, int);
field public static final deprecated int CHANNEL_CONFIGURATION_DEFAULT = 1; // 0x1
field public static final deprecated int CHANNEL_CONFIGURATION_INVALID = 0; // 0x0
field public static final deprecated int CHANNEL_CONFIGURATION_MONO = 2; // 0x2
@@ -19765,6 +19769,7 @@
field public static final int CHANNEL_OUT_SIDE_RIGHT = 4096; // 0x1000
field public static final int CHANNEL_OUT_STEREO = 12; // 0xc
field public static final int CHANNEL_OUT_SURROUND = 1052; // 0x41c
+ field public static final android.os.Parcelable.Creator<android.media.AudioFormat> CREATOR;
field public static final int ENCODING_AC3 = 5; // 0x5
field public static final int ENCODING_DEFAULT = 1; // 0x1
field public static final int ENCODING_DTS = 7; // 0x7
@@ -22761,6 +22766,7 @@
method public static final android.net.Uri buildProgramsUriForChannel(android.net.Uri, long, long);
method public static final android.net.Uri buildRecordedProgramUri(long);
field public static final java.lang.String AUTHORITY = "android.media.tv";
+ field public static final java.lang.String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
}
public static abstract interface TvContract.BaseTvColumns implements android.provider.BaseColumns {
@@ -46086,7 +46092,9 @@
method public long getBase();
method public java.lang.String getFormat();
method public android.widget.Chronometer.OnChronometerTickListener getOnChronometerTickListener();
+ method public boolean isCountDown();
method public void setBase(long);
+ method public void setCountDown(boolean);
method public void setFormat(java.lang.String);
method public void setOnChronometerTickListener(android.widget.Chronometer.OnChronometerTickListener);
method public void start();
diff --git a/api/test-removed.txt b/api/test-removed.txt
index 0bf6594..50a24f6 100644
--- a/api/test-removed.txt
+++ b/api/test-removed.txt
@@ -9,6 +9,8 @@
package android.app.admin {
public class DevicePolicyManager {
+ method public deprecated android.os.UserHandle createAndInitializeUser(android.content.ComponentName, java.lang.String, java.lang.String, android.content.ComponentName, android.os.Bundle);
+ method public deprecated android.os.UserHandle createUser(android.content.ComponentName, java.lang.String);
method public deprecated java.lang.String getDeviceInitializerApp();
method public deprecated android.content.ComponentName getDeviceInitializerComponent();
}
@@ -35,7 +37,7 @@
package android.media {
- public class AudioFormat {
+ public class AudioFormat implements android.os.Parcelable {
ctor public AudioFormat();
}
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index 8717353..837ceb6 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -268,154 +268,155 @@
return mClassLoader;
}
- if (mIncludeCode && !mPackageName.equals("android")) {
- // Avoid the binder call when the package is the current application package.
- // The activity manager will perform ensure that dexopt is performed before
- // spinning up the process.
- if (!Objects.equals(mPackageName, ActivityThread.currentPackageName())) {
- final String isa = VMRuntime.getRuntime().vmInstructionSet();
- try {
- ActivityThread.getPackageManager().notifyPackageUse(mPackageName);
- } catch (RemoteException re) {
- // Ignored.
- }
- }
-
- final List<String> zipPaths = new ArrayList<>();
- final List<String> apkPaths = new ArrayList<>();
- final List<String> libPaths = new ArrayList<>();
-
- if (mRegisterPackage) {
- try {
- ActivityManagerNative.getDefault().addPackageDependency(mPackageName);
- } catch (RemoteException e) {
- }
- }
-
- zipPaths.add(mAppDir);
- if (mSplitAppDirs != null) {
- Collections.addAll(zipPaths, mSplitAppDirs);
- }
-
- libPaths.add(mLibDir);
-
- /*
- * The following is a bit of a hack to inject
- * instrumentation into the system: If the app
- * being started matches one of the instrumentation names,
- * then we combine both the "instrumentation" and
- * "instrumented" app into the path, along with the
- * concatenation of both apps' shared library lists.
- */
-
- String instrumentationPackageName = mActivityThread.mInstrumentationPackageName;
- String instrumentationAppDir = mActivityThread.mInstrumentationAppDir;
- String[] instrumentationSplitAppDirs = mActivityThread.mInstrumentationSplitAppDirs;
- String instrumentationLibDir = mActivityThread.mInstrumentationLibDir;
-
- String instrumentedAppDir = mActivityThread.mInstrumentedAppDir;
- String[] instrumentedSplitAppDirs = mActivityThread.mInstrumentedSplitAppDirs;
- String instrumentedLibDir = mActivityThread.mInstrumentedLibDir;
- String[] instrumentationLibs = null;
-
- if (mAppDir.equals(instrumentationAppDir)
- || mAppDir.equals(instrumentedAppDir)) {
- zipPaths.clear();
- zipPaths.add(instrumentationAppDir);
- if (instrumentationSplitAppDirs != null) {
- Collections.addAll(zipPaths, instrumentationSplitAppDirs);
- }
- zipPaths.add(instrumentedAppDir);
- if (instrumentedSplitAppDirs != null) {
- Collections.addAll(zipPaths, instrumentedSplitAppDirs);
- }
-
- libPaths.clear();
- libPaths.add(instrumentationLibDir);
- libPaths.add(instrumentedLibDir);
-
- if (!instrumentedAppDir.equals(instrumentationAppDir)) {
- instrumentationLibs = getLibrariesFor(instrumentationPackageName);
- }
- }
-
- apkPaths.addAll(zipPaths);
-
- if (mSharedLibraries != null) {
- for (String lib : mSharedLibraries) {
- if (!zipPaths.contains(lib)) {
- zipPaths.add(0, lib);
- }
- }
- }
-
- if (instrumentationLibs != null) {
- for (String lib : instrumentationLibs) {
- if (!zipPaths.contains(lib)) {
- zipPaths.add(0, lib);
- }
- }
- }
-
- final String zip = TextUtils.join(File.pathSeparator, zipPaths);
-
- // Add path to libraries in apk for current abi
- if (mApplicationInfo.primaryCpuAbi != null) {
- for (String apk : apkPaths) {
- libPaths.add(apk + "!/lib/" + mApplicationInfo.primaryCpuAbi);
- }
- }
-
- String libraryPermittedPath = mDataDir;
- boolean isBundledApp = false;
-
- if (mApplicationInfo.isSystemApp()) {
- isBundledApp = true;
- // Add path to system libraries to libPaths;
- // Access to system libs should be limited
- // to bundled applications; this is why updated
- // system apps are not included.
- libPaths.add(System.getProperty("java.library.path"));
-
- // This is necessary to grant bundled apps access to
- // libraries located in subdirectories of /system/lib
- libraryPermittedPath += File.pathSeparator +
- System.getProperty("java.library.path");
- }
- // DO NOT SHIP: this is a workaround for apps loading native libraries
- // provided by 3rd party apps using absolute path instead of corresponding
- // classloader; see http://b/26954419 for example.
- if (mApplicationInfo.targetSdkVersion <= 23) {
- libraryPermittedPath += File.pathSeparator + "/data/app";
- }
- // -----------------------------------------------------------------------------
-
- final String librarySearchPath = TextUtils.join(File.pathSeparator, libPaths);
-
- /*
- * With all the combination done (if necessary, actually
- * create the class loader.
- */
-
- if (ActivityThread.localLOGV)
- Slog.v(ActivityThread.TAG, "Class path: " + zip +
- ", JNI path: " + librarySearchPath);
-
- // Temporarily disable logging of disk reads on the Looper thread
- // as this is early and necessary.
- StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
-
- mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, isBundledApp,
- librarySearchPath, libraryPermittedPath, mBaseClassLoader);
-
- StrictMode.setThreadPolicy(oldPolicy);
- } else {
+ if (mPackageName.equals("android")) {
if (mBaseClassLoader == null) {
mClassLoader = ClassLoader.getSystemClassLoader();
} else {
mClassLoader = mBaseClassLoader;
}
+ return mClassLoader;
}
+
+ // Avoid the binder call when the package is the current application package.
+ // The activity manager will perform ensure that dexopt is performed before
+ // spinning up the process.
+ if (!Objects.equals(mPackageName, ActivityThread.currentPackageName())) {
+ final String isa = VMRuntime.getRuntime().vmInstructionSet();
+ try {
+ ActivityThread.getPackageManager().notifyPackageUse(mPackageName);
+ } catch (RemoteException re) {
+ // Ignored.
+ }
+ }
+
+ final List<String> zipPaths = new ArrayList<>();
+ final List<String> apkPaths = new ArrayList<>();
+ final List<String> libPaths = new ArrayList<>();
+
+ if (mRegisterPackage) {
+ try {
+ ActivityManagerNative.getDefault().addPackageDependency(mPackageName);
+ } catch (RemoteException e) {
+ }
+ }
+
+ zipPaths.add(mAppDir);
+ if (mSplitAppDirs != null) {
+ Collections.addAll(zipPaths, mSplitAppDirs);
+ }
+
+ libPaths.add(mLibDir);
+
+ /*
+ * The following is a bit of a hack to inject
+ * instrumentation into the system: If the app
+ * being started matches one of the instrumentation names,
+ * then we combine both the "instrumentation" and
+ * "instrumented" app into the path, along with the
+ * concatenation of both apps' shared library lists.
+ */
+
+ String instrumentationPackageName = mActivityThread.mInstrumentationPackageName;
+ String instrumentationAppDir = mActivityThread.mInstrumentationAppDir;
+ String[] instrumentationSplitAppDirs = mActivityThread.mInstrumentationSplitAppDirs;
+ String instrumentationLibDir = mActivityThread.mInstrumentationLibDir;
+
+ String instrumentedAppDir = mActivityThread.mInstrumentedAppDir;
+ String[] instrumentedSplitAppDirs = mActivityThread.mInstrumentedSplitAppDirs;
+ String instrumentedLibDir = mActivityThread.mInstrumentedLibDir;
+ String[] instrumentationLibs = null;
+
+ if (mAppDir.equals(instrumentationAppDir)
+ || mAppDir.equals(instrumentedAppDir)) {
+ zipPaths.clear();
+ zipPaths.add(instrumentationAppDir);
+ if (instrumentationSplitAppDirs != null) {
+ Collections.addAll(zipPaths, instrumentationSplitAppDirs);
+ }
+ zipPaths.add(instrumentedAppDir);
+ if (instrumentedSplitAppDirs != null) {
+ Collections.addAll(zipPaths, instrumentedSplitAppDirs);
+ }
+
+ libPaths.clear();
+ libPaths.add(instrumentationLibDir);
+ libPaths.add(instrumentedLibDir);
+
+ if (!instrumentedAppDir.equals(instrumentationAppDir)) {
+ instrumentationLibs = getLibrariesFor(instrumentationPackageName);
+ }
+ }
+
+ apkPaths.addAll(zipPaths);
+
+ if (mSharedLibraries != null) {
+ for (String lib : mSharedLibraries) {
+ if (!zipPaths.contains(lib)) {
+ zipPaths.add(0, lib);
+ }
+ }
+ }
+
+ if (instrumentationLibs != null) {
+ for (String lib : instrumentationLibs) {
+ if (!zipPaths.contains(lib)) {
+ zipPaths.add(0, lib);
+ }
+ }
+ }
+
+ final String zip = mIncludeCode ? TextUtils.join(File.pathSeparator, zipPaths) : "";
+
+ // Add path to libraries in apk for current abi
+ if (mApplicationInfo.primaryCpuAbi != null) {
+ for (String apk : apkPaths) {
+ libPaths.add(apk + "!/lib/" + mApplicationInfo.primaryCpuAbi);
+ }
+ }
+
+ String libraryPermittedPath = mDataDir;
+ boolean isBundledApp = false;
+
+ if (mApplicationInfo.isSystemApp()) {
+ isBundledApp = true;
+ // Add path to system libraries to libPaths;
+ // Access to system libs should be limited
+ // to bundled applications; this is why updated
+ // system apps are not included.
+ libPaths.add(System.getProperty("java.library.path"));
+
+ // This is necessary to grant bundled apps access to
+ // libraries located in subdirectories of /system/lib
+ libraryPermittedPath += File.pathSeparator +
+ System.getProperty("java.library.path");
+ }
+ // DO NOT SHIP: this is a workaround for apps loading native libraries
+ // provided by 3rd party apps using absolute path instead of corresponding
+ // classloader; see http://b/26954419 for example.
+ if (mApplicationInfo.targetSdkVersion <= 23) {
+ libraryPermittedPath += File.pathSeparator + "/data/app";
+ }
+ // -----------------------------------------------------------------------------
+
+ final String librarySearchPath = TextUtils.join(File.pathSeparator, libPaths);
+
+ /*
+ * With all the combination done (if necessary, actually
+ * create the class loader.
+ */
+
+ if (ActivityThread.localLOGV)
+ Slog.v(ActivityThread.TAG, "Class path: " + zip +
+ ", JNI path: " + librarySearchPath);
+
+ // Temporarily disable logging of disk reads on the Looper thread
+ // as this is early and necessary.
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
+
+ mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, isBundledApp,
+ librarySearchPath, libraryPermittedPath, mBaseClassLoader);
+
+ StrictMode.setThreadPolicy(oldPolicy);
return mClassLoader;
}
}
diff --git a/core/java/android/app/NativeActivity.java b/core/java/android/app/NativeActivity.java
index aa14cb5..3c7f48b 100644
--- a/core/java/android/app/NativeActivity.java
+++ b/core/java/android/app/NativeActivity.java
@@ -166,7 +166,8 @@
String path = classLoader.findLibrary(libname);
if (path == null) {
- throw new IllegalArgumentException("Unable to find native library: " + libname);
+ throw new IllegalArgumentException("Unable to find native library " + libname +
+ " using classloader: " + classLoader.toString());
}
byte[] nativeSavedState = savedInstanceState != null
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 52fba3b..307c3eb 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -91,9 +91,11 @@
import android.os.IBinder;
import android.os.IHardwarePropertiesManager;
import android.os.IPowerManager;
+import android.os.IRecoverySystem;
import android.os.IUserManager;
import android.os.PowerManager;
import android.os.Process;
+import android.os.RecoverySystem;
import android.os.ServiceManager;
import android.os.SystemVibrator;
import android.os.UserHandle;
@@ -380,6 +382,18 @@
service, ctx.mMainThread.getHandler());
}});
+ registerService(Context.RECOVERY_SERVICE, RecoverySystem.class,
+ new CachedServiceFetcher<RecoverySystem>() {
+ @Override
+ public RecoverySystem createService(ContextImpl ctx) {
+ IBinder b = ServiceManager.getService(Context.RECOVERY_SERVICE);
+ IRecoverySystem service = IRecoverySystem.Stub.asInterface(b);
+ if (service == null) {
+ Log.wtf(TAG, "Failed to get recovery service.");
+ }
+ return new RecoverySystem(service);
+ }});
+
registerService(Context.SEARCH_SERVICE, SearchManager.class,
new CachedServiceFetcher<SearchManager>() {
@Override
diff --git a/core/java/android/app/admin/DeviceAdminReceiver.java b/core/java/android/app/admin/DeviceAdminReceiver.java
index d5ca0e9..a34e8551e 100644
--- a/core/java/android/app/admin/DeviceAdminReceiver.java
+++ b/core/java/android/app/admin/DeviceAdminReceiver.java
@@ -383,8 +383,8 @@
*
* <p> If the admin is activated by a device owner, then the intent
* may contain private extras that are relevant to user setup.
- * {@see DevicePolicyManager#createAndInitializeUser(ComponentName, String, String,
- * ComponentName, Intent)}
+ * {@see DevicePolicyManager#createAndManageUser(ComponentName, String, ComponentName,
+ * PersistableBundle, int)}
*
* @param context The running context as per {@link #onReceive}.
* @param intent The received intent as per {@link #onReceive}.
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 820e0e8..ba8f1f4 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -341,6 +341,7 @@
*
* @hide
*/
+ @SystemApi
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_PROVISION_FINALIZATION
= "android.app.action.PROVISION_FINALIZATION";
@@ -932,30 +933,35 @@
* No management for current user in-effect. This is the default.
* @hide
*/
+ @SystemApi
public static final int STATE_USER_UNMANAGED = 0;
/**
* Management partially setup, user setup needs to be completed.
* @hide
*/
+ @SystemApi
public static final int STATE_USER_SETUP_INCOMPLETE = 1;
/**
* Management partially setup, user setup completed.
* @hide
*/
+ @SystemApi
public static final int STATE_USER_SETUP_COMPLETE = 2;
/**
* Management setup and active on current user.
* @hide
*/
+ @SystemApi
public static final int STATE_USER_SETUP_FINALIZED = 3;
/**
* Management partially setup on a managed profile.
* @hide
*/
+ @SystemApi
public static final int STATE_USER_PROFILE_COMPLETE = 4;
/**
@@ -4539,14 +4545,10 @@
* user could not be created.
*
* @deprecated From {@link android.os.Build.VERSION_CODES#M}
+ * @removed From {@link android.os.Build.VERSION_CODES#N}
*/
@Deprecated
public UserHandle createUser(@NonNull ComponentName admin, String name) {
- try {
- return mService.createUser(admin, name);
- } catch (RemoteException re) {
- Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, re);
- }
return null;
}
@@ -4576,16 +4578,11 @@
* user could not be created.
*
* @deprecated From {@link android.os.Build.VERSION_CODES#M}
+ * @removed From {@link android.os.Build.VERSION_CODES#N}
*/
@Deprecated
public UserHandle createAndInitializeUser(@NonNull ComponentName admin, String name,
String ownerName, @NonNull ComponentName profileOwnerComponent, Bundle adminExtras) {
- try {
- return mService.createAndInitializeUser(admin, name, ownerName, profileOwnerComponent,
- adminExtras);
- } catch (RemoteException re) {
- Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, re);
- }
return null;
}
@@ -5900,6 +5897,7 @@
* return {@link #STATE_USER_UNMANAGED}
* @hide
*/
+ @SystemApi
@UserProvisioningState
public int getUserProvisioningState() {
if (mService != null) {
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index e9af872..e78c0ac 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -185,8 +185,6 @@
boolean setApplicationHidden(in ComponentName admin, in String packageName, boolean hidden);
boolean isApplicationHidden(in ComponentName admin, in String packageName);
- UserHandle createUser(in ComponentName who, in String name);
- UserHandle createAndInitializeUser(in ComponentName who, in String name, in String profileOwnerName, in ComponentName profileOwnerComponent, in Bundle adminExtras);
UserHandle createAndManageUser(in ComponentName who, in String name, in ComponentName profileOwner, in PersistableBundle adminExtras, in int flags);
boolean removeUser(in ComponentName who, in UserHandle userHandle);
boolean switchUser(in ComponentName who, in UserHandle userHandle);
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 0cdbef0..b935b25 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -2863,6 +2863,16 @@
/**
* Use with {@link #getSystemService} to retrieve a
+ * {@link android.os.RecoverySystem} for accessing the recovery system
+ * service.
+ *
+ * @see #getSystemService
+ * @hide
+ */
+ public static final String RECOVERY_SERVICE = "recovery";
+
+ /**
+ * Use with {@link #getSystemService} to retrieve a
* {@link android.view.WindowManager} for accessing the system's window
* manager.
*
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 188e1d7..b7f968c 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -1573,6 +1573,48 @@
/**
* Feature for {@link #getSystemAvailableFeatures} and
+ * {@link #hasSystemFeature}: If this feature is supported, the Vulkan native API will enumerate
+ * at least one {@code VkPhysicalDevice}, and the feature version will indicate what
+ * level of optional hardware features limits it supports.
+ * <p>
+ * Level 0 includes the base Vulkan requirements as well as:
+ * <ul><li>{@code VkPhysicalDeviceFeatures::textureCompressionETC2}</li></ul>
+ * <p>
+ * Level 1 additionally includes:
+ * <ul>
+ * <li>{@code VkPhysicalDeviceFeatures::fullDrawIndexUint32}</li>
+ * <li>{@code VkPhysicalDeviceFeatures::imageCubeArray}</li>
+ * <li>{@code VkPhysicalDeviceFeatures::independentBlend}</li>
+ * <li>{@code VkPhysicalDeviceFeatures::geometryShader}</li>
+ * <li>{@code VkPhysicalDeviceFeatures::tessellationShader}</li>
+ * <li>{@code VkPhysicalDeviceFeatures::sampleRateShading}</li>
+ * <li>{@code VkPhysicalDeviceFeatures::textureCompressionASTC_LDR}</li>
+ * <li>{@code VkPhysicalDeviceFeatures::fragmentStoresAndAtomics}</li>
+ * <li>{@code VkPhysicalDeviceFeatures::shaderImageGatherExtended}</li>
+ * <li>{@code VkPhysicalDeviceFeatures::shaderUniformBufferArrayDynamicIndexing}</li>
+ * <li>{@code VkPhysicalDeviceFeatures::shaderSampledImageArrayDynamicIndexing}</li>
+ * </ul>
+ */
+ @SdkConstant(SdkConstantType.FEATURE)
+ public static final String FEATURE_VULKAN_HARDWARE_LEVEL = "android.hardware.vulkan.level";
+
+ /**
+ * Feature for {@link #getSystemAvailableFeatures} and
+ * {@link #hasSystemFeature}: The version of this feature indicates the highest
+ * {@code VkPhysicalDeviceProperties::apiVersion} supported by the physical devices that support
+ * the hardware level indicated by {@link #FEATURE_VULKAN_HARDWARE_LEVEL}. The feature version
+ * uses the same encoding as Vulkan version numbers:
+ * <ul>
+ * <li>Major version number in bits 31-22</li>
+ * <li>Minor version number in bits 21-12</li>
+ * <li>Patch version number in bits 11-0</li>
+ * </ul>
+ */
+ @SdkConstant(SdkConstantType.FEATURE)
+ public static final String FEATURE_VULKAN_HARDWARE_VERSION = "android.hardware.vulkan.version";
+
+ /**
+ * Feature for {@link #getSystemAvailableFeatures} and
* {@link #hasSystemFeature}: The device includes an accelerometer.
*/
@SdkConstant(SdkConstantType.FEATURE)
@@ -4059,7 +4101,7 @@
*
* @return A list of {@link InstrumentationInfo} objects containing one
* entry for each matching instrumentation. If there are no
- * instrumentation available, returns and empty list.
+ * instrumentation available, returns an empty list.
*
* @see #GET_META_DATA
*/
diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java
index 9da2ba9..984a960 100644
--- a/core/java/android/content/pm/PermissionInfo.java
+++ b/core/java/android/content/pm/PermissionInfo.java
@@ -16,6 +16,7 @@
package android.content.pm;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
@@ -145,11 +146,12 @@
public static final int FLAG_COSTS_MONEY = 1<<0;
/**
- * Flag for {@link #flags}, corresponding to <code>hidden</code>
+ * Flag for {@link #flags}, corresponding to <code>removed</code>
* value of {@link android.R.attr#permissionFlags}.
* @hide
*/
- public static final int FLAG_HIDDEN = 1<<1;
+ @SystemApi
+ public static final int FLAG_REMOVED = 1<<1;
/**
* Flag for {@link #flags}, indicating that this permission has been
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
index 02d4e59..3dbe437 100644
--- a/core/java/android/hardware/Camera.java
+++ b/core/java/android/hardware/Camera.java
@@ -19,6 +19,7 @@
import android.app.ActivityThread;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
+import android.app.job.JobInfo;
import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.Point;
@@ -177,19 +178,27 @@
private static final int NO_ERROR = 0;
/**
+ * @deprecated This broadcast is no longer delivered by the system; use
+ * {@link android.app.job.JobInfo.Builder JobInfo.Builder}.{@link android.app.job.JobInfo.Builder#addTriggerContentUri}
+ * instead.
* Broadcast Action: A new picture is taken by the camera, and the entry of
* the picture has been added to the media store.
* {@link android.content.Intent#getData} is URI of the picture.
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @Deprecated
public static final String ACTION_NEW_PICTURE = "android.hardware.action.NEW_PICTURE";
/**
+ * @deprecated This broadcast is no longer delivered by the system; use
+ * {@link android.app.job.JobInfo.Builder JobInfo.Builder}.{@link android.app.job.JobInfo.Builder#addTriggerContentUri}
+ * instead.
* Broadcast Action: A new video is recorded by the camera, and the entry
* of the video has been added to the media store.
* {@link android.content.Intent#getData} is URI of the video.
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @Deprecated
public static final String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO";
/**
diff --git a/core/java/android/os/IRecoverySystem.aidl b/core/java/android/os/IRecoverySystem.aidl
new file mode 100644
index 0000000..12830a4
--- /dev/null
+++ b/core/java/android/os/IRecoverySystem.aidl
@@ -0,0 +1,28 @@
+/* //device/java/android/android/os/IRecoverySystem.aidl
+**
+** Copyright 2016, 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.os;
+
+import android.os.IRecoverySystemProgressListener;
+
+/** @hide */
+
+interface IRecoverySystem {
+ boolean uncrypt(in String packageFile, IRecoverySystemProgressListener listener);
+ boolean setupBcb(in String command);
+ boolean clearBcb();
+}
diff --git a/core/java/android/os/IRecoverySystemProgressListener.aidl b/core/java/android/os/IRecoverySystemProgressListener.aidl
new file mode 100644
index 0000000..d6f712e
--- /dev/null
+++ b/core/java/android/os/IRecoverySystemProgressListener.aidl
@@ -0,0 +1,24 @@
+/* //device/java/android/android/os/IRecoverySystemProgressListener.aidl
+**
+** Copyright 2016, 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.os;
+
+/** @hide */
+
+oneway interface IRecoverySystemProgressListener {
+ void onProgress(int progress);
+}
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 314b7d5..dcc28d6 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -385,9 +385,9 @@
public static final int GO_TO_SLEEP_FLAG_NO_DOZE = 1 << 0;
/**
- * The value to pass as the 'reason' argument to reboot() to
- * reboot into recovery mode (for applying system updates, doing
- * factory resets, etc.).
+ * The value to pass as the 'reason' argument to reboot() to reboot into
+ * recovery mode for tasks other than applying system updates, such as
+ * doing factory resets.
* <p>
* Requires the {@link android.Manifest.permission#RECOVERY}
* permission (in addition to
@@ -398,6 +398,18 @@
public static final String REBOOT_RECOVERY = "recovery";
/**
+ * The value to pass as the 'reason' argument to reboot() to reboot into
+ * recovery mode for applying system updates.
+ * <p>
+ * Requires the {@link android.Manifest.permission#RECOVERY}
+ * permission (in addition to
+ * {@link android.Manifest.permission#REBOOT}).
+ * </p>
+ * @hide
+ */
+ public static final String REBOOT_RECOVERY_UPDATE = "recovery-update";
+
+ /**
* The value to pass as the 'reason' argument to reboot() when device owner requests a reboot on
* the device.
* @hide
diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java
index 154c9bb..ddcd635 100644
--- a/core/java/android/os/RecoverySystem.java
+++ b/core/java/android/os/RecoverySystem.java
@@ -16,6 +16,7 @@
package android.os;
+import android.annotation.SystemApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -66,15 +67,34 @@
private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500;
/** Used to communicate with recovery. See bootable/recovery/recovery.cpp. */
- private static File RECOVERY_DIR = new File("/cache/recovery");
- private static File BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map");
- private static File COMMAND_FILE = new File(RECOVERY_DIR, "command");
- private static File UNCRYPT_FILE = new File(RECOVERY_DIR, "uncrypt_file");
- private static File LOG_FILE = new File(RECOVERY_DIR, "log");
- private static String LAST_PREFIX = "last_";
+ private static final File RECOVERY_DIR = new File("/cache/recovery");
+ private static final File LOG_FILE = new File(RECOVERY_DIR, "log");
+ private static final String LAST_PREFIX = "last_";
+
+ /**
+ * The recovery image uses this file to identify the location (i.e. blocks)
+ * of an OTA package on the /data partition. The block map file is
+ * generated by uncrypt.
+ *
+ * @hide
+ */
+ public static final File BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map");
+
+ /**
+ * UNCRYPT_PACKAGE_FILE stores the filename to be uncrypt'd, which will be
+ * read by uncrypt.
+ *
+ * @hide
+ */
+ public static final File UNCRYPT_PACKAGE_FILE = new File(RECOVERY_DIR, "uncrypt_file");
// Length limits for reading files.
- private static int LOG_FILE_MAX_LENGTH = 64 * 1024;
+ private static final int LOG_FILE_MAX_LENGTH = 64 * 1024;
+
+ // Prevent concurrent execution of requests.
+ private static final Object sRequestLock = new Object();
+
+ private final IRecoverySystem mService;
/**
* Interface definition for a callback to be invoked regularly as
@@ -287,6 +307,89 @@
}
/**
+ * Process a given package with uncrypt. No-op if the package is not on the
+ * /data partition.
+ *
+ * @param Context the Context to use
+ * @param packageFile the package to be processed
+ * @param listener an object to receive periodic progress updates as
+ * processing proceeds. May be null.
+ * @param handler the Handler upon which the callbacks will be
+ * executed.
+ *
+ * @throws IOException if there were any errors processing the package file.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static void processPackage(Context context,
+ File packageFile,
+ final ProgressListener listener,
+ final Handler handler)
+ throws IOException {
+ String filename = packageFile.getCanonicalPath();
+ if (!filename.startsWith("/data/")) {
+ return;
+ }
+
+ RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
+ IRecoverySystemProgressListener progressListener = null;
+ if (listener != null) {
+ final Handler progressHandler;
+ if (handler != null) {
+ progressHandler = handler;
+ } else {
+ progressHandler = new Handler(context.getMainLooper());
+ }
+ progressListener = new IRecoverySystemProgressListener.Stub() {
+ int lastProgress = 0;
+ long lastPublishTime = System.currentTimeMillis();
+
+ @Override
+ public void onProgress(final int progress) {
+ final long now = System.currentTimeMillis();
+ progressHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (progress > lastProgress &&
+ now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
+ lastProgress = progress;
+ lastPublishTime = now;
+ listener.onProgress(progress);
+ }
+ }
+ });
+ }
+ };
+ }
+
+ if (!rs.uncrypt(filename, progressListener)) {
+ throw new IOException("process package failed");
+ }
+ }
+
+ /**
+ * Process a given package with uncrypt. No-op if the package is not on the
+ * /data partition.
+ *
+ * @param Context the Context to use
+ * @param packageFile the package to be processed
+ * @param listener an object to receive periodic progress updates as
+ * processing proceeds. May be null.
+ *
+ * @throws IOException if there were any errors processing the package file.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static void processPackage(Context context,
+ File packageFile,
+ final ProgressListener listener)
+ throws IOException {
+ processPackage(context, packageFile, listener, null);
+ }
+
+ /**
* Reboots the device in order to install the given update
* package.
* Requires the {@link android.Manifest.permission#REBOOT} permission.
@@ -301,30 +404,127 @@
* fails, or if the reboot itself fails.
*/
public static void installPackage(Context context, File packageFile)
- throws IOException {
+ throws IOException {
+ installPackage(context, packageFile, false);
+ }
+
+ /**
+ * If the package hasn't been processed (i.e. uncrypt'd), set up
+ * UNCRYPT_PACKAGE_FILE and delete BLOCK_MAP_FILE to trigger uncrypt during the
+ * reboot.
+ *
+ * @param context the Context to use
+ * @param packageFile the update package to install. Must be on a
+ * partition mountable by recovery.
+ * @param processed if the package has been processed (uncrypt'd).
+ *
+ * @throws IOException if writing the recovery command file fails, or if
+ * the reboot itself fails.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static void installPackage(Context context, File packageFile, boolean processed)
+ throws IOException {
+ synchronized (sRequestLock) {
+ LOG_FILE.delete();
+ // Must delete the file in case it was created by system server.
+ UNCRYPT_PACKAGE_FILE.delete();
+
+ String filename = packageFile.getCanonicalPath();
+ Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
+
+ if (!processed && filename.startsWith("/data/")) {
+ FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE);
+ try {
+ uncryptFile.write(filename + "\n");
+ } finally {
+ uncryptFile.close();
+ }
+ // UNCRYPT_PACKAGE_FILE needs to be readable and writable by system server.
+ if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false)
+ || !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) {
+ Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE);
+ }
+
+ BLOCK_MAP_FILE.delete();
+ }
+
+ // If the package is on the /data partition, use the block map file as
+ // the package name instead.
+ if (filename.startsWith("/data/")) {
+ filename = "@/cache/recovery/block.map";
+ }
+
+ final String filenameArg = "--update_package=" + filename + "\n";
+ final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n";
+ final String command = filenameArg + localeArg;
+
+ RecoverySystem rs = (RecoverySystem) context.getSystemService(
+ Context.RECOVERY_SERVICE);
+ if (!rs.setupBcb(command)) {
+ throw new IOException("Setup BCB failed");
+ }
+
+ // Having set up the BCB (bootloader control block), go ahead and reboot
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ pm.reboot(PowerManager.REBOOT_RECOVERY_UPDATE);
+
+ throw new IOException("Reboot failed (no permissions?)");
+ }
+ }
+
+ /**
+ * Schedule to install the given package on next boot. The caller needs to
+ * ensure that the package must have been processed (uncrypt'd) if needed.
+ * It sets up the command in BCB (bootloader control block), which will
+ * be read by the bootloader and the recovery image.
+ *
+ * @param Context the Context to use.
+ * @param packageFile the package to be installed.
+ *
+ * @throws IOException if there were any errors setting up the BCB.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static void scheduleUpdateOnBoot(Context context, File packageFile)
+ throws IOException {
String filename = packageFile.getCanonicalPath();
- FileWriter uncryptFile = new FileWriter(UNCRYPT_FILE);
- try {
- uncryptFile.write(filename + "\n");
- } finally {
- uncryptFile.close();
- }
- // UNCRYPT_FILE needs to be readable by system server on bootup.
- if (!UNCRYPT_FILE.setReadable(true, false)) {
- Log.e(TAG, "Error setting readable for " + UNCRYPT_FILE.getCanonicalPath());
- }
- Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
-
- // If the package is on the /data partition, write the block map file
- // into COMMAND_FILE instead.
+ // If the package is on the /data partition, use the block map file as
+ // the package name instead.
if (filename.startsWith("/data/")) {
filename = "@/cache/recovery/block.map";
}
- final String filenameArg = "--update_package=" + filename;
- final String localeArg = "--locale=" + Locale.getDefault().toString();
- bootCommand(context, filenameArg, localeArg);
+ final String filenameArg = "--update_package=" + filename + "\n";
+ final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n";
+ final String command = filenameArg + localeArg;
+
+ RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
+ if (!rs.setupBcb(command)) {
+ throw new IOException("schedule update on boot failed");
+ }
+ }
+
+ /**
+ * Cancel any scheduled update by clearing up the BCB (bootloader control
+ * block).
+ *
+ * @param Context the Context to use.
+ *
+ * @throws IOException if there were any errors clearing up the BCB.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static void cancelScheduledUpdate(Context context)
+ throws IOException {
+ RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
+ if (!rs.clearBcb()) {
+ throw new IOException("cancel scheduled update failed");
+ }
}
/**
@@ -434,27 +634,28 @@
* @throws IOException if something goes wrong.
*/
private static void bootCommand(Context context, String... args) throws IOException {
- RECOVERY_DIR.mkdirs(); // In case we need it
- COMMAND_FILE.delete(); // In case it's not writable
- LOG_FILE.delete();
+ synchronized (sRequestLock) {
+ LOG_FILE.delete();
- FileWriter command = new FileWriter(COMMAND_FILE);
- try {
+ StringBuilder command = new StringBuilder();
for (String arg : args) {
if (!TextUtils.isEmpty(arg)) {
- command.write(arg);
- command.write("\n");
+ command.append(arg);
+ command.append("\n");
}
}
- } finally {
- command.close();
+
+ // Write the command into BCB (bootloader control block).
+ RecoverySystem rs = (RecoverySystem) context.getSystemService(
+ Context.RECOVERY_SERVICE);
+ rs.setupBcb(command.toString());
+
+ // Having set up the BCB, go ahead and reboot.
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ pm.reboot(PowerManager.REBOOT_RECOVERY);
+
+ throw new IOException("Reboot failed (no permissions?)");
}
-
- // Having written the command file, go ahead and reboot
- PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- pm.reboot(PowerManager.REBOOT_RECOVERY);
-
- throw new IOException("Reboot failed (no permissions?)");
}
/**
@@ -476,10 +677,10 @@
// Only remove the OTA package if it's partially processed (uncrypt'd).
boolean reservePackage = BLOCK_MAP_FILE.exists();
- if (!reservePackage && UNCRYPT_FILE.exists()) {
+ if (!reservePackage && UNCRYPT_PACKAGE_FILE.exists()) {
String filename = null;
try {
- filename = FileUtils.readTextFile(UNCRYPT_FILE, 0, null);
+ filename = FileUtils.readTextFile(UNCRYPT_PACKAGE_FILE, 0, null);
} catch (IOException e) {
Log.e(TAG, "Error reading uncrypt file", e);
}
@@ -487,7 +688,7 @@
// Remove the OTA package on /data that has been (possibly
// partially) processed. (Bug: 24973532)
if (filename != null && filename.startsWith("/data")) {
- if (UNCRYPT_FILE.delete()) {
+ if (UNCRYPT_PACKAGE_FILE.delete()) {
Log.i(TAG, "Deleted: " + filename);
} else {
Log.e(TAG, "Can't delete: " + filename);
@@ -499,13 +700,13 @@
// the block map file (BLOCK_MAP_FILE) for a package. BLOCK_MAP_FILE
// will be created at the end of a successful uncrypt. If seeing this
// file, we keep the block map file and the file that contains the
- // package name (UNCRYPT_FILE). This is to reduce the work for GmsCore
- // to avoid re-downloading everything again.
+ // package name (UNCRYPT_PACKAGE_FILE). This is to reduce the work for
+ // GmsCore to avoid re-downloading everything again.
String[] names = RECOVERY_DIR.list();
for (int i = 0; names != null && i < names.length; i++) {
if (names[i].startsWith(LAST_PREFIX)) continue;
if (reservePackage && names[i].equals(BLOCK_MAP_FILE.getName())) continue;
- if (reservePackage && names[i].equals(UNCRYPT_FILE.getName())) continue;
+ if (reservePackage && names[i].equals(UNCRYPT_PACKAGE_FILE.getName())) continue;
recursiveDelete(new File(RECOVERY_DIR, names[i]));
}
@@ -533,6 +734,39 @@
}
/**
+ * Talks to RecoverySystemService via Binder to trigger uncrypt.
+ */
+ private boolean uncrypt(String packageFile, IRecoverySystemProgressListener listener) {
+ try {
+ return mService.uncrypt(packageFile, listener);
+ } catch (RemoteException unused) {
+ }
+ return false;
+ }
+
+ /**
+ * Talks to RecoverySystemService via Binder to set up the BCB.
+ */
+ private boolean setupBcb(String command) {
+ try {
+ return mService.setupBcb(command);
+ } catch (RemoteException unused) {
+ }
+ return false;
+ }
+
+ /**
+ * Talks to RecoverySystemService via Binder to clear up the BCB.
+ */
+ private boolean clearBcb() {
+ try {
+ return mService.clearBcb();
+ } catch (RemoteException unused) {
+ }
+ return false;
+ }
+
+ /**
* Internally, recovery treats each line of the command file as a separate
* argv, so we only need to protect against newlines and nulls.
*/
@@ -546,5 +780,14 @@
/**
* @removed Was previously made visible by accident.
*/
- public RecoverySystem() { }
+ public RecoverySystem() {
+ mService = null;
+ }
+
+ /**
+ * @hide
+ */
+ public RecoverySystem(IRecoverySystem service) {
+ mService = service;
+ }
}
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 69d564f..1a093d8 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -379,8 +379,7 @@
* Specifies that the user is not allowed to make outgoing
* phone calls. Emergency calls are still permitted.
* The default value is <code>false</code>.
- * <p>This restriction has no effect on managed profiles since call intents are normally
- * forwarded to the primary user.
+ * <p>This restriction has no effect on managed profiles.
*
* <p>Key for user restrictions.
* <p>Type: Boolean
diff --git a/core/java/android/provider/BlockedNumberContract.java b/core/java/android/provider/BlockedNumberContract.java
index ed7c7c5..b921351 100644
--- a/core/java/android/provider/BlockedNumberContract.java
+++ b/core/java/android/provider/BlockedNumberContract.java
@@ -239,20 +239,20 @@
public static class SystemContract {
/**
* A protected broadcast intent action for letting components with
- * {@link android.Manifest.permission#READ_BLOCKED_NUMBERS} know that the block suppressal
- * status as returned by {@link #getBlockSuppressalStatus(Context)} has been updated.
+ * {@link android.Manifest.permission#READ_BLOCKED_NUMBERS} know that the block suppression
+ * status as returned by {@link #getBlockSuppressionStatus(Context)} has been updated.
*/
- public static final String ACTION_BLOCK_SUPPRESSAL_STATE_CHANGED =
- "android.provider.action.BLOCK_SUPPRESSAL_STATE_CHANGED";
+ public static final String ACTION_BLOCK_SUPPRESSION_STATE_CHANGED =
+ "android.provider.action.BLOCK_SUPPRESSION_STATE_CHANGED";
public static final String METHOD_NOTIFY_EMERGENCY_CONTACT = "notify_emergency_contact";
- public static final String METHOD_END_BLOCK_SUPPRESSAL = "end_block_suppressal";
+ public static final String METHOD_END_BLOCK_SUPPRESSION = "end_block_suppression";
public static final String METHOD_SHOULD_SYSTEM_BLOCK_NUMBER = "should_system_block_number";
- public static final String METHOD_GET_BLOCK_SUPPRESSAL_STATUS =
- "get_block_suppresal_status";
+ public static final String METHOD_GET_BLOCK_SUPPRESSION_STATUS =
+ "get_block_suppression_status";
public static final String RES_IS_BLOCKING_SUPPRESSED = "blocking_suppressed";
@@ -264,7 +264,7 @@
* <p> This results in {@link #shouldSystemBlockNumber} returning {@code false} independent
* of the contents of the provider for a duration defined by
* {@link android.telephony.CarrierConfigManager#KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT}
- * the provider unless {@link #endBlockSuppressal(Context)} is called.
+ * the provider unless {@link #endBlockSuppression(Context)} is called.
*/
public static void notifyEmergencyContact(Context context) {
context.getContentResolver().call(
@@ -275,9 +275,9 @@
* Notifies the provider to disable suppressing blocking. If emergency services were not
* contacted recently at all, calling this method is a no-op.
*/
- public static void endBlockSuppressal(Context context) {
+ public static void endBlockSuppression(Context context) {
context.getContentResolver().call(
- AUTHORITY_URI, METHOD_END_BLOCK_SUPPRESSAL, null, null);
+ AUTHORITY_URI, METHOD_END_BLOCK_SUPPRESSION, null, null);
}
/**
@@ -292,26 +292,26 @@
return res != null && res.getBoolean(RES_NUMBER_IS_BLOCKED, false);
}
- public static BlockSuppressalStatus getBlockSuppressalStatus(Context context) {
+ public static BlockSuppressionStatus getBlockSuppressionStatus(Context context) {
final Bundle res = context.getContentResolver().call(
- AUTHORITY_URI, METHOD_GET_BLOCK_SUPPRESSAL_STATUS, null, null);
- return new BlockSuppressalStatus(res.getBoolean(RES_IS_BLOCKING_SUPPRESSED, false),
+ AUTHORITY_URI, METHOD_GET_BLOCK_SUPPRESSION_STATUS, null, null);
+ return new BlockSuppressionStatus(res.getBoolean(RES_IS_BLOCKING_SUPPRESSED, false),
res.getLong(RES_BLOCKING_SUPPRESSED_UNTIL_TIMESTAMP, 0));
}
/**
* Represents the current status of {@link #shouldSystemBlockNumber(Context, String)}. If
* emergency services have been contacted recently, {@link #isSuppressed} is {@code true},
- * and blocking is disabled until the timestamp {@link #untilTimestampMillis}.
+ * and blocking is disabled until the timestamp {@link #untilTimestampMillis}.
*/
- public static class BlockSuppressalStatus {
+ public static class BlockSuppressionStatus {
public final boolean isSuppressed;
/**
* Timestamp in milliseconds from epoch.
*/
public final long untilTimestampMillis;
- public BlockSuppressalStatus(boolean isSuppressed, long untilTimestampMillis) {
+ public BlockSuppressionStatus(boolean isSuppressed, long untilTimestampMillis) {
this.isSuppressed = isSuppressed;
this.untilTimestampMillis = untilTimestampMillis;
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 770cde7..0074788 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7043,6 +7043,7 @@
* Developer setting to enable WebView multiprocess rendering.
* @hide
*/
+ @SystemApi
public static final String WEBVIEW_MULTIPROCESS = "webview_multiprocess";
/**
diff --git a/core/java/android/security/net/config/ResourceCertificateSource.java b/core/java/android/security/net/config/ResourceCertificateSource.java
index 8803c4b..22fbee2 100644
--- a/core/java/android/security/net/config/ResourceCertificateSource.java
+++ b/core/java/android/security/net/config/ResourceCertificateSource.java
@@ -44,7 +44,7 @@
public ResourceCertificateSource(int resourceId, Context context) {
mResourceId = resourceId;
- mContext = context.getApplicationContext();
+ mContext = context;
}
private void ensureInitialized() {
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index 51e1f4b..e0c6770 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -1780,6 +1780,7 @@
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_SPACE:
+ case KeyEvent.KEYCODE_NUMPAD_ENTER:
return true;
default:
return false;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 0517788..673c609 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -3797,7 +3797,8 @@
return true;
} else if ((!mAttachInfo.mHasWindowFocus
&& !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) || mStopped
- || mIsAmbientMode || (mPausedForTransition && !isBack(q.mEvent))) {
+ || (mIsAmbientMode && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON))
+ || (mPausedForTransition && !isBack(q.mEvent))) {
// This is a focus event and the window doesn't currently have input focus or
// has stopped. This could be an event that came back from the previous stage
// but the window has lost focus or stopped in the meantime.
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 18dfdfd..6e02516 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -30,7 +30,7 @@
import android.util.Log;
import java.util.List;
-
+import java.util.Objects;
/**
* The interface that apps use to talk to the window manager.
@@ -1950,7 +1950,7 @@
// already have one.
packageName = o.packageName;
}
- if (o.mTitle != null) {
+ if (!Objects.equals(mTitle, o.mTitle) && o.mTitle != null) {
// NOTE: mTitle only copied if the originator set one.
mTitle = o.mTitle;
changes |= TITLE_CHANGED;
diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java
index a8b7a7b..e36077b 100644
--- a/core/java/android/view/WindowManagerPolicy.java
+++ b/core/java/android/view/WindowManagerPolicy.java
@@ -1362,4 +1362,11 @@
*/
public void getNonDecorInsetsLw(int displayRotation, int displayWidth, int displayHeight,
Rect outInsets);
+
+ /**
+ * @return True if a specified {@param dockSide} is allowed on the current device, or false
+ * otherwise. It is guaranteed that at least one dock side for a particular orientation
+ * is allowed, so for example, if DOCKED_RIGHT is not allowed, DOCKED_LEFT is allowed.
+ */
+ public boolean isDockSideAllowed(int dockSide);
}
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 2de9897..4c015ba 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -1245,15 +1245,9 @@
if (DEBUG) Log.v(TAG, "START INPUT: " + view + " ic="
+ ic + " tba=" + tba + " controlFlags=#"
+ Integer.toHexString(controlFlags));
- InputBindResult res;
- if (windowGainingFocus != null) {
- res = mService.windowGainedFocus(startInputReason, mClient, windowGainingFocus,
- controlFlags, softInputMode, windowFlags,
- tba, servedContext);
- } else {
- res = mService.startInput(startInputReason, mClient,
- servedContext, tba, controlFlags);
- }
+ final InputBindResult res = mService.startInputOrWindowGainedFocus(
+ startInputReason, mClient, windowGainingFocus, controlFlags, softInputMode,
+ windowFlags, tba, servedContext);
if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
if (res != null) {
if (res.id != null) {
@@ -1471,13 +1465,13 @@
return;
}
}
-
+
// For some reason we didn't do a startInput + windowFocusGain, so
// we'll just do a window focus gain and call it a day.
synchronized (mH) {
try {
if (DEBUG) Log.v(TAG, "Reporting focus gain, without startInput");
- mService.windowGainedFocus(
+ mService.startInputOrWindowGainedFocus(
InputMethodClient.START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient,
rootView.getWindowToken(), controlFlags, softInputMode, windowFlags, null,
null);
diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java
index 9a68593..34f3a47 100644
--- a/core/java/android/widget/AbsSeekBar.java
+++ b/core/java/android/widget/AbsSeekBar.java
@@ -694,7 +694,7 @@
final int halfH = h >= 0 ? h / 2 : 1;
mTickMark.setBounds(-halfW, -halfH, halfW, halfH);
- final int spacing = (getWidth() - mPaddingLeft - mPaddingRight) / count;
+ final float spacing = (getWidth() - mPaddingLeft - mPaddingRight) / (float) count;
final int saveCount = canvas.save();
canvas.translate(mPaddingLeft, getHeight() / 2);
for (int i = 0; i <= count; i++) {
diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java
index 4d707e3..3421790 100644
--- a/core/java/android/widget/Chronometer.java
+++ b/core/java/android/widget/Chronometer.java
@@ -19,15 +19,14 @@
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
-import android.os.Handler;
-import android.os.Message;
import android.os.SystemClock;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.util.Log;
-import android.view.accessibility.AccessibilityEvent;
import android.widget.RemoteViews.RemoteView;
+import com.android.internal.R;
+
import java.util.Formatter;
import java.util.IllegalFormatException;
import java.util.Locale;
@@ -37,11 +36,17 @@
* <p>
* You can give it a start time in the {@link SystemClock#elapsedRealtime} timebase,
* and it counts up from that, or if you don't give it a base time, it will use the
- * time at which you call {@link #start}. By default it will display the current
+ * time at which you call {@link #start}.
+ *
+ * <p>The timer can also count downward towards the base time by
+ * setting {@link #setCountDown(boolean)} to true.
+ *
+ * <p>By default it will display the current
* timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat}
* to format the timer value into an arbitrary string.
*
* @attr ref android.R.styleable#Chronometer_format
+ * @attr ref android.R.styleable#Chronometer_countDown
*/
@RemoteView
public class Chronometer extends TextView {
@@ -72,6 +77,7 @@
private StringBuilder mFormatBuilder;
private OnChronometerTickListener mOnChronometerTickListener;
private StringBuilder mRecycle = new StringBuilder(8);
+ private boolean mCountDown;
/**
* Initialize this Chronometer object.
@@ -102,7 +108,8 @@
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes);
- setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format));
+ setFormat(a.getString(R.styleable.Chronometer_format));
+ setCountDown(a.getBoolean(R.styleable.Chronometer_countDown, false));
a.recycle();
init();
@@ -114,6 +121,27 @@
}
/**
+ * Set this view to count down to the base instead of counting up from it.
+ *
+ * @param countDown whether this view should count down
+ *
+ * @see #setBase(long)
+ */
+ @android.view.RemotableViewMethod
+ public void setCountDown(boolean countDown) {
+ mCountDown = countDown;
+ }
+
+ /**
+ * @return whether this view counts down
+ *
+ * @see #setCountDown(boolean)
+ */
+ public boolean isCountDown() {
+ return mCountDown;
+ }
+
+ /**
* Set the time that the count-up timer is in reference to.
*
* @param base Use the {@link SystemClock#elapsedRealtime} time base.
@@ -226,9 +254,17 @@
private synchronized void updateText(long now) {
mNow = now;
- long seconds = now - mBase;
+ long seconds = mCountDown ? mBase - now : now - mBase;
seconds /= 1000;
+ boolean negative = false;
+ if (seconds < 0) {
+ seconds = -seconds;
+ negative = true;
+ }
String text = DateUtils.formatElapsedTime(mRecycle, seconds);
+ if (negative) {
+ text = getResources().getString(R.string.negative_duration, text);
+ }
if (mFormat != null) {
Locale loc = Locale.getDefault();
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 1826dd8..b2ef687 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -965,20 +965,21 @@
private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
final Layout layout = mTextView.getLayout();
if (layout == null) return offset;
- final CharSequence text = mTextView.getText();
- final int nextOffset = layout.getPaint().getTextRunCursor(text, 0, text.length(),
- layout.isRtlCharAt(offset) ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR,
- offset, findAfterGivenOffset ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE);
- return nextOffset == -1 ? offset : nextOffset;
+ return findAfterGivenOffset == layout.isRtlCharAt(offset) ?
+ layout.getOffsetToLeftOf(offset) : layout.getOffsetToRightOf(offset);
}
private long getCharClusterRange(int offset) {
final int textLength = mTextView.getText().length();
if (offset < textLength) {
- return TextUtils.packRangeInLong(offset, getNextCursorOffset(offset, true));
+ final int clusterEndOffset = getNextCursorOffset(offset, true);
+ return TextUtils.packRangeInLong(
+ getNextCursorOffset(clusterEndOffset, false), clusterEndOffset);
}
if (offset - 1 >= 0) {
- return TextUtils.packRangeInLong(getNextCursorOffset(offset, false), offset);
+ final int clusterStartOffset = getNextCursorOffset(offset, false);
+ return TextUtils.packRangeInLong(clusterStartOffset,
+ getNextCursorOffset(clusterStartOffset, true));
}
return TextUtils.packRangeInLong(offset, offset);
}
@@ -1089,7 +1090,7 @@
CharSequence selectedText = mTextView.getTransformedText(start, end);
ClipData data = ClipData.newPlainText(null, selectedText);
DragLocalState localState = new DragLocalState(mTextView, start, end);
- mTextView.startDragAndDrop(data, getTextThumbnailBuilder(selectedText), localState,
+ mTextView.startDragAndDrop(data, getTextThumbnailBuilder(start, end), localState,
View.DRAG_FLAG_GLOBAL);
stopTextActionMode();
if (hasSelectionController()) {
@@ -2296,7 +2297,7 @@
}
}
- private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
+ private DragShadowBuilder getTextThumbnailBuilder(int start, int end) {
TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
com.android.internal.R.layout.text_drag_thumbnail, null);
@@ -2304,9 +2305,11 @@
throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
}
- if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
- text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
+ if (end - start > DRAG_SHADOW_MAX_TEXT_LENGTH) {
+ final long range = getCharClusterRange(start + DRAG_SHADOW_MAX_TEXT_LENGTH);
+ end = TextUtils.unpackRangeEndFromLong(range);
}
+ final CharSequence text = mTextView.getTransformedText(start, end);
shadowView.setText(text);
shadowView.setTextColor(mTextView.getTextColors());
diff --git a/core/java/com/android/internal/util/MessageUtils.java b/core/java/com/android/internal/util/MessageUtils.java
index 1014bfd..184245e 100644
--- a/core/java/com/android/internal/util/MessageUtils.java
+++ b/core/java/com/android/internal/util/MessageUtils.java
@@ -21,6 +21,7 @@
import android.util.SparseArray;
import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
/**
* Static utility class for dealing with {@link Message} objects.
@@ -62,8 +63,12 @@
}
for (Field field : fields) {
- String name = field.getName();
+ int modifiers = field.getModifiers();
+ if (!Modifier.isStatic(modifiers) | !Modifier.isFinal(modifiers)) {
+ continue;
+ }
+ String name = field.getName();
for (String prefix : prefixes) {
// Does this look like a constant?
if (!name.startsWith(prefix)) {
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index db3ecc6..5576f13 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -46,17 +46,14 @@
in IInputContext inputContext, int uid, int pid);
void removeClient(in IInputMethodClient client);
- InputBindResult startInput(/* @InputMethodClient.StartInputReason */ int startInputReason,
- in IInputMethodClient client, IInputContext inputContext, in EditorInfo attribute,
- int controlFlags);
void finishInput(in IInputMethodClient client);
boolean showSoftInput(in IInputMethodClient client, int flags,
in ResultReceiver resultReceiver);
boolean hideSoftInput(in IInputMethodClient client, int flags,
in ResultReceiver resultReceiver);
- // Report that a window has gained focus. If 'attribute' is non-null,
- // this will also do a startInput.
- InputBindResult windowGainedFocus(
+ // If windowToken is null, this just does startInput(). Otherwise this reports that a window
+ // has gained focus, and if 'attribute' is non-null then also does startInput.
+ InputBindResult startInputOrWindowGainedFocus(
/* @InputMethodClient.StartInputReason */ int startInputReason,
in IInputMethodClient client, in IBinder windowToken, int controlFlags,
int softInputMode, int windowFlags, in EditorInfo attribute,
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index eeff00f..30d6f29 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -296,10 +296,6 @@
<protected-broadcast android:name="android.intent.action.AIRPLANE_MODE" />
<protected-broadcast android:name="android.intent.action.ADVANCED_SETTINGS" />
<protected-broadcast android:name="android.intent.action.APPLICATION_RESTRICTIONS_CHANGED" />
- <protected-broadcast android:name="android.intent.action.BUGREPORT_STARTED" />
- <protected-broadcast android:name="android.intent.action.BUGREPORT_FINISHED" />
- <protected-broadcast android:name="android.intent.action.REMOTE_BUGREPORT_FINISHED" />
- <protected-broadcast android:name="android.intent.action.REMOTE_BUGREPORT_DISPATCH" />
<!-- Legacy -->
<protected-broadcast android:name="android.intent.action.ACTION_IDLE_MAINTENANCE_START" />
@@ -893,77 +889,77 @@
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.READ_PROFILE"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.WRITE_PROFILE"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.READ_SOCIAL_STREAM"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.WRITE_SOCIAL_STREAM"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.READ_USER_DICTIONARY"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.WRITE_USER_DICTIONARY"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.WRITE_SMS"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.MANAGE_ACCOUNTS"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.USE_CREDENTIALS"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.SUBSCRIBED_FEEDS_READ"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.SUBSCRIBED_FEEDS_WRITE"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- @hide We need to keep this around for backwards compatibility -->
<permission android:name="android.permission.FLASHLIGHT"
android:protectionLevel="normal"
- android:permissionFlags="hidden"/>
+ android:permissionFlags="removed"/>
<!-- ====================================================================== -->
<!-- INSTALL PERMISSIONS -->
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 4480944..2ab95412 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -3539,6 +3539,9 @@
If no format string is specified, the Chronometer will simply display
"MM:SS" or "H:MM:SS". -->
<attr name="format" format="string" localization="suggested" />
+ <!-- Specifies whether this Chronometer counts down or counts up from the base.
+ If not specified this is false and the Chronometer counts up. -->
+ <attr name="countDown" format="boolean" />
</declare-styleable>
<declare-styleable name="CompoundButton">
<!-- Indicates the initial checked state of this button. -->
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 4b81987..d0d1d5a 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -248,9 +248,12 @@
may cost the user money. Such permissions may be highlighted
when shown to the user with this additional information. -->
<flag name="costsMoney" value="0x0001" />
- <!-- Additional flag from base permission type: this permission is hidden
- and should not show in the UI. -->
- <flag name="hidden" value="0x2" />
+ <!-- Additional flag from base permission type: this permission has been
+ removed and it is no longer enforced. It shouldn't be shown in the
+ UI. Removed permissions are kept as normal permissions for backwards
+ compatibility as apps may be checking them before calling an API.
+ -->
+ <flag name="removed" value="0x2" />
</attr>
<!-- Specified the name of a group that this permission is associated
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 13812630..dbaa737 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2702,6 +2702,7 @@
<public type="attr" name="hotSpotY" />
<public type="attr" name="version" />
<public type="attr" name="backupInForeground" />
+ <public type="attr" name="countDown" />
<public type="style" name="Theme.Material.Light.DialogWhenLarge.DarkActionBar" />
<public type="style" name="Widget.Material.SeekBar.Discrete" />
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index da88146..3e9a6ca 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4230,4 +4230,7 @@
<!-- View application info for a target. -->
<string name="app_info">App info</string>
+ <!-- The representation of a time duration when negative. An example is -1:14. This can be used with a countdown timer for example.-->
+ <string name="negative_duration">\u2212<xliff:g id="time" example="1:14">%1$s</xliff:g></string>
+
</resources>
diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml
index 9efcfda..db418f7 100644
--- a/core/res/res/values/styles_material.xml
+++ b/core/res/res/values/styles_material.xml
@@ -996,7 +996,6 @@
<item name="alpha">.87</item>
<item name="textColor">@color/black</item>
<item name="drawablePadding">8dip</item>
- <item name="ellipsize">marquee</item>
<item name="gravity">start|center_vertical</item>
<item name="layout_gravity">start|center_vertical</item>
<item name="layout_height">48dip</item>
@@ -1017,7 +1016,6 @@
<item name="alpha">.87</item>
<item name="textColor">#009688</item>
<item name="drawablePadding">8dip</item>
- <item name="ellipsize">marquee</item>
<item name="gravity">start|center_vertical</item>
<item name="layout_gravity">start|center_vertical</item>
<item name="layout_height">48dip</item>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 9950884..a7a9292 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2541,4 +2541,5 @@
<java-symbol type="string" name="carrier_app_dialog_not_now" />
<java-symbol type="string" name="carrier_app_notification_title" />
<java-symbol type="string" name="carrier_app_notification_text" />
+ <java-symbol type="string" name="negative_duration" />
</resources>
diff --git a/core/tests/utiltests/src/com/android/internal/util/MessageUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/MessageUtilsTest.java
new file mode 100644
index 0000000..32b969a
--- /dev/null
+++ b/core/tests/utiltests/src/com/android/internal/util/MessageUtilsTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.util;
+
+import static org.junit.Assert.*;
+
+import com.android.internal.util.MessageUtils;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.SparseArray;
+
+import org.junit.Test;
+
+
+class A {
+ // Should not see these.
+ private int mMember;
+ public final int CMD_NOT_STATIC = 7;
+ private static final String CMD_NOT_INT = "not an integer";
+ public static int CMD_NOT_FINAL = 34;
+ public static final int kWrongPrefix = 99;
+
+ // Should see these.
+ private static final int CMD_DO_SOMETHING = 12;
+ public static final int EVENT_SOMETHING_HAPPENED = 45;
+}
+
+class B {
+ public static final int CMD_FOO = 56;
+ public static final int EVENT_BAR = 55;
+ public static final int NOTIFICATION_BAZ = 12;
+}
+
+/**
+ * Unit tests for {@link com.android.util.MessageUtils}.
+ */
+@SmallTest
+public class MessageUtilsTest {
+
+ private static final Class[] CLASSES = { A.class, B.class };
+
+ private SparseArray<String> makeSparseArray(int[] keys, String[] values) throws Exception {
+ assertEquals("Must specify same number of keys and values", keys.length, values.length);
+ SparseArray<String> out = new SparseArray<>();
+ for (int i = 0; i < keys.length; i++) {
+ out.put(keys[i], values[i]);
+ }
+ return out;
+ }
+
+ private void assertSparseArrayEquals(
+ SparseArray<String> a1, SparseArray<String> a2) throws Exception {
+ String msg = String.format("%s != %s", a1.toString(), a2.toString());
+ assertEquals(msg, a1.size(), a2.size());
+ int size = a1.size();
+ for (int i = 0; i < size; i++) {
+ assertEquals(msg, a1.keyAt(i), a2.keyAt(i));
+ assertEquals(msg, a1.valueAt(i), a2.valueAt(i));
+ }
+ }
+
+ @Test
+ public void basicOperation() throws Exception {
+ SparseArray<String> expected = makeSparseArray(
+ new int[]{12, 45, 55, 56},
+ new String[]{"CMD_DO_SOMETHING", "EVENT_SOMETHING_HAPPENED", "EVENT_BAR", "CMD_FOO"});
+ assertSparseArrayEquals(expected, MessageUtils.findMessageNames(CLASSES));
+ }
+
+ @Test
+ public void withPrefixes() throws Exception {
+ SparseArray<String> expected = makeSparseArray(
+ new int[]{45, 55},
+ new String[]{"EVENT_SOMETHING_HAPPENED", "EVENT_BAR"});
+ assertSparseArrayEquals(expected, MessageUtils.findMessageNames(CLASSES,
+ new String[]{"EVENT_"}));
+ }
+
+ @Test(expected=MessageUtils.DuplicateConstantError.class)
+ public void duplicateConstants() {
+ MessageUtils.findMessageNames(CLASSES, new String[]{"CMD_", "NOTIFICATION_"});
+ }
+
+}
diff --git a/docs/html/preview/behavior-changes.jd b/docs/html/preview/behavior-changes.jd
index cab4163..264e741 100644
--- a/docs/html/preview/behavior-changes.jd
+++ b/docs/html/preview/behavior-changes.jd
@@ -393,7 +393,7 @@
only affects notifications generated by applications in the managed profile.</li>
</ul>
</li>
- <li>The {@link android.app.admin.DevicePolicyManager#createAndInitializeUser(android.content.ComponentName, java.lang.String, java.lang.String, android.content.ComponentName, android.os.Bundle) createAndInitializeUser()} and {@link android.app.admin.DevicePolicyManager#createUser(android.content.ComponentName, java.lang.String) createUser()} methods have been deprecated.</li>
+ <li>The {@link android.app.admin.DevicePolicyManager#createAndManageUser(android.content.ComponentName, java.lang.String, android.content.ComponentName, android.os.PersistableBundle, int) createAndManageUser()} method replaces createAndInitializeUser(), which has been removed.</li>
<li>The {@link android.app.admin.DevicePolicyManager#setScreenCaptureDisabled(android.content.ComponentName, boolean) setScreenCaptureDisabled()}
method now also blocks the assist structure when an app of the given user is in the foreground. </li>
<li>{@link android.app.admin.DevicePolicyManager#EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM}
diff --git a/libs/hwui/Animator.cpp b/libs/hwui/Animator.cpp
index 294edb6..bd71e0d 100644
--- a/libs/hwui/Animator.cpp
+++ b/libs/hwui/Animator.cpp
@@ -160,6 +160,10 @@
}
if (!mStagingRequests.empty()) {
+ // No interpolator was set, use the default
+ if (mPlayState == PlayState::NotStarted && !mInterpolator) {
+ mInterpolator.reset(Interpolator::createDefaultInterpolator());
+ }
// Keep track of the play state and play time before they are changed when
// staging requests are resolved.
nsecs_t currentPlayTime = mPlayTime;
@@ -222,10 +226,6 @@
// Set to 0 so that the animate() basically instantly finishes
mStartTime = 0;
}
- // No interpolator was set, use the default
- if (!mInterpolator) {
- mInterpolator.reset(Interpolator::createDefaultInterpolator());
- }
if (mDuration < 0 || mDuration > 50000) {
ALOGW("Your duration is strange and confusing: %" PRId64, mDuration);
}
diff --git a/libs/hwui/BakedOpDispatcher.cpp b/libs/hwui/BakedOpDispatcher.cpp
index f83e1fa..78764b5 100644
--- a/libs/hwui/BakedOpDispatcher.cpp
+++ b/libs/hwui/BakedOpDispatcher.cpp
@@ -334,15 +334,15 @@
}
static void renderPathTexture(BakedOpRenderer& renderer, const BakedOpState& state,
- PathTexture& texture, const RecordedOp& op) {
+ float xOffset, float yOffset, PathTexture& texture, const SkPaint& paint) {
Rect dest(texture.width(), texture.height());
- dest.translate(texture.left - texture.offset,
- texture.top - texture.offset);
+ dest.translate(xOffset + texture.left - texture.offset,
+ yOffset + texture.top - texture.offset);
Glop glop;
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
.setRoundRectClipState(state.roundRectClipState)
.setMeshTexturedUnitQuad(nullptr)
- .setFillPathTexturePaint(texture, *(op.paint), state.alpha)
+ .setFillPathTexturePaint(texture, paint, state.alpha)
.setTransform(state.computedState.transform, TransformFlags::None)
.setModelViewMapUnitToRect(dest)
.build();
@@ -368,7 +368,8 @@
op.startAngle, op.sweepAngle, op.useCenter, op.paint);
const AutoTexture holder(texture);
if (CC_LIKELY(holder.texture)) {
- renderPathTexture(renderer, state, *texture, op);
+ renderPathTexture(renderer, state, op.unmappedBounds.left, op.unmappedBounds.right,
+ *texture, *(op.paint));
}
} else {
SkRect rect = getBoundsOfFill(op);
@@ -519,7 +520,8 @@
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.paint);
const AutoTexture holder(texture);
if (CC_LIKELY(holder.texture)) {
- renderPathTexture(renderer, state, *texture, op);
+ renderPathTexture(renderer, state, op.unmappedBounds.left, op.unmappedBounds.right,
+ *texture, *(op.paint));
}
} else {
SkPath path;
@@ -562,7 +564,9 @@
PathTexture* texture = renderer.caches().pathCache.get(op.path, op.paint);
const AutoTexture holder(texture);
if (CC_LIKELY(holder.texture)) {
- renderPathTexture(renderer, state, *texture, op);
+ // Unlike other callers to renderPathTexture, no offsets are used because PathOp doesn't
+ // have any translate built in, other than what's in the SkPath itself
+ renderPathTexture(renderer, state, 0, 0, *texture, *(op.paint));
}
}
@@ -588,7 +592,8 @@
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.paint);
const AutoTexture holder(texture);
if (CC_LIKELY(holder.texture)) {
- renderPathTexture(renderer, state, *texture, op);
+ renderPathTexture(renderer, state, op.unmappedBounds.left, op.unmappedBounds.top,
+ *texture, *(op.paint));
}
} else {
SkPath path;
@@ -622,7 +627,8 @@
op.rx, op.ry, op.paint);
const AutoTexture holder(texture);
if (CC_LIKELY(holder.texture)) {
- renderPathTexture(renderer, state, *texture, op);
+ renderPathTexture(renderer, state, op.unmappedBounds.left, op.unmappedBounds.top,
+ *texture, *(op.paint));
}
} else {
const VertexBuffer* buffer = renderer.caches().tessellationCache.getRoundRect(
diff --git a/libs/hwui/LayerBuilder.cpp b/libs/hwui/LayerBuilder.cpp
index 1ba3bf2..c5d7191 100644
--- a/libs/hwui/LayerBuilder.cpp
+++ b/libs/hwui/LayerBuilder.cpp
@@ -239,7 +239,7 @@
// put the verts in the frame allocator, since
// 1) SimpleRectsOps needs verts, not rects
// 2) even if mClearRects stored verts, std::vectors will move their contents
- Vertex* const verts = (Vertex*) allocator.alloc<Vertex>(vertCount * sizeof(Vertex));
+ Vertex* const verts = (Vertex*) allocator.create_trivial_array<Vertex>(vertCount);
Vertex* currentVert = verts;
Rect bounds = mClearRects[0];
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index 0adb21c..78a0b13 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -290,7 +290,7 @@
void RecordingCanvas::drawSimpleRects(const float* rects, int vertexCount, const SkPaint* paint) {
if (rects == nullptr) return;
- Vertex* rectData = (Vertex*) mDisplayList->allocator.alloc<Vertex>(vertexCount * sizeof(Vertex));
+ Vertex* rectData = (Vertex*) mDisplayList->allocator.create_trivial_array<Vertex>(vertexCount);
Vertex* vertex = rectData;
float left = FLT_MAX;
diff --git a/libs/hwui/Snapshot.h b/libs/hwui/Snapshot.h
index 0ac2f14..b03643f 100644
--- a/libs/hwui/Snapshot.h
+++ b/libs/hwui/Snapshot.h
@@ -44,7 +44,7 @@
*/
class RoundRectClipState {
public:
- /** static void* operator new(size_t size); PURPOSELY OMITTED, allocator only **/
+ static void* operator new(size_t size) = delete;
static void* operator new(size_t size, LinearAllocator& allocator) {
return allocator.alloc<RoundRectClipState>(size);
}
@@ -65,7 +65,7 @@
class ProjectionPathMask {
public:
- /** static void* operator new(size_t size); PURPOSELY OMITTED, allocator only **/
+ static void* operator new(size_t size) = delete;
static void* operator new(size_t size, LinearAllocator& allocator) {
return allocator.alloc<ProjectionPathMask>(size);
}
diff --git a/libs/hwui/utils/LinearAllocator.h b/libs/hwui/utils/LinearAllocator.h
index 0a0e185..34c8c6b 100644
--- a/libs/hwui/utils/LinearAllocator.h
+++ b/libs/hwui/utils/LinearAllocator.h
@@ -84,6 +84,13 @@
return new (allocImpl(sizeof(T))) T(std::forward<Params>(params)...);
}
+ template<class T>
+ T* create_trivial_array(int count) {
+ static_assert(std::is_trivially_destructible<T>::value,
+ "Error, called create_trivial_array on a non-trivial type");
+ return reinterpret_cast<T*>(allocImpl(sizeof(T) * count));
+ }
+
/**
* Attempt to deallocate the given buffer, with the LinearAllocator attempting to rewind its
* state if possible.
diff --git a/media/java/android/media/AudioDeviceInfo.java b/media/java/android/media/AudioDeviceInfo.java
index 45529ef..9922b72 100644
--- a/media/java/android/media/AudioDeviceInfo.java
+++ b/media/java/android/media/AudioDeviceInfo.java
@@ -111,6 +111,10 @@
* A device type connected over IP.
*/
public static final int TYPE_IP = 20;
+ /**
+ * A type-agnostic device used for communication with external audio systems
+ */
+ public static final int TYPE_BUS = 21;
private final AudioDevicePort mPort;
@@ -279,6 +283,7 @@
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_FM, TYPE_FM);
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_AUX_LINE, TYPE_AUX_LINE);
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_IP, TYPE_IP);
+ INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_BUS, TYPE_BUS);
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_IN_BUILTIN_MIC, TYPE_BUILTIN_MIC);
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET, TYPE_BLUETOOTH_SCO);
@@ -296,6 +301,7 @@
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_IN_SPDIF, TYPE_LINE_DIGITAL);
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, TYPE_BLUETOOTH_A2DP);
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_IN_IP, TYPE_IP);
+ INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_IN_BUS, TYPE_BUS);
// not covered here, legacy
//AudioSystem.DEVICE_OUT_REMOTE_SUBMIX
@@ -323,6 +329,7 @@
EXT_TO_INT_DEVICE_MAPPING.put(TYPE_TELEPHONY, AudioSystem.DEVICE_OUT_TELEPHONY_TX);
EXT_TO_INT_DEVICE_MAPPING.put(TYPE_AUX_LINE, AudioSystem.DEVICE_OUT_AUX_LINE);
EXT_TO_INT_DEVICE_MAPPING.put(TYPE_IP, AudioSystem.DEVICE_OUT_IP);
+ EXT_TO_INT_DEVICE_MAPPING.put(TYPE_BUS, AudioSystem.DEVICE_OUT_BUS);
}
}
diff --git a/media/java/android/media/AudioFormat.aidl b/media/java/android/media/AudioFormat.aidl
new file mode 100644
index 0000000..8613f55
--- /dev/null
+++ b/media/java/android/media/AudioFormat.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2016 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.media;
+
+parcelable AudioFormat;
diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java
index 000a56d..22f4f04 100644
--- a/media/java/android/media/AudioFormat.java
+++ b/media/java/android/media/AudioFormat.java
@@ -18,10 +18,13 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
+import java.util.Objects;
/**
* The {@link AudioFormat} class is used to access a number of audio format and
@@ -209,7 +212,7 @@
* AudioTrack.getPlaybackHeadPosition()}),
* depending on the context where audio frame is used.
*/
-public class AudioFormat {
+public class AudioFormat implements Parcelable {
//---------------------------------------------------------
// Constants
@@ -874,6 +877,44 @@
}
@Override
+ public int hashCode() {
+ return Objects.hash(mPropertySetMask, mSampleRate, mEncoding, mChannelMask,
+ mChannelIndexMask);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mPropertySetMask);
+ dest.writeInt(mEncoding);
+ dest.writeInt(mSampleRate);
+ dest.writeInt(mChannelMask);
+ dest.writeInt(mChannelIndexMask);
+ }
+
+ private AudioFormat(Parcel in) {
+ mPropertySetMask = in.readInt();
+ mEncoding = in.readInt();
+ mSampleRate = in.readInt();
+ mChannelMask = in.readInt();
+ mChannelIndexMask = in.readInt();
+ }
+
+ public static final Parcelable.Creator<AudioFormat> CREATOR =
+ new Parcelable.Creator<AudioFormat>() {
+ public AudioFormat createFromParcel(Parcel p) {
+ return new AudioFormat(p);
+ }
+ public AudioFormat[] newArray(int size) {
+ return new AudioFormat[size];
+ }
+ };
+
+ @Override
public String toString () {
return new String("AudioFormat:"
+ " props=" + mPropertySetMask
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index aa0d78d..b3f73be 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -346,6 +346,7 @@
public static final int DEVICE_OUT_AUX_LINE = 0x200000;
public static final int DEVICE_OUT_SPEAKER_SAFE = 0x400000;
public static final int DEVICE_OUT_IP = 0x800000;
+ public static final int DEVICE_OUT_BUS = 0x1000000;
public static final int DEVICE_OUT_DEFAULT = DEVICE_BIT_DEFAULT;
@@ -373,6 +374,7 @@
DEVICE_OUT_AUX_LINE |
DEVICE_OUT_SPEAKER_SAFE |
DEVICE_OUT_IP |
+ DEVICE_OUT_BUS |
DEVICE_OUT_DEFAULT);
public static final int DEVICE_OUT_ALL_A2DP = (DEVICE_OUT_BLUETOOTH_A2DP |
DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES |
@@ -412,6 +414,7 @@
public static final int DEVICE_IN_BLUETOOTH_A2DP = DEVICE_BIT_IN | 0x20000;
public static final int DEVICE_IN_LOOPBACK = DEVICE_BIT_IN | 0x40000;
public static final int DEVICE_IN_IP = DEVICE_BIT_IN | 0x80000;
+ public static final int DEVICE_IN_BUS = DEVICE_BIT_IN | 0x100000;
public static final int DEVICE_IN_DEFAULT = DEVICE_BIT_IN | DEVICE_BIT_DEFAULT;
public static final int DEVICE_IN_ALL = (DEVICE_IN_COMMUNICATION |
@@ -434,6 +437,7 @@
DEVICE_IN_BLUETOOTH_A2DP |
DEVICE_IN_LOOPBACK |
DEVICE_IN_IP |
+ DEVICE_IN_BUS |
DEVICE_IN_DEFAULT);
public static final int DEVICE_IN_ALL_SCO = DEVICE_IN_BLUETOOTH_SCO_HEADSET;
public static final int DEVICE_IN_ALL_USB = (DEVICE_IN_USB_ACCESSORY |
@@ -469,6 +473,7 @@
public static final String DEVICE_OUT_AUX_LINE_NAME = "aux_line";
public static final String DEVICE_OUT_SPEAKER_SAFE_NAME = "speaker_safe";
public static final String DEVICE_OUT_IP_NAME = "ip";
+ public static final String DEVICE_OUT_BUS_NAME = "bus";
public static final String DEVICE_IN_COMMUNICATION_NAME = "communication";
public static final String DEVICE_IN_AMBIENT_NAME = "ambient";
@@ -490,6 +495,7 @@
public static final String DEVICE_IN_BLUETOOTH_A2DP_NAME = "bt_a2dp";
public static final String DEVICE_IN_LOOPBACK_NAME = "loopback";
public static final String DEVICE_IN_IP_NAME = "ip";
+ public static final String DEVICE_IN_BUS_NAME = "bus";
public static String getOutputDeviceName(int device)
{
@@ -542,6 +548,8 @@
return DEVICE_OUT_SPEAKER_SAFE_NAME;
case DEVICE_OUT_IP:
return DEVICE_OUT_IP_NAME;
+ case DEVICE_OUT_BUS:
+ return DEVICE_OUT_BUS_NAME;
case DEVICE_OUT_DEFAULT:
default:
return Integer.toString(device);
@@ -591,6 +599,8 @@
return DEVICE_IN_LOOPBACK_NAME;
case DEVICE_IN_IP:
return DEVICE_IN_IP_NAME;
+ case DEVICE_IN_BUS:
+ return DEVICE_IN_BUS_NAME;
case DEVICE_IN_DEFAULT:
default:
return Integer.toString(device);
diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java
index 1c11842..3e6aa9f 100644
--- a/media/java/android/media/tv/TvContract.java
+++ b/media/java/android/media/tv/TvContract.java
@@ -53,6 +53,12 @@
/** The authority for the TV provider. */
public static final String AUTHORITY = "android.media.tv";
+ /**
+ * Permission to read TV listings. This is required to read all the TV channel and program
+ * information available on the system.
+ */
+ public static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
+
private static final String PATH_CHANNEL = "channel";
private static final String PATH_PROGRAM = "program";
private static final String PATH_RECORDED_PROGRAM = "recorded_program";
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 58e7709..637e06e 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.documentsui">
+ <uses-permission android:name="android.permission.GET_APP_GRANTED_URI_PERMISSIONS" />
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
<uses-permission android:name="android.permission.REMOVE_TASKS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
index b67a6915..845e32c 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -23,9 +23,11 @@
import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_NONE;
import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_SIDE;
import static com.android.internal.util.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
import android.app.Activity;
import android.app.Fragment;
+import android.app.FragmentManager;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
@@ -46,7 +48,7 @@
import android.view.MenuItem;
import android.widget.Spinner;
-import com.android.documentsui.SearchManager.SearchManagerListener;
+import com.android.documentsui.SearchViewManager.SearchManagerListener;
import com.android.documentsui.State.ViewMode;
import com.android.documentsui.dirlist.DirectoryFragment;
import com.android.documentsui.dirlist.Model;
@@ -64,14 +66,12 @@
public abstract class BaseActivity extends Activity
implements SearchManagerListener, NavigationView.Environment {
- static final String EXTRA_STATE = "state";
-
// See comments where this const is referenced for details.
private static final int DRAWER_NO_FIDDLE_DELAY = 1500;
State mState;
RootsCache mRoots;
- SearchManager mSearchManager;
+ SearchViewManager mSearchManager;
DrawerController mDrawer;
NavigationView mNavigator;
@@ -121,7 +121,7 @@
}
});
- mSearchManager = new SearchManager(this);
+ mSearchManager = new SearchViewManager(this, icicle);
DocumentsToolbar toolbar = (DocumentsToolbar) findViewById(R.id.toolbar);
setActionBar(toolbar);
@@ -141,6 +141,7 @@
boolean showMenu = super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.activity, menu);
+ mNavigator.update();
mSearchManager.install((DocumentsToolbar) findViewById(R.id.toolbar));
return showMenu;
@@ -188,7 +189,7 @@
private State getState(@Nullable Bundle icicle) {
if (icicle != null) {
- State state = icicle.<State>getParcelable(EXTRA_STATE);
+ State state = icicle.<State>getParcelable(Shared.EXTRA_STATE);
if (DEBUG) Log.d(mTag, "Recovered existing state object: " + state);
return state;
}
@@ -224,6 +225,9 @@
}
void onRootPicked(RootInfo root) {
+ // Clicking on the current root removes search
+ mSearchManager.cancelSearch();
+
// Skip refreshing if root nor directory didn't change
if (root.equals(getCurrentRoot()) && mState.stack.size() == 1) {
return;
@@ -233,7 +237,6 @@
// Clear entire backstack and start in new root
mState.onRootChanged(root);
- mSearchManager.update(root);
// Recents is always in memory, so we just load it directly.
// Otherwise we delegate loading data from disk to a task
@@ -314,6 +317,12 @@
CreateDirectoryFragment.show(getFragmentManager());
}
+ void onDirectoryCreated(DocumentInfo doc) {
+ // By default we do nothing, just let the new directory appear.
+ // DocumentsActivity auto-opens directories after creating them
+ // As that is more attuned to the "picker" use cases it supports.
+ }
+
/**
* Returns true if a directory can be created in the current location.
* @return
@@ -328,11 +337,6 @@
&& !root.isDownloads();
}
- void onDirectoryCreated(DocumentInfo doc) {
- checkArgument(doc.isDirectory());
- openContainerDocument(doc);
- }
-
void openContainerDocument(DocumentInfo doc) {
checkArgument(doc.isContainer());
mState.pushDocument(doc);
@@ -370,18 +374,18 @@
* e.g. The current directory name displayed on the action bar won't get updated.
*/
@Override
- public void onSearchChanged() {
- refreshDirectory(ANIM_NONE);
+ public void onSearchChanged(@Nullable String query) {
+ // We should not get here if root is not searchable
+ checkState(canSearchRoot());
+ reloadSearch(query);
}
- /**
- * Called when search query changed.
- * Updates the state object.
- * @param query - New query
- */
- @Override
- public void onSearchQueryChanged(String query) {
- mState.currentSearch = query;
+ private void reloadSearch(String query) {
+ FragmentManager fm = getFragmentManager();
+ RootInfo root = getCurrentRoot();
+ DocumentInfo cwd = getCurrentDirectory();
+
+ DirectoryFragment.reloadSearch(fm, root, cwd, query);
}
final List<String> getExcludedAuthorities() {
@@ -486,7 +490,8 @@
@Override
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
- state.putParcelable(EXTRA_STATE, mState);
+ state.putParcelable(Shared.EXTRA_STATE, mState);
+ mSearchManager.onSaveInstanceState(state);
}
@Override
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
index b0542b9..13b7b14 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
@@ -53,19 +53,22 @@
private final RootInfo mRoot;
private final Uri mUri;
private final int mUserSortOrder;
+ private final boolean mSearchMode;
private DocumentInfo mDoc;
private CancellationSignal mSignal;
private DirectoryResult mResult;
+
public DirectoryLoader(Context context, int type, RootInfo root, DocumentInfo doc, Uri uri,
- int userSortOrder) {
+ int userSortOrder, boolean inSearchMode) {
super(context, ProviderExecutor.forAuthority(root.authority));
mType = type;
mRoot = root;
mUri = uri;
mUserSortOrder = userSortOrder;
mDoc = doc;
+ mSearchMode = inSearchMode;
}
@Override
@@ -83,7 +86,7 @@
final DirectoryResult result = new DirectoryResult();
// Use default document when searching
- if (mType == DirectoryFragment.TYPE_SEARCH) {
+ if (mSearchMode) {
final Uri docUri = DocumentsContract.buildDocumentUri(
mRoot.authority, mRoot.documentId);
try {
@@ -106,7 +109,7 @@
}
// Search always uses ranking from provider
- if (mType == DirectoryFragment.TYPE_SEARCH) {
+ if (mSearchMode) {
result.sortOrder = State.SORT_ORDER_UNKNOWN;
}
@@ -127,7 +130,7 @@
cursor = new RootCursorWrapper(mUri.getAuthority(), mRoot.rootId, cursor, -1);
- if (mType == DirectoryFragment.TYPE_SEARCH) {
+ if (mSearchMode) {
// Filter directories out of search results, for now
cursor = new FilteringCursorWrapper(cursor, null, SEARCH_REJECT_MIMES);
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index ec7dde9..6ecdc90 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -23,6 +23,7 @@
import static com.android.documentsui.State.ACTION_OPEN_TREE;
import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION;
import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_NONE;
+import static com.android.internal.util.Preconditions.checkArgument;
import android.app.Activity;
import android.app.Fragment;
@@ -290,13 +291,8 @@
mState.derivedMode = visualMimes ? State.MODE_GRID : State.MODE_LIST;
}
} else {
- if (mSearchManager.isSearching()) {
- // Ongoing search
- DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim);
- } else {
// Normal boring directory
DirectoryFragment.showDirectory(fm, root, cwd, anim);
- }
}
// Forget any replacement target
@@ -321,6 +317,12 @@
.executeOnExecutor(getExecutorForCurrentDirectory());
}
+ @Override
+ void onDirectoryCreated(DocumentInfo doc) {
+ checkArgument(doc.isDirectory());
+ openContainerDocument(doc);
+ }
+
void onSaveRequested(String mimeType, String displayName) {
new CreateFinishTask(this, mimeType, displayName)
.executeOnExecutor(getExecutorForCurrentDirectory());
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DownloadsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DownloadsActivity.java
index f7a45e2..9609dee 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DownloadsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DownloadsActivity.java
@@ -16,8 +16,10 @@
package com.android.documentsui;
+import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.State.ACTION_MANAGE;
import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_NONE;
+import static com.android.internal.util.Preconditions.checkState;
import android.app.Activity;
import android.app.Fragment;
@@ -119,17 +121,14 @@
final RootInfo root = getCurrentRoot();
final DocumentInfo cwd = getCurrentDirectory();
+ if (DEBUG) checkState(!mSearchManager.isSearching());
+
// If started in manage roots mode, there has to be a cwd (i.e. the root dir of the managed
// root).
Preconditions.checkNotNull(cwd);
- if (mState.currentSearch != null) {
- // Ongoing search
- DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim);
- } else {
- // Normal boring directory
- DirectoryFragment.showDirectory(fm, root, cwd, anim);
- }
+ // Normal boring directory
+ DirectoryFragment.showDirectory(fm, root, cwd, anim);
}
@Override
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
index c0faba3..8b42ac5 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
@@ -33,7 +33,6 @@
import android.os.Bundle;
import android.os.Parcelable;
import android.provider.DocumentsContract;
-import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.util.Log;
import android.view.KeyEvent;
@@ -82,7 +81,6 @@
if (mState.restored) {
if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData());
- refreshCurrentRootAndDirectory(ANIM_NONE);
} else if (!mState.stack.isEmpty()) {
// If a non-empty stack is present in our state it was read (presumably)
// from EXTRA_STACK intent extra. In this case, we'll skip other means of
@@ -248,16 +246,13 @@
final RootInfo root = getCurrentRoot();
final DocumentInfo cwd = getCurrentDirectory();
+ if (DEBUG) checkState(!mSearchManager.isSearching());
+
if (cwd == null) {
DirectoryFragment.showRecentsOpen(fm, anim);
} else {
- if (mState.currentSearch != null) {
- // Ongoing search
- DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim);
- } else {
- // Normal boring directory
- DirectoryFragment.showDirectory(fm, root, cwd, anim);
- }
+ // Normal boring directory
+ DirectoryFragment.showDirectory(fm, root, cwd, anim);
}
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
index 025faea..27d6797 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
@@ -22,6 +22,7 @@
import static com.android.documentsui.Shared.DEBUG;
import android.app.Activity;
+import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
@@ -33,6 +34,7 @@
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
+import android.content.UriPermission;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
@@ -62,6 +64,8 @@
private static final String EXTRA_APP_LABEL = "com.android.documentsui.APP_LABEL";
private static final String EXTRA_VOLUME_LABEL = "com.android.documentsui.VOLUME_LABEL";
+ private ContentProviderClient mExternalStorageClient;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -98,12 +102,20 @@
}
}
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mExternalStorageClient != null) {
+ mExternalStorageClient.close();
+ }
+ }
+
/**
* Validates the given path (volume + directory) and display the appropriate dialog asking the
* user to grant access to it.
*/
- private static boolean showFragment(Activity activity, int userId, StorageVolume storageVolume,
- String directoryName) {
+ private static boolean showFragment(OpenExternalDirectoryActivity activity, int userId,
+ StorageVolume storageVolume, String directoryName) {
if (DEBUG)
Log.d(TAG, "showFragment() for volume " + storageVolume.dump() + ", directory "
+ directoryName + ", and user " + userId);
@@ -129,7 +141,7 @@
return false;
}
- // Gets volume label and converted path
+ // Gets volume label and converted path.
String volumeLabel = null;
final List<VolumeInfo> volumes = sm.getVolumes();
if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size());
@@ -143,6 +155,15 @@
break;
}
}
+
+ // Checks if the user has granted the permission already.
+ final Intent intent = getIntentForExistingPermission(activity, file);
+ if (intent != null) {
+ activity.setResult(RESULT_OK, intent);
+ activity.finish();
+ return true;
+ }
+
if (volumeLabel == null) {
Log.e(TAG, "Could not get volume for " + file);
return false;
@@ -196,8 +217,7 @@
return volume.isVisibleForWrite(userId) && root.equals(path);
}
- private static Intent createGrantedUriPermissionsIntent(ContentProviderClient provider,
- File file) {
+ private static Uri getGrantedUriPermission(ContentProviderClient provider, File file) {
// Calls ExternalStorageProvider to get the doc id for the file
final Bundle bundle;
try {
@@ -218,8 +238,17 @@
Log.e(TAG, "Could not get URI for doc id " + docId);
return null;
}
-
if (DEBUG) Log.d(TAG, "URI for " + file + ": " + uri);
+ return uri;
+ }
+
+ private static Intent createGrantedUriPermissionsIntent(ContentProviderClient provider,
+ File file) {
+ final Uri uri = getGrantedUriPermission(provider, file);
+ return createGrantedUriPermissionsIntent(uri);
+ }
+
+ private static Intent createGrantedUriPermissionsIntent(Uri uri) {
final Intent intent = new Intent();
intent.setData(uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
@@ -229,13 +258,31 @@
return intent;
}
+ private static Intent getIntentForExistingPermission(OpenExternalDirectoryActivity activity,
+ File file) {
+ final String packageName = activity.getCallingPackage();
+ final Uri grantedUri = getGrantedUriPermission(activity.getExternalStorageClient(), file);
+ if (DEBUG)
+ Log.d(TAG, "checking if " + packageName + " already has permission for " + grantedUri);
+ final ActivityManager am =
+ (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
+ for (UriPermission uriPermission : am.getGrantedUriPermissions(packageName).getList()) {
+ final Uri uri = uriPermission.getUri();
+ if (uri.equals(grantedUri)) {
+ if (DEBUG) Log.d(TAG, packageName + " already has permission: " + uriPermission);
+ return createGrantedUriPermissionsIntent(uri);
+ }
+ }
+ if (DEBUG) Log.d(TAG, packageName + " does not have permission for " + grantedUri);
+ return null;
+ }
+
public static class OpenExternalDirectoryDialogFragment extends DialogFragment {
private File mFile;
private String mVolumeLabel;
private String mAppLabel;
- private ContentProviderClient mExternalStorageClient;
- private ContentResolver mResolver;
+ private OpenExternalDirectoryActivity mActivity;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -245,16 +292,8 @@
mFile = new File(args.getString(EXTRA_FILE));
mVolumeLabel = args.getString(EXTRA_VOLUME_LABEL);
mAppLabel = args.getString(EXTRA_APP_LABEL);
- mResolver = getContext().getContentResolver();
}
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (mExternalStorageClient != null) {
- mExternalStorageClient.close();
- }
+ mActivity = (OpenExternalDirectoryActivity) getActivity();
}
@Override
@@ -267,8 +306,8 @@
public void onClick(DialogInterface dialog, int which) {
Intent intent = null;
if (which == DialogInterface.BUTTON_POSITIVE) {
- intent = createGrantedUriPermissionsIntent(getExternalStorageClient(),
- mFile);
+ intent = createGrantedUriPermissionsIntent(
+ mActivity.getExternalStorageClient(), mFile);
}
if (which == DialogInterface.BUTTON_NEGATIVE || intent == null) {
activity.setResult(RESULT_CANCELED);
@@ -297,13 +336,13 @@
activity.setResult(RESULT_CANCELED);
activity.finish();
}
+ }
- private synchronized ContentProviderClient getExternalStorageClient() {
- if (mExternalStorageClient == null) {
- mExternalStorageClient =
- mResolver.acquireContentProviderClient(EXTERNAL_STORAGE_AUTH);
- }
- return mExternalStorageClient;
+ private synchronized ContentProviderClient getExternalStorageClient() {
+ if (mExternalStorageClient == null) {
+ mExternalStorageClient =
+ getContentResolver().acquireContentProviderClient(EXTERNAL_STORAGE_AUTH);
}
+ return mExternalStorageClient;
}
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SearchManager.java b/packages/DocumentsUI/src/com/android/documentsui/SearchViewManager.java
similarity index 78%
rename from packages/DocumentsUI/src/com/android/documentsui/SearchManager.java
rename to packages/DocumentsUI/src/com/android/documentsui/SearchViewManager.java
index 69f54c7..0496862 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SearchManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SearchViewManager.java
@@ -16,6 +16,8 @@
package com.android.documentsui;
+import android.annotation.Nullable;
+import android.os.Bundle;
import android.provider.DocumentsContract.Root;
import android.text.TextUtils;
import android.util.Log;
@@ -31,28 +33,27 @@
/**
* Manages searching UI behavior.
*/
-final class SearchManager implements
+final class SearchViewManager implements
SearchView.OnCloseListener, OnQueryTextListener, OnClickListener, OnFocusChangeListener {
public interface SearchManagerListener {
- void onSearchChanged();
-
- void onSearchQueryChanged(String query);
+ void onSearchChanged(@Nullable String query);
}
public static final String TAG = "SearchManger";
private SearchManagerListener mListener;
- private String currentSearch;
private boolean mSearchExpanded;
+ private String mCurrentSearch;
private boolean mIgnoreNextClose;
private DocumentsToolbar mActionBar;
private MenuItem mMenu;
private SearchView mView;
- public SearchManager(SearchManagerListener listener) {
+ public SearchViewManager(SearchManagerListener listener, @Nullable Bundle savedState) {
mListener = listener;
+ mCurrentSearch = savedState != null ? savedState.getString(Shared.EXTRA_QUERY) : null;
}
public void setSearchMangerListener(SearchManagerListener listener) {
@@ -69,6 +70,8 @@
mView.setOnCloseListener(this);
mView.setOnSearchClickListener(this);
mView.setOnQueryTextFocusChangeListener(this);
+
+ restoreSearch();
}
/**
@@ -80,12 +83,12 @@
return;
}
- if (currentSearch != null) {
+ if (mCurrentSearch != null) {
mMenu.expandActionView();
mView.setIconified(false);
mView.clearFocus();
- mView.setQuery(currentSearch, false);
+ mView.setQuery(mCurrentSearch, false);
} else {
mView.clearFocus();
if (!mView.isIconified()) {
@@ -108,13 +111,11 @@
return;
}
- mMenu.setVisible(visible);
if (!visible) {
- currentSearch = null;
- if (mListener != null) {
- mListener.onSearchQueryChanged(currentSearch);
- }
+ mCurrentSearch = null;
}
+
+ mMenu.setVisible(visible);
}
/**
@@ -133,8 +134,23 @@
return false;
}
+ private void restoreSearch() {
+ if (isSearching()) {
+ onSearchExpanded();
+ mView.setIconified(false);
+ mView.setQuery(mCurrentSearch, false);
+ mView.clearFocus();
+ }
+ }
+
+ private void onSearchExpanded() {
+ mSearchExpanded = true;
+ mView.setBackgroundColor(
+ mView.getResources().getColor(R.color.menu_search_background, null));
+ }
+
boolean isSearching() {
- return currentSearch != null;
+ return mCurrentSearch != null;
}
boolean isExpanded() {
@@ -142,6 +158,14 @@
}
/**
+ * Called when owning activity is saving state to be used to restore state during creation.
+ * @param state Bundle to save state too
+ */
+ public void onSaveInstanceState(Bundle state) {
+ state.putString(Shared.EXTRA_QUERY, mCurrentSearch);
+ }
+
+ /**
* Clears the search. Clears the SearchView background color. Triggers refreshing of the
* directory content.
* @return True if the default behavior of clearing/dismissing SearchView should be overridden.
@@ -159,11 +183,10 @@
mView.getResources().getColor(android.R.color.transparent, null));
// Refresh the directory if a search was done
- if (currentSearch != null) {
- currentSearch = null;
+ if (mCurrentSearch != null) {
+ mCurrentSearch = null;
if (mListener != null) {
- mListener.onSearchQueryChanged(currentSearch);
- mListener.onSearchChanged();
+ mListener.onSearchChanged(mCurrentSearch);
}
}
return false;
@@ -176,18 +199,15 @@
*/
@Override
public void onClick(View v) {
- mSearchExpanded = true;
- mView.setBackgroundColor(
- mView.getResources().getColor(R.color.menu_search_background, null));
+ onSearchExpanded();
}
@Override
public boolean onQueryTextSubmit(String query) {
- currentSearch = query;
+ mCurrentSearch = query;
mView.clearFocus();
if (mListener != null) {
- mListener.onSearchQueryChanged(currentSearch);
- mListener.onSearchChanged();
+ mListener.onSearchChanged(mCurrentSearch);
}
return true;
}
@@ -195,7 +215,7 @@
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
- if (currentSearch == null) {
+ if (mCurrentSearch == null) {
mView.setIconified(true);
} else if (TextUtils.isEmpty(mView.getQuery())) {
cancelSearch();
@@ -207,4 +227,9 @@
public boolean onQueryTextChange(String newText) {
return false;
}
+
+ String getCurrentSearch() {
+ return mCurrentSearch;
+ }
+
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Shared.java b/packages/DocumentsUI/src/com/android/documentsui/Shared.java
index b90a119..a288fe8 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Shared.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Shared.java
@@ -37,10 +37,50 @@
* specifies if the destination directory needs to create new directory or not.
*/
public static final String EXTRA_DIRECTORY_COPY = "com.android.documentsui.DIRECTORY_COPY";
+ public static final String EXTRA_STACK = "com.android.documentsui.STACK";
+
+ /**
+ * Extra flag used to store query of type String in the bundle.
+ */
+ public static final String EXTRA_QUERY = "query";
+
+ /**
+ * Extra flag used to store state of type State in the bundle.
+ */
+ public static final String EXTRA_STATE = "state";
+
+ /**
+ * Extra flag used to store type of DirectoryFragment's type ResultType type in the bundle.
+ */
+ public static final String EXTRA_TYPE = "type";
+
+ /**
+ * Extra flag used to store root of type RootInfo in the bundle.
+ */
+ public static final String EXTRA_ROOT = "root";
+
+ /**
+ * Extra flag used to store document of DocumentInfo type in the bundle.
+ */
+ public static final String EXTRA_DOC = "document";
+
+ /**
+ * Extra flag used to store DirectoryFragment's selection of Selection type in the bundle.
+ */
+ public static final String EXTRA_SELECTION = "selection";
+
+ /**
+ * Extra flag used to store DirectoryFragment's search mode of boolean type in the bundle.
+ */
+ public static final String EXTRA_SEARCH_MODE = "searchMode";
+
+ /**
+ * Extra flag used to store DirectoryFragment's ignore state of boolean type in the bundle.
+ */
+ public static final String EXTRA_IGNORE_STATE = "ignoreState";
public static final boolean DEBUG = true;
public static final String TAG = "Documents";
- public static final String EXTRA_STACK = "com.android.documentsui.STACK";
/**
diff --git a/packages/DocumentsUI/src/com/android/documentsui/State.java b/packages/DocumentsUI/src/com/android/documentsui/State.java
index 0948ab1..2ecbdf6 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/State.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/State.java
@@ -105,9 +105,6 @@
private boolean mInitialRootChanged;
private boolean mInitialDocChanged;
- /** Currently active search, overriding any stack. */
- public String currentSearch;
-
/** Instance state for every shown directory */
public HashMap<String, SparseArray<Parcelable>> dirState = new HashMap<>();
@@ -186,7 +183,6 @@
out.writeInt(showAdvanced ? 1 : 0);
out.writeInt(restored ? 1 : 0);
DurableUtils.writeToParcel(out, stack);
- out.writeString(currentSearch);
out.writeMap(dirState);
out.writeParcelable(selectedDocuments, 0);
out.writeList(selectedDocumentsForCopy);
@@ -217,7 +213,6 @@
state.showAdvanced = in.readInt() != 0;
state.restored = in.readInt() != 0;
DurableUtils.readFromParcel(in, state.stack);
- state.currentSearch = in.readString();
in.readMap(state.dirState, loader);
state.selectedDocuments = in.readParcelable(loader);
in.readList(state.selectedDocumentsForCopy, loader);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 4583dec..7138c2d 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -113,19 +113,26 @@
/**
* Display the documents inside a single directory.
*/
-public class DirectoryFragment extends Fragment implements DocumentsAdapter.Environment {
+public class DirectoryFragment extends Fragment
+ implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult> {
@IntDef(flag = true, value = {
TYPE_NORMAL,
- TYPE_SEARCH,
TYPE_RECENT_OPEN
})
@Retention(RetentionPolicy.SOURCE)
public @interface ResultType {}
public static final int TYPE_NORMAL = 1;
- public static final int TYPE_SEARCH = 2;
- public static final int TYPE_RECENT_OPEN = 3;
+ public static final int TYPE_RECENT_OPEN = 2;
+ @IntDef(flag = true, value = {
+ ANIM_NONE,
+ ANIM_SIDE,
+ ANIM_LEAVE,
+ ANIM_ENTER
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AnimationType {}
public static final int ANIM_NONE = 1;
public static final int ANIM_SIDE = 2;
public static final int ANIM_LEAVE = 3;
@@ -146,12 +153,6 @@
private static final int DELETE_JOB_DELAY = 5500;
private static final int EMPTY_REVEAL_DURATION = 250;
- private static final String EXTRA_TYPE = "type";
- private static final String EXTRA_ROOT = "root";
- private static final String EXTRA_DOC = "doc";
- private static final String EXTRA_QUERY = "query";
- private static final String EXTRA_IGNORE_STATE = "ignoreState";
-
private Model mModel;
private MultiSelectManager mSelectionManager;
private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
@@ -164,12 +165,10 @@
private RecyclerView mRecView;
private ListeningGestureDetector mGestureDetector;
- private @ResultType int mType = TYPE_NORMAL;
private String mStateKey;
private int mLastSortOrder = SORT_ORDER_UNKNOWN;
private DocumentsAdapter mAdapter;
- private LoaderCallbacks<DirectoryResult> mCallbacks;
private FragmentTuner mTuner;
private DocumentClipper mClipper;
private GridLayoutManager mLayout;
@@ -178,6 +177,14 @@
private MessageBar mMessageBar;
private View mProgressBar;
+ // Directory fragment state is defined by: root, document, query, type, selection
+ private @ResultType int mType = TYPE_NORMAL;
+ private RootInfo mRoot;
+ private DocumentInfo mDocument;
+ private String mQuery = null;
+ private Selection mSelection = null;
+ private boolean mSearchMode = false;
+
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -226,9 +233,16 @@
final Context context = getActivity();
final State state = getDisplayState();
- final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
- final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
- mStateKey = buildStateKey(root, doc);
+ // Read arguments when object created for the first time.
+ // Restore state if fragment recreated.
+ Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState;
+ mRoot = args.getParcelable(Shared.EXTRA_ROOT);
+ mDocument = args.getParcelable(Shared.EXTRA_DOC);
+ mStateKey = buildStateKey(mRoot, mDocument);
+ mQuery = args.getString(Shared.EXTRA_QUERY);
+ mType = args.getInt(Shared.EXTRA_TYPE);
+ mSelection = args.getParcelable(Shared.EXTRA_SELECTION);
+ mSearchMode = args.getBoolean(Shared.EXTRA_SEARCH_MODE);
mIconHelper = new IconHelper(context, MODE_GRID);
@@ -248,13 +262,6 @@
mRecView.addOnItemTouchListener(mGestureDetector);
- // final here because we'll manually bump the listener iwhen we had an initial selection,
- // but only after the model is fully loaded.
- final SelectionModeListener selectionListener = new SelectionModeListener();
- final Selection initialSelection = state.selectedDocuments.hasDirectoryKey(mStateKey)
- ? state.selectedDocuments
- : null;
-
// TODO: instead of inserting the view into the constructor, extract listener-creation code
// and set the listener on the view after the fact. Then the view doesn't need to be passed
// into the selection manager.
@@ -264,9 +271,9 @@
state.allowMultiple
? MultiSelectManager.MODE_MULTIPLE
: MultiSelectManager.MODE_SINGLE,
- initialSelection);
+ null);
- mSelectionManager.addCallback(selectionListener);
+ mSelectionManager.addCallback(new SelectionModeListener());
mModel = new Model();
mModel.addUpdateListener(mAdapter);
@@ -275,8 +282,6 @@
// Make sure this is done after the RecyclerView is set up.
mFocusManager = new FocusManager(context, mRecView, mModel);
- mType = getArguments().getInt(EXTRA_TYPE);
-
mTuner = FragmentTuner.pick(getContext(), state);
mClipper = new DocumentClipper(context);
@@ -286,7 +291,7 @@
hideGridTitles = MimePredicate.mimeMatches(
MimePredicate.VISUAL_MIMES, state.acceptMimes);
} else {
- hideGridTitles = (doc != null) && doc.isGridTitlesHidden();
+ hideGridTitles = (mDocument != null) && mDocument.isGridTitlesHidden();
}
GridDocumentHolder.setHideTitles(hideGridTitles);
@@ -295,86 +300,20 @@
boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
mIconHelper.setThumbnailsEnabled(!svelte);
- mCallbacks = new LoaderCallbacks<DirectoryResult>() {
- @Override
- public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
- final String query = getArguments().getString(EXTRA_QUERY);
-
- Uri contentsUri;
- switch (mType) {
- case TYPE_NORMAL:
- contentsUri = DocumentsContract.buildChildDocumentsUri(
- doc.authority, doc.documentId);
- if (state.action == ACTION_MANAGE) {
- contentsUri = DocumentsContract.setManageMode(contentsUri);
- }
- return new DirectoryLoader(
- context, mType, root, doc, contentsUri, state.userSortOrder);
- case TYPE_SEARCH:
- contentsUri = DocumentsContract.buildSearchDocumentsUri(
- root.authority, root.rootId, query);
- if (state.action == ACTION_MANAGE) {
- contentsUri = DocumentsContract.setManageMode(contentsUri);
- }
- return new DirectoryLoader(
- context, mType, root, doc, contentsUri, state.userSortOrder);
- case TYPE_RECENT_OPEN:
- final RootsCache roots = DocumentsApplication.getRootsCache(context);
- return new RecentsLoader(context, roots, state);
- default:
- throw new IllegalStateException("Unknown type " + mType);
- }
- }
-
- @Override
- public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
- if (!isAdded()) return;
-
- mModel.update(result);
- state.derivedSortOrder = result.sortOrder;
-
- updateDisplayState();
-
- if (initialSelection != null) {
- selectionListener.onSelectionChanged();
- }
-
- // Restore any previous instance state
- final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
- if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) {
- getView().restoreHierarchyState(container);
- } else if (mLastSortOrder != state.derivedSortOrder) {
- // The derived sort order takes the user sort order into account, but applies
- // directory-specific defaults when the user doesn't explicitly set the sort
- // order. Scroll to the top if the sort order actually changed.
- mRecView.smoothScrollToPosition(0);
- }
-
- mLastSortOrder = state.derivedSortOrder;
-
- mTuner.onModelLoaded(mModel, mType);
- }
-
- @Override
- public void onLoaderReset(Loader<DirectoryResult> loader) {
- mModel.update(null);
- }
- };
-
// Kick off loader at least once
- getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks);
+ getLoaderManager().restartLoader(LOADER_ID, null, this);
}
@Override
public void onSaveInstanceState(Bundle outState) {
- State state = getDisplayState();
- if (mSelectionManager.hasSelection()) {
- mSelectionManager.getSelection(state.selectedDocuments);
- state.selectedDocuments.setDirectoryKey(mStateKey);
- if (!state.selectedDocuments.isEmpty()) {
- if (DEBUG) Log.d(TAG, "Persisted selection: " + state.selectedDocuments);
- }
- }
+ super.onSaveInstanceState(outState);
+
+ outState.putInt(Shared.EXTRA_TYPE, mType);
+ outState.putParcelable(Shared.EXTRA_ROOT, mRoot);
+ outState.putParcelable(Shared.EXTRA_DOC, mDocument);
+ outState.putString(Shared.EXTRA_QUERY, mQuery);
+ outState.putParcelable(Shared.EXTRA_SELECTION, mSelectionManager.getSelection());
+ outState.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
}
@Override
@@ -449,7 +388,7 @@
public void onSortOrderChanged() {
// Sort order is implemented as a sorting wrapper around directory
// results. So when sort order changes, we force a reload of the directory.
- getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks);
+ getLoaderManager().restartLoader(LOADER_ID, null, this);
}
public void onViewModeChanged() {
@@ -552,7 +491,8 @@
// triggered on "silent" selection updates (i.e. we might be reacting to unfinalized
// selection changes here)
final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
- if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
+ if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0
+ && (docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
mNoDeleteCount += selected ? 1 : -1;
}
if ((docFlags & Document.FLAG_SUPPORTS_RENAME) != 0) {
@@ -1342,7 +1282,7 @@
mProgressBar.setVisibility(model.isLoading() ? View.VISIBLE : View.GONE);
if (model.isEmpty()) {
- if (getDisplayState().currentSearch != null) {
+ if (mSearchMode) {
showNoResults(getDisplayState().stack.root);
} else {
showEmptyDirectory();
@@ -1468,32 +1408,50 @@
public static void showDirectory(
FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
- show(fm, TYPE_NORMAL, root, doc, null, anim);
- }
-
- public static void showSearch(FragmentManager fm, RootInfo root, String query, int anim) {
- show(fm, TYPE_SEARCH, root, null, query, anim);
+ create(fm, TYPE_NORMAL, root, doc, null, anim);
}
public static void showRecentsOpen(FragmentManager fm, int anim) {
- show(fm, TYPE_RECENT_OPEN, null, null, null, anim);
+ create(fm, TYPE_RECENT_OPEN, null, null, null, anim);
}
- private static void show(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
+ public static void reloadSearch(FragmentManager fm, RootInfo root, DocumentInfo doc,
+ String query) {
+ DirectoryFragment df = get(fm);
+
+ df.mQuery = query;
+ df.mRoot = root;
+ df.mDocument = doc;
+ df.mSearchMode = query != null;
+ df.getLoaderManager().restartLoader(LOADER_ID, null, df);
+ }
+
+ public static void reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
+ String query) {
+ DirectoryFragment df = get(fm);
+ df.mType = type;
+ df.mQuery = query;
+ df.mRoot = root;
+ df.mDocument = doc;
+ df.mSearchMode = query != null;
+ df.getLoaderManager().restartLoader(LOADER_ID, null, df);
+ }
+
+ public static void create(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
String query, int anim) {
final Bundle args = new Bundle();
- args.putInt(EXTRA_TYPE, type);
- args.putParcelable(EXTRA_ROOT, root);
- args.putParcelable(EXTRA_DOC, doc);
- args.putString(EXTRA_QUERY, query);
+ args.putInt(Shared.EXTRA_TYPE, type);
+ args.putParcelable(Shared.EXTRA_ROOT, root);
+ args.putParcelable(Shared.EXTRA_DOC, doc);
+ args.putString(Shared.EXTRA_QUERY, query);
final FragmentTransaction ft = fm.beginTransaction();
switch (anim) {
case ANIM_SIDE:
- args.putBoolean(EXTRA_IGNORE_STATE, true);
+ args.putBoolean(Shared.EXTRA_IGNORE_STATE, true);
break;
case ANIM_ENTER:
- args.putBoolean(EXTRA_IGNORE_STATE, true);
+ args.putBoolean(Shared.EXTRA_IGNORE_STATE, true);
ft.setCustomAnimations(R.animator.dir_enter, R.animator.dir_frozen);
break;
case ANIM_LEAVE:
@@ -1504,7 +1462,7 @@
final DirectoryFragment fragment = new DirectoryFragment();
fragment.setArguments(args);
- ft.replace(R.id.container_directory, fragment);
+ ft.replace(getFragmentId(), fragment);
ft.commitAllowingStateLoss();
}
@@ -1518,9 +1476,78 @@
public static @Nullable DirectoryFragment get(FragmentManager fm) {
// TODO: deal with multiple directories shown at once
- Fragment fragment = fm.findFragmentById(R.id.container_directory);
+ Fragment fragment = fm.findFragmentById(getFragmentId());
return fragment instanceof DirectoryFragment
? (DirectoryFragment) fragment
: null;
}
-}
+
+ private static int getFragmentId() {
+ return R.id.container_directory;
+ }
+
+ @Override
+ public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
+ Context context = getActivity();
+ State state = getDisplayState();
+
+ Uri contentsUri;
+ switch (mType) {
+ case TYPE_NORMAL:
+ contentsUri = mSearchMode ? DocumentsContract.buildSearchDocumentsUri(
+ mRoot.authority, mRoot.rootId, mQuery)
+ : DocumentsContract.buildChildDocumentsUri(
+ mDocument.authority, mDocument.documentId);
+ if (state.action == ACTION_MANAGE) {
+ contentsUri = DocumentsContract.setManageMode(contentsUri);
+ }
+ return new DirectoryLoader(
+ context, mType, mRoot, mDocument, contentsUri, state.userSortOrder,
+ mSearchMode);
+ case TYPE_RECENT_OPEN:
+ final RootsCache roots = DocumentsApplication.getRootsCache(context);
+ return new RecentsLoader(context, roots, state);
+ default:
+ throw new IllegalStateException("Unknown type " + mType);
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
+ if (!isAdded()) return;
+
+ State state = getDisplayState();
+
+ mAdapter.notifyDataSetChanged();
+ mModel.update(result);
+
+ state.derivedSortOrder = result.sortOrder;
+
+ updateLayout(state.derivedMode);
+
+ if (mSelection != null) {
+ mSelectionManager.setItemsSelected(mSelection.toList(), true);
+ }
+
+ // Restore any previous instance state
+ final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
+ if (container != null && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
+ getView().restoreHierarchyState(container);
+ } else if (mLastSortOrder != state.derivedSortOrder) {
+ // The derived sort order takes the user sort order into account, but applies
+ // directory-specific defaults when the user doesn't explicitly set the sort
+ // order. Scroll to the top if the sort order actually changed.
+ mRecView.smoothScrollToPosition(0);
+ }
+
+ mLastSortOrder = state.derivedSortOrder;
+
+ mTuner.onModelLoaded(mModel, mType, mSearchMode);
+
+ }
+
+ @Override
+ public void onLoaderReset(Loader<DirectoryResult> loader) {
+ mModel.update(null);
+ }
+ }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FragmentTuner.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FragmentTuner.java
index a9b0fd1..59efed6 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FragmentTuner.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FragmentTuner.java
@@ -26,7 +26,6 @@
import static com.android.internal.util.Preconditions.checkArgument;
import android.content.Context;
-import android.os.SystemProperties;
import android.provider.DocumentsContract.Document;
import android.util.Log;
import android.view.Menu;
@@ -66,8 +65,8 @@
}
- public abstract void updateActionMenu(Menu menu, int dirType, boolean canDelete,
- boolean canRename);
+ public abstract void updateActionMenu(
+ Menu menu, @ResultType int dirType, boolean canDelete, boolean canRename);
// Subtly different from isDocumentEnabled. The reason may be illuminated as follows.
// A folder is enabled such that it may be double clicked, even in settings
@@ -84,7 +83,7 @@
return MimePredicate.mimeMatches(mState.acceptMimes, docMimeType);
}
- abstract void onModelLoaded(Model model, @ResultType int resultType);
+ abstract void onModelLoaded(Model model, @ResultType int resultType, boolean isSearch);
/**
* Provides support for Platform specific specializations of DirectoryFragment.
@@ -140,33 +139,22 @@
}
@Override
- public void updateActionMenu(Menu menu, int dirType, boolean canDelete,
- boolean canRename) {
+ public void updateActionMenu(
+ Menu menu, @ResultType int dirType, boolean canDelete, boolean canRename) {
- boolean copyEnabled = dirType != DirectoryFragment.TYPE_RECENT_OPEN;
- boolean moveEnabled =
- SystemProperties.getBoolean("debug.documentsui.enable_move", false);
- menu.findItem(R.id.menu_copy_to_clipboard).setEnabled(copyEnabled);
-
- final MenuItem open = menu.findItem(R.id.menu_open);
- final MenuItem share = menu.findItem(R.id.menu_share);
- final MenuItem delete = menu.findItem(R.id.menu_delete);
- final MenuItem copyTo = menu.findItem(R.id.menu_copy_to);
- final MenuItem moveTo = menu.findItem(R.id.menu_move_to);
- final MenuItem rename = menu.findItem(R.id.menu_rename);
+ MenuItem open = menu.findItem(R.id.menu_open);
+ MenuItem share = menu.findItem(R.id.menu_share);
+ MenuItem delete = menu.findItem(R.id.menu_delete);
+ MenuItem rename = menu.findItem(R.id.menu_rename);
open.setVisible(true);
share.setVisible(false);
delete.setVisible(false);
- copyTo.setVisible(copyEnabled);
- copyTo.setEnabled(copyEnabled);
- moveTo.setVisible(moveEnabled);
- moveTo.setEnabled(moveEnabled);
rename.setVisible(false);
}
@Override
- void onModelLoaded(Model model, @ResultType int resultType) {
+ void onModelLoaded(Model model, @ResultType int resultType, boolean isSearch) {
// When launched into empty recents, show drawer
if (resultType == DirectoryFragment.TYPE_RECENT_OPEN
&& model.isEmpty()
@@ -186,32 +174,29 @@
}
@Override
- public void updateActionMenu(Menu menu, int dirType, boolean canDelete,
- boolean canRename) {
- checkArgument(dirType != DirectoryFragment.TYPE_RECENT_OPEN);
+ public void updateActionMenu(
+ Menu menu, @ResultType int resultType, boolean canDelete, boolean canRename) {
+ checkArgument(resultType != DirectoryFragment.TYPE_RECENT_OPEN);
- boolean moveEnabled =
- SystemProperties.getBoolean("debug.documentsui.enable_move", false);
- menu.findItem(R.id.menu_copy_to_clipboard).setEnabled(true);
-
- final MenuItem open = menu.findItem(R.id.menu_open);
- final MenuItem share = menu.findItem(R.id.menu_share);
- final MenuItem delete = menu.findItem(R.id.menu_delete);
- final MenuItem copyTo = menu.findItem(R.id.menu_copy_to);
- final MenuItem moveTo = menu.findItem(R.id.menu_move_to);
- final MenuItem rename = menu.findItem(R.id.menu_rename);
+ MenuItem open = menu.findItem(R.id.menu_open);
+ MenuItem delete = menu.findItem(R.id.menu_delete);
+ MenuItem copyTo = menu.findItem(R.id.menu_copy_to);
+ MenuItem moveTo = menu.findItem(R.id.menu_move_to);
+ MenuItem rename = menu.findItem(R.id.menu_rename);
+ MenuItem copy = menu.findItem(R.id.menu_copy_to_clipboard);
open.setVisible(false);
delete.setVisible(canDelete);
+ copy.setEnabled(true); // to clipboard
copyTo.setVisible(true);
copyTo.setEnabled(true);
- moveTo.setVisible(moveEnabled);
- moveTo.setEnabled(moveEnabled);
+ moveTo.setVisible(true);
+ moveTo.setEnabled(true);
rename.setVisible(false);
}
@Override
- void onModelLoaded(Model model, @ResultType int resultType) {}
+ void onModelLoaded(Model model, @ResultType int resultType, boolean isSearch) {}
}
/**
@@ -226,8 +211,8 @@
}
@Override
- public void updateActionMenu(Menu menu, int dirType, boolean canDelete,
- boolean canRename) {
+ public void updateActionMenu(
+ Menu menu, @ResultType int dirType, boolean canDelete, boolean canRename) {
MenuItem copy = menu.findItem(R.id.menu_copy_to_clipboard);
MenuItem paste = menu.findItem(R.id.menu_paste_from_clipboard);
@@ -239,20 +224,19 @@
menu.findItem(R.id.menu_share).setVisible(true);
menu.findItem(R.id.menu_delete).setVisible(canDelete);
-
menu.findItem(R.id.menu_open).setVisible(false);
menu.findItem(R.id.menu_copy_to).setVisible(true);
menu.findItem(R.id.menu_move_to).setVisible(true);
+ menu.findItem(R.id.menu_move_to).setEnabled(canDelete);
Menus.disableHiddenItems(menu, copy, paste);
}
@Override
- void onModelLoaded(Model model, @ResultType int resultType) {
+ void onModelLoaded(Model model, @ResultType int resultType, boolean isSearch) {
if (DEBUG) Log.d(TAG, "Handling model loaded. Has Location shcnage: " + mState.initialLocationHasChanged());
// When launched into empty root, open drawer.
- if (model.isEmpty() && !mState.initialLocationHasChanged()
- && resultType != DirectoryFragment.TYPE_SEARCH) {
+ if (model.isEmpty() && !mState.initialLocationHasChanged() && !isSearch) {
if (DEBUG) Log.d(TAG, "Showing roots drawer cuz stuffs empty.");
// This noops on layouts without drawer, so no need to guard.
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
index c8b6f85..4cf1048 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
@@ -657,15 +657,22 @@
// item A is tapped (and selected), then an in-progress band select covers A then uncovers
// A, A should still be selected as it has been saved. To ensure this behavior, the saved
// selection must be tracked separately.
- private Set<String> mSelection = new HashSet<>();
- private Set<String> mProvisionalSelection = new HashSet<>();
+ private final Set<String> mSelection;
+ private final Set<String> mProvisionalSelection;
private String mDirectoryKey;
- @VisibleForTesting
- public Selection(String... ids) {
- for (int i = 0; i < ids.length; i++) {
- add(ids[i]);
- }
+ public Selection() {
+ mSelection = new HashSet<String>();
+ mProvisionalSelection = new HashSet<String>();
+ }
+
+ /**
+ * Used by CREATOR.
+ */
+ private Selection(String directoryKey, List<String> selection) {
+ mDirectoryKey = directoryKey;
+ mSelection = new HashSet<String>(selection);
+ mProvisionalSelection = new HashSet<String>();
}
/**
@@ -687,7 +694,7 @@
* Returns an unordered array of selected positions (including any
* provisional selections current in effect).
*/
- private List<String> toList() {
+ public List<String> toList() {
ArrayList<String> selection = new ArrayList<String>(mSelection);
selection.addAll(mProvisionalSelection);
return selection;
@@ -810,8 +817,11 @@
@VisibleForTesting
void copyFrom(Selection source) {
- mSelection = new HashSet<>(source.mSelection);
- mProvisionalSelection = new HashSet<>(source.mProvisionalSelection);
+ mSelection.clear();
+ mSelection.addAll(source.mSelection);
+
+ mProvisionalSelection.clear();
+ mProvisionalSelection.addAll(source.mProvisionalSelection);
}
@Override
@@ -878,6 +888,26 @@
// We don't include provisional selection since it is
// typically coupled to some other runtime state (like a band).
}
+
+ public static final ClassLoaderCreator<Selection> CREATOR =
+ new ClassLoaderCreator<Selection>() {
+ @Override
+ public Selection createFromParcel(Parcel in) {
+ return createFromParcel(in, null);
+ }
+
+ @Override
+ public Selection createFromParcel(Parcel in, ClassLoader loader) {
+ return new Selection(
+ in.readString(),
+ (ArrayList<String>) in.readArrayList(loader));
+ }
+
+ @Override
+ public Selection[] newArray(int size) {
+ return new Selection[size];
+ }
+ };
}
/**
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/RenameDocumentFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
index 7394c12..2957bf0 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
@@ -46,6 +46,7 @@
import com.android.documentsui.BaseActivity;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.R;
+import com.android.documentsui.Shared;
import com.android.documentsui.Snackbars;
import com.android.documentsui.model.DocumentInfo;
@@ -55,6 +56,7 @@
public class RenameDocumentFragment extends DialogFragment {
private static final String TAG_RENAME_DOCUMENT = "rename_document";
private DocumentInfo mDocument;
+ private EditText mEditText;
public static void show(FragmentManager fm, DocumentInfo document) {
final RenameDocumentFragment dialog = new RenameDocumentFragment();
@@ -62,6 +64,11 @@
dialog.show(fm, TAG_RENAME_DOCUMENT);
}
+ /**
+ * Creates the dialog UI.
+ * @param savedInstanceState
+ * @return
+ */
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Context context = getActivity();
@@ -69,8 +76,7 @@
LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
View view = dialogInflater.inflate(R.layout.dialog_file_name, null, false);
- final EditText editText = (EditText) view.findViewById(android.R.id.text1);
- fillWithFileName(editText, mDocument.displayName);
+ mEditText = (EditText) view.findViewById(android.R.id.text1);
builder.setTitle(R.string.menu_rename);
builder.setView(view);
@@ -79,7 +85,7 @@
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
- renameDocuments(editText.getText().toString());
+ renameDocuments(mEditText.getText().toString());
}
});
@@ -87,7 +93,7 @@
final AlertDialog dialog = builder.create();
- editText.setOnEditorActionListener(
+ mEditText.setOnEditorActionListener(
new OnEditorActionListener() {
@Override
public boolean onEditorAction(
@@ -95,18 +101,51 @@
if ((actionId == EditorInfo.IME_ACTION_DONE) || (event != null
&& event.getKeyCode() == KeyEvent.KEYCODE_ENTER
&& event.hasNoModifiers())) {
- renameDocuments(editText.getText().toString());
+ renameDocuments(mEditText.getText().toString());
dialog.dismiss();
return true;
}
return false;
}
});
-
return dialog;
}
/**
+ * Sets/Restores the data.
+ * @param savedInstanceState
+ * @return
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ if(savedInstanceState == null) {
+ // Fragment created for the first time, we set the text.
+ // mDocument value was set in show
+ mEditText.setText(mDocument.displayName);
+ }
+ else {
+ // Fragment restored, text was restored automatically.
+ // mDocument value needs to be restored.
+ mDocument = savedInstanceState.getParcelable(Shared.EXTRA_DOC);
+ }
+ // Do selection in both cases, because we cleared it.
+ selectFileName(mEditText);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ // Clear selection before storing state and restore it manually,
+ // because otherwise after rotation selection is displayed with cut/copy menu visible :/
+ clearFileNameSelection(mEditText);
+
+ super.onSaveInstanceState(outState);
+
+ outState.putParcelable(Shared.EXTRA_DOC, mDocument);
+ }
+
+ /**
* Validates if string is a proper document name.
* Checks if string is not empty. More rules might be added later.
* @param docName string representing document name
@@ -120,12 +159,20 @@
* Fills text field with the file name and selects the name without extension.
*
* @param editText text field to be filled
- * @param name full name of the file
*/
- private void fillWithFileName(EditText editText, String name) {
- editText.setText(name);
- int separatorIndex = name.indexOf(".");
- editText.setSelection(0, separatorIndex == -1 ? name.length() : separatorIndex);
+ private void selectFileName(EditText editText) {
+ String text = editText.getText().toString();
+ int separatorIndex = text.indexOf(".");
+ editText.setSelection(0, separatorIndex == -1 ? text.length() : separatorIndex);
+ }
+
+ /**
+ * Clears selection in text field.
+ *
+ * @param editText text field to be cleared.
+ */
+ private void clearFileNameSelection(EditText editText) {
+ editText.setSelection(0, 0);
}
private void renameDocuments(String newDisplayName) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
index e9fdab0..d74121e 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
@@ -207,7 +207,13 @@
return "Document{"
+ "docId=" + documentId
+ ", name=" + displayName
+ + ", isContainer=" + isContainer()
+ ", isDirectory=" + isDirectory()
+ + ", isArchive=" + isArchive()
+ + ", isVirtualDocument=" + isVirtualDocument()
+ + ", isDeleteSupported=" + isDeleteSupported()
+ + ", isCreateSupported=" + isCreateSupported()
+ + ", isRenameSupported=" + isRenameSupported()
+ "}";
}
@@ -231,6 +237,10 @@
return (flags & Document.FLAG_SUPPORTS_DELETE) != 0;
}
+ public boolean isRemoveSupported() {
+ return (flags & Document.FLAG_SUPPORTS_REMOVE) != 0;
+ }
+
public boolean isRenameSupported() {
return (flags & Document.FLAG_SUPPORTS_RENAME) != 0;
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
index dad8697..34a35ee 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
@@ -129,10 +129,19 @@
}
Notification getProgressNotification(@StringRes int msgId) {
- double completed = (double) this.mBytesCopied / mBatchSize;
- mProgressBuilder.setProgress(100, (int) (completed * 100), false);
- mProgressBuilder.setContentInfo(
- NumberFormat.getPercentInstance().format(completed));
+ if (mBatchSize >= 0) {
+ double completed = (double) this.mBytesCopied / mBatchSize;
+ mProgressBuilder.setProgress(100, (int) (completed * 100), false);
+ mProgressBuilder.setContentInfo(
+ NumberFormat.getPercentInstance().format(completed));
+ } else {
+ // If the total file size failed to compute on some files, then show
+ // an indeterminate spinner. CopyJob would most likely fail on those
+ // files while copying, but would continue with another files.
+ // Also, if the total size is 0 bytes, show an indeterminate spinner.
+ mProgressBuilder.setProgress(0, 0, true);
+ }
+
if (mRemainingTime > 0) {
mProgressBuilder.setContentText(service.getString(msgId,
DateUtils.formatDuration(mRemainingTime)));
@@ -208,11 +217,15 @@
}
@Override
- void start() throws RemoteException {
+ void start() {
mStartTime = elapsedRealtime();
- // client
- mBatchSize = calculateSize(mSrcs);
+ try {
+ mBatchSize = calculateSize(mSrcs);
+ } catch (ResourceException e) {
+ Log.w(TAG, "Failed to calculate total size. Copying without progress.");
+ mBatchSize = -1;
+ }
DocumentInfo srcInfo;
DocumentInfo dstInfo = stack.peek();
@@ -220,9 +233,13 @@
srcInfo = mSrcs.get(i);
// Guard unsupported recursive operation.
- if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
- onFileFailed(srcInfo,
- "Skipping recursive operation on directory " + dstInfo.derivedUri + ".");
+ try {
+ if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
+ throw new ResourceException("Cannot copy to itself recursively.");
+ }
+ } catch (ResourceException e) {
+ Log.e(TAG, e.toString());
+ onFileFailed(srcInfo);
continue;
}
@@ -230,7 +247,12 @@
"Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
+ " to " + dstInfo.displayName + " (" + dstInfo.derivedUri + ")");
- processDocument(srcInfo, null, dstInfo);
+ try {
+ processDocument(srcInfo, null, dstInfo);
+ } catch (ResourceException e) {
+ Log.e(TAG, e.toString());
+ onFileFailed(srcInfo);
+ }
}
Metrics.logFileOperation(service, operationType, mSrcs, dstInfo);
}
@@ -259,13 +281,12 @@
* @param src DocumentInfos for the documents to copy.
* @param srcParent DocumentInfo for the parent of the document to process.
* @param dstDirInfo The destination directory.
- * @return True on success, false on failure.
- * @throws RemoteException
+ * @throws ResourceException
*
* TODO: Stop passing srcParent, as it's not used for copy, but for move only.
*/
- boolean processDocument(DocumentInfo src, DocumentInfo srcParent,
- DocumentInfo dstDirInfo) throws RemoteException {
+ void processDocument(DocumentInfo src, DocumentInfo srcParent,
+ DocumentInfo dstDirInfo) throws ResourceException {
// TODO: When optimized copy kicks in, we'll not making any progress updates.
// For now. Local storage isn't using optimized copy.
@@ -274,22 +295,28 @@
// If not supported, then fallback to byte-by-byte copy/move.
if (src.authority.equals(dstDirInfo.authority)) {
if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
- if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,
- dstDirInfo.derivedUri) == null) {
- onFileFailed(src,
- "Provider side copy failed for documents: " + src.derivedUri + ".");
- return false;
+ try {
+ if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,
+ dstDirInfo.derivedUri) == null) {
+ throw new ResourceException("Provider side copy failed for document %s.",
+ src.derivedUri);
+ }
+ } catch (ResourceException e) {
+ throw e;
+ } catch (RemoteException | RuntimeException e) {
+ throw new ResourceException(
+ "Provider side copy failed for document %s due to an exception.",
+ src.derivedUri, e);
}
- return true;
+ return;
}
}
// If we couldn't do an optimized copy...we fall back to vanilla byte copy.
- return byteCopyDocument(src, dstDirInfo);
+ byteCopyDocument(src, dstDirInfo);
}
- boolean byteCopyDocument(DocumentInfo src, DocumentInfo dest)
- throws RemoteException {
+ void byteCopyDocument(DocumentInfo src, DocumentInfo dest) throws ResourceException {
final String dstMimeType;
final String dstDisplayName;
@@ -297,8 +324,14 @@
// If the file is virtual, but can be converted to another format, then try to copy it
// as such format. Also, append an extension for the target mime type (if known).
if (src.isVirtualDocument()) {
- final String[] streamTypes = getContentResolver().getStreamTypes(
- src.derivedUri, "*/*");
+ String[] streamTypes = null;
+ try {
+ streamTypes = getContentResolver().getStreamTypes(src.derivedUri, "*/*");
+ } catch (RuntimeException e) {
+ throw new ResourceException(
+ "Failed to obtain streamable types for %s due to an exception.",
+ src.derivedUri, e);
+ }
if (streamTypes != null && streamTypes.length > 0) {
dstMimeType = streamTypes[0];
final String extension = MimeTypeMap.getSingleton().
@@ -306,8 +339,8 @@
dstDisplayName = src.displayName +
(extension != null ? "." + extension : src.displayName);
} else {
- onFileFailed(src, "Cannot copy virtual file. No streamable formats available.");
- return false;
+ throw new ResourceException("Cannot copy virtual file %s. No streamable formats "
+ + "available.", src.derivedUri);
}
} else {
dstMimeType = src.mimeType;
@@ -316,33 +349,35 @@
// Create the target document (either a file or a directory), then copy recursively the
// contents (bytes or children).
- final Uri dstUri = DocumentsContract.createDocument(
- getClient(dest), dest.derivedUri, dstMimeType, dstDisplayName);
+ Uri dstUri = null;
+ try {
+ dstUri = DocumentsContract.createDocument(
+ getClient(dest), dest.derivedUri, dstMimeType, dstDisplayName);
+ } catch (RemoteException | RuntimeException e) {
+ throw new ResourceException(
+ "Couldn't create destination document " + dstDisplayName + " in directory %s "
+ + "due to an exception.", dest.derivedUri, e);
+ }
if (dstUri == null) {
// If this is a directory, the entire subdir will not be copied over.
- onFileFailed(src,
- "Couldn't create destination document " + dstDisplayName
- + " in directory " + dest.displayName + ".");
- return false;
+ throw new ResourceException(
+ "Couldn't create destination document " + dstDisplayName + " in directory %s.",
+ dest.derivedUri);
}
DocumentInfo dstInfo = null;
try {
dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
- } catch (FileNotFoundException e) {
- onFileFailed(src,
- "Could not load DocumentInfo for newly created file: " + dstUri + ".");
- return false;
+ } catch (FileNotFoundException | RuntimeException e) {
+ throw new ResourceException("Could not load DocumentInfo for newly created file %s.",
+ dstUri);
}
- final boolean success;
if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
- success = copyDirectoryHelper(src, dstInfo);
+ copyDirectoryHelper(src, dstInfo);
} else {
- success = copyFileHelper(src, dstInfo, dstMimeType);
+ copyFileHelper(src, dstInfo, dstMimeType);
}
-
- return success;
}
/**
@@ -352,11 +387,10 @@
* @param srcDir Info of the directory to copy from. The routine will copy the directory's
* contents, not the directory itself.
* @param destDir Info of the directory to copy to. Must be created beforehand.
- * @return True on success, false if some of the children failed to copy.
- * @throws RemoteException
+ * @throws ResourceException
*/
- private boolean copyDirectoryHelper(DocumentInfo srcDir, DocumentInfo destDir)
- throws RemoteException {
+ private void copyDirectoryHelper(DocumentInfo srcDir, DocumentInfo destDir)
+ throws ResourceException {
// Recurse into directories. Copy children into the new subdirectory.
final String queryColumns[] = new String[] {
Document.COLUMN_DISPLAY_NAME,
@@ -367,19 +401,39 @@
};
Cursor cursor = null;
boolean success = true;
+ // Iterate over srcs in the directory; copy to the destination directory.
+ final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId);
try {
- // Iterate over srcs in the directory; copy to the destination directory.
- final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId);
- cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null);
- while (cursor.moveToNext() && !isCanceled()) {
- DocumentInfo src = DocumentInfo.fromCursor(cursor, srcDir.authority);
- success &= processDocument(src, srcDir, destDir);
+ try {
+ cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null);
+ } catch (RemoteException | RuntimeException e) {
+ throw new ResourceException("Failed to query children of %s due to an exception.",
+ srcDir.derivedUri, e);
}
+
+ DocumentInfo src;
+ while (cursor.moveToNext() && !isCanceled()) {
+ try {
+ src = DocumentInfo.fromCursor(cursor, srcDir.authority);
+ processDocument(src, srcDir, destDir);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Failed to recursively process a file %s due to an exception."
+ .format(srcDir.derivedUri.toString()), e);
+ success = false;
+ }
+ }
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Failed to copy a file %s to %s. "
+ .format(srcDir.derivedUri.toString(), destDir.derivedUri.toString()), e);
+ success = false;
} finally {
IoUtils.closeQuietly(cursor);
}
- return success;
+ if (!success) {
+ throw new RuntimeException("Some files failed to copy during a recursive "
+ + "directory copy.");
+ }
}
/**
@@ -388,85 +442,101 @@
* @param srcUriInfo Info of the file to copy from.
* @param dstUriInfo Info of the *file* to copy to. Must be created beforehand.
* @param mimeType Mime type for the target. Can be different than source for virtual files.
- * @return True on success, false on error.
- * @throws RemoteException
+ * @throws ResourceException
*/
- private boolean copyFileHelper(DocumentInfo src, DocumentInfo dest, String mimeType)
- throws RemoteException {
+ private void copyFileHelper(DocumentInfo src, DocumentInfo dest, String mimeType)
+ throws ResourceException {
CancellationSignal canceller = new CancellationSignal();
+ AssetFileDescriptor srcFileAsAsset = null;
ParcelFileDescriptor srcFile = null;
ParcelFileDescriptor dstFile = null;
InputStream in = null;
OutputStream out = null;
+ boolean success = false;
- boolean success = true;
try {
// If the file is virtual, but can be converted to another format, then try to copy it
// as such format.
if (src.isVirtualDocument()) {
- final AssetFileDescriptor srcFileAsAsset =
- getClient(src).openTypedAssetFileDescriptor(
+ try {
+ srcFileAsAsset = getClient(src).openTypedAssetFileDescriptor(
src.derivedUri, mimeType, null, canceller);
+ } catch (FileNotFoundException | RemoteException | RuntimeException e) {
+ throw new ResourceException("Failed to open a file as asset for %s due to an "
+ + "exception.", src.derivedUri, e);
+ }
srcFile = srcFileAsAsset.getParcelFileDescriptor();
- in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
+ try {
+ in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
+ } catch (IOException e) {
+ throw new ResourceException("Failed to open a file input stream for %s due "
+ + "an exception.", src.derivedUri, e);
+ }
} else {
- srcFile = getClient(src).openFile(src.derivedUri, "r", canceller);
+ try {
+ srcFile = getClient(src).openFile(src.derivedUri, "r", canceller);
+ } catch (FileNotFoundException | RemoteException | RuntimeException e) {
+ throw new ResourceException(
+ "Failed to open a file for %s due to an exception.", src.derivedUri, e);
+ }
in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
}
- dstFile = getClient(dest).openFile(dest.derivedUri, "w", canceller);
+ try {
+ dstFile = getClient(dest).openFile(dest.derivedUri, "w", canceller);
+ } catch (FileNotFoundException | RemoteException | RuntimeException e) {
+ throw new ResourceException("Failed to open the destination file %s for writing "
+ + "due to an exception.", dest.derivedUri, e);
+ }
out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
byte[] buffer = new byte[32 * 1024];
int len;
- while ((len = in.read(buffer)) != -1) {
- if (isCanceled()) {
- if (DEBUG) Log.d(TAG, "Canceled copy mid-copy. Id:" + id);
- success = false;
- break;
+ try {
+ while ((len = in.read(buffer)) != -1) {
+ if (isCanceled()) {
+ throw new ResourceException("Canceled copy mid-copy of %s",
+ src.derivedUri);
+ }
+ out.write(buffer, 0, len);
+ makeCopyProgress(len);
}
- out.write(buffer, 0, len);
- makeCopyProgress(len);
+
+ srcFile.checkError();
+ } catch (IOException e) {
+ throw new ResourceException(
+ "Failed to copy bytes from %s to %s due to an IO exception.",
+ src.derivedUri, dest.derivedUri, e);
}
- srcFile.checkError();
- } catch (IOException e) {
- success = false;
- onFileFailed(src, "Exception thrown while copying from "
- + src.derivedUri + " to " + dest.derivedUri + ".");
-
- if (dstFile != null) {
- try {
- dstFile.closeWithError(e.getMessage());
- } catch (IOException closeError) {
- Log.e(TAG, "Error closing destination", closeError);
- }
+ if (src.isVirtualDocument()) {
+ convertedFiles.add(src);
}
+
+ success = true;
} finally {
+ if (!success) {
+ if (dstFile != null) {
+ try {
+ dstFile.closeWithError("Error copying bytes.");
+ } catch (IOException closeError) {
+ Log.w(TAG, "Error closing destination.", closeError);
+ }
+ }
+
+ if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
+ canceller.cancel();
+ try {
+ DocumentsContract.deleteDocument(getClient(dest), dest.derivedUri);
+ } catch (RemoteException | RuntimeException e) {
+ Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
+ }
+ }
+
// This also ensures the file descriptors are closed.
IoUtils.closeQuietly(in);
IoUtils.closeQuietly(out);
}
-
- if (!success) {
- if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
- canceller.cancel();
- try {
- DocumentsContract.deleteDocument(getClient(dest), dest.derivedUri);
- } catch (RemoteException e) {
- // RemoteExceptions usually signal that the connection is dead, so there's no
- // point attempting to continue. Propagate the exception up so the copy job is
- // cancelled.
- Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
- throw e;
- }
- }
-
- if (src.isVirtualDocument() && success) {
- convertedFiles.add(src);
- }
-
- return success;
}
/**
@@ -475,16 +545,20 @@
*
* @param srcs
* @return Size in bytes.
- * @throws RemoteException
+ * @throws ResourceException
*/
- private long calculateSize(List<DocumentInfo> srcs)
- throws RemoteException {
+ private long calculateSize(List<DocumentInfo> srcs) throws ResourceException {
long result = 0;
for (DocumentInfo src : srcs) {
if (src.isDirectory()) {
// Directories need to be recursed into.
- result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
+ try {
+ result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
+ } catch (RemoteException e) {
+ throw new ResourceException("Failed to obtain the client for %s.",
+ src.derivedUri);
+ }
} else {
result += src.size;
}
@@ -495,10 +569,10 @@
/**
* Calculates (recursively) the cumulative size of all the files under the given directory.
*
- * @throws RemoteException
+ * @throws ResourceException
*/
private static long calculateFileSizesRecursively(
- ContentProviderClient client, Uri uri) throws RemoteException {
+ ContentProviderClient client, Uri uri) throws ResourceException {
final String authority = uri.getAuthority();
final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
final String queryColumns[] = new String[] {
@@ -524,6 +598,9 @@
result += size > 0 ? size : 0;
}
}
+ } catch (RemoteException | RuntimeException e) {
+ throw new ResourceException(
+ "Failed to calculate size for %s due to an exception.", uri, e);
} finally {
IoUtils.closeQuietly(cursor);
}
@@ -533,21 +610,22 @@
/**
* Returns true if {@code doc} is a descendant of {@code parentDoc}.
- * @throws RemoteException
+ * @throws ResourceException
*/
boolean isDescendentOf(DocumentInfo doc, DocumentInfo parent)
- throws RemoteException {
+ throws ResourceException {
if (parent.isDirectory() && doc.authority.equals(parent.authority)) {
- return isChildDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
+ try {
+ return isChildDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
+ } catch (RemoteException | RuntimeException e) {
+ throw new ResourceException(
+ "Failed to check if %s is a child of %s due to an exception.",
+ doc.derivedUri, parent.derivedUri, e);
+ }
}
return false;
}
- private void onFileFailed(DocumentInfo file, String msg) {
- Log.w(TAG, msg);
- onFileFailed(file);
- }
-
@Override
public String toString() {
return new StringBuilder()
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java
index 11c3a29..374d27b 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java
@@ -22,7 +22,6 @@
import android.app.Notification;
import android.app.Notification.Builder;
import android.content.Context;
-import android.os.RemoteException;
import android.util.Log;
import com.android.documentsui.Metrics;
@@ -81,13 +80,13 @@
}
@Override
- void start() throws RemoteException {
+ void start() {
for (DocumentInfo doc : mSrcs) {
if (DEBUG) Log.d(TAG, "Deleting document @ " + doc.derivedUri);
- // TODO: Start using mSrcParent as soon as DocumentsProvider::removeDocument() is
- // implemented.
- if (!deleteDocument(doc)) {
- Log.w(TAG, "Failed to delete document @ " + doc.derivedUri);
+ try {
+ deleteDocument(doc);
+ } catch (ResourceException e) {
+ Log.e(TAG, "Failed to delete document @ " + doc.derivedUri);
onFileFailed(doc);
}
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/Job.java b/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
index 77517ca..572c0d7 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
@@ -116,19 +116,17 @@
listener.onStart(this);
try {
start();
- } catch (Exception e) {
- // In the case of an unmanaged failure, we still want
- // to resolve business in an orderly fashion. That'll
- // ensure the service is shut down and notifications
- // shown/closed.
- Log.e(TAG, "Operation failed due to an exception.", e);
+ } catch (RuntimeException e) {
+ // No exceptions should be thrown here, as all calls to the provider must be
+ // handled within Job implementations. However, just in case catch them here.
+ Log.e(TAG, "Operation failed due to an unhandled runtime exception.", e);
Metrics.logFileOperationErrors(service, operationType, failedFiles);
} finally {
listener.onFinished(this);
}
}
- abstract void start() throws RemoteException;
+ abstract void start();
abstract Notification getSetupNotification();
// TODO: Progress notification for deletes.
@@ -186,15 +184,13 @@
return false;
}
- final boolean deleteDocument(DocumentInfo doc) {
+ final void deleteDocument(DocumentInfo doc) throws ResourceException {
try {
DocumentsContract.deleteDocument(getClient(doc), doc.derivedUri);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to delete file: " + doc.derivedUri, e);
- return false;
+ } catch (Exception e) {
+ throw new ResourceException("Failed to delete file %s due to an exception.",
+ doc.derivedUri, e);
}
-
- return true; // victory dance!
}
Notification getSetupNotification(String content) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
index 9b72077..8835a9f 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
@@ -80,8 +80,8 @@
R.plurals.move_error_notification_title, R.drawable.ic_menu_copy);
}
- boolean processDocument(DocumentInfo src, DocumentInfo srcParent, DocumentInfo dest)
- throws RemoteException {
+ void processDocument(DocumentInfo src, DocumentInfo srcParent, DocumentInfo dest)
+ throws ResourceException {
// TODO: When optimized move kicks in, we're not making any progress updates. FIX IT!
@@ -89,13 +89,19 @@
// If not supported, then fallback to byte-by-byte copy/move.
if (src.authority.equals(dest.authority)) {
if ((src.flags & Document.FLAG_SUPPORTS_MOVE) != 0) {
- if (DocumentsContract.moveDocument(getClient(src), src.derivedUri,
- srcParent != null ? srcParent.derivedUri : mSrcParent.derivedUri,
- dest.derivedUri) == null) {
- onFileFailed(src);
- return false;
+ try {
+ if (DocumentsContract.moveDocument(getClient(src), src.derivedUri,
+ srcParent != null ? srcParent.derivedUri : mSrcParent.derivedUri,
+ dest.derivedUri) == null) {
+ throw new ResourceException("Provider side move failed for document %s.",
+ src.derivedUri);
+ }
+ } catch (RuntimeException | RemoteException e) {
+ throw new ResourceException(
+ "Provider side move failed for document %s due to an exception.",
+ src.derivedUri, e);
}
- return true;
+ return;
}
}
@@ -103,16 +109,12 @@
// conversion, and the source file should not be deleted in such case (as it's a different
// file).
if (src.isVirtualDocument()) {
- Log.w(TAG, "Cannot move virtual files byte by byte.");
- onFileFailed(src);
- return false;
+ throw new ResourceException("Cannot move virtual file %s byte by byte.",
+ src.derivedUri);
}
// If we couldn't do an optimized copy...we fall back to vanilla byte copy.
- boolean copied = byteCopyDocument(src, dest);
-
- // TODO: Replace deleteDocument() with removeDocument() once implemented.
- return copied && !isCanceled() && deleteDocument(src);
+ byteCopyDocument(src, dest);
}
@Override
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/ResourceException.java b/packages/DocumentsUI/src/com/android/documentsui/services/ResourceException.java
new file mode 100644
index 0000000..9c6b51c
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/ResourceException.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 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.documentsui.services;
+
+import android.net.Uri;
+
+public class ResourceException extends Exception {
+ public ResourceException(String message, Exception e) {
+ super(message, e);
+ }
+
+ public ResourceException(String message, Uri uri1, Exception e) {
+ super(message.format(uri1.toString()), e);
+ }
+
+ public ResourceException(String message, Uri uri1, Uri uri2, Exception e) {
+ super(message.format(uri1.toString(), uri2.toString()), e);
+ }
+
+ public ResourceException(String message) {
+ super(message);
+ }
+
+ public ResourceException(String message, Uri uri1) {
+ super(message.format(uri1.toString()));
+ }
+
+ public ResourceException(String message, Uri uri1, Uri uri2) {
+ super(message.format(uri1.toString(), uri2.toString()));
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/ActivityTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/ActivityTest.java
index 34f1120..08d366a 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/ActivityTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/ActivityTest.java
@@ -21,21 +21,22 @@
import static com.android.documentsui.StubProvider.ROOT_1_ID;
import android.app.Activity;
-import android.app.Instrumentation;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.RemoteException;
import android.provider.DocumentsContract.Document;
-import android.support.test.uiautomator.By;
import android.support.test.uiautomator.Configurator;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObjectNotFoundException;
-import android.support.test.uiautomator.Until;
import android.test.ActivityInstrumentationTestCase2;
import android.view.MotionEvent;
+import com.android.documentsui.bots.DirectoryListBot;
+import com.android.documentsui.bots.KeyboardBot;
+import com.android.documentsui.bots.RootsListBot;
+import com.android.documentsui.bots.UiBot;
import com.android.documentsui.model.RootInfo;
/**
@@ -56,7 +57,7 @@
public static final String fileName4 = "poodles.text";
public static final String fileNameNoRename = "NO_RENAMEfile.txt";
- public UiBot bot;
+ public Bots bots;
public UiDevice device;
public Context context;
@@ -76,10 +77,11 @@
device = UiDevice.getInstance(getInstrumentation());
// NOTE: Must be the "target" context, else security checks in content provider will fail.
context = getInstrumentation().getTargetContext();
- bot = new UiBot(device, context, TIMEOUT);
+
+ bots = new Bots(device, context, TIMEOUT);
Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_MOUSE);
- bot.revealLauncher();
+ bots.main.revealLauncher();
mResolver = context.getContentResolver();
mClient = mResolver.acquireUnstableContentProviderClient(DEFAULT_AUTHORITY);
@@ -90,7 +92,7 @@
launchActivity();
- bot.revealApp();
+ bots.main.revealApp();
resetStorage();
}
@@ -125,12 +127,31 @@
}
void assertDefaultContentOfTestDir0() throws UiObjectNotFoundException {
- bot.assertDocumentsCount(ROOT_0_ID, 4);
- bot.assertHasDocuments(fileName1, fileName2, dirName1, fileNameNoRename);
+ bots.roots.openRoot(ROOT_0_ID);
+ bots.directory.assertDocumentsCount(4);
+ bots.directory.assertDocumentsPresent(fileName1, fileName2, dirName1, fileNameNoRename);
}
void assertDefaultContentOfTestDir1() throws UiObjectNotFoundException {
- bot.assertDocumentsCount(ROOT_1_ID, 2);
- bot.assertHasDocuments(fileName3, fileName4);
+ bots.roots.openRoot(ROOT_1_ID);
+ bots.directory.assertDocumentsCount(2);
+ bots.directory.assertDocumentsPresent(fileName3, fileName4);
+ }
+
+ /**
+ * Handy collection of bots for working with Files app.
+ */
+ public static final class Bots {
+ public final UiBot main;
+ public final RootsListBot roots;
+ public final DirectoryListBot directory;
+ public final KeyboardBot keyboard;
+
+ private Bots(UiDevice device, Context context, int timeout) {
+ this.main = new UiBot(device, context, TIMEOUT);
+ this.roots = new RootsListBot(device, context, TIMEOUT);
+ this.directory = new DirectoryListBot(device, context, TIMEOUT);
+ this.keyboard = new KeyboardBot(device, context, TIMEOUT);
+ }
}
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/DownloadsActivityUiTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/DownloadsActivityUiTest.java
index c51f927..79d7887 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/DownloadsActivityUiTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/DownloadsActivityUiTest.java
@@ -52,13 +52,13 @@
public void testWindowTitle() throws Exception {
initTestFiles();
- bot.assertWindowTitle(ROOT_0_ID);
+ bots.main.assertWindowTitle(ROOT_0_ID);
}
public void testFilesListed() throws Exception {
initTestFiles();
- bot.assertHasDocuments("file0.log", "file1.png", "file2.csv");
+ bots.directory.assertDocumentsPresent("file0.log", "file1.png", "file2.csv");
}
public void testFilesList_LiveUpdate() throws Exception {
@@ -66,30 +66,31 @@
mDocsHelper.createDocument(rootDir0, "yummers/sandwich", "Ham & Cheese.sandwich");
- bot.waitForDocument("Ham & Cheese.sandwich");
- bot.assertHasDocuments("file0.log", "file1.png", "file2.csv", "Ham & Cheese.sandwich");
+ bots.directory.waitForDocument("Ham & Cheese.sandwich");
+ bots.directory.assertDocumentsPresent(
+ "file0.log", "file1.png", "file2.csv", "Ham & Cheese.sandwich");
}
public void testDeleteDocument() throws Exception {
initTestFiles();
- bot.clickDocument("file1.png");
+ bots.directory.clickDocument("file1.png");
device.waitForIdle();
- bot.menuDelete().click();
+ bots.main.menuDelete().click();
- bot.waitForDeleteSnackbar();
- assertFalse(bot.hasDocuments("file1.png"));
+ bots.directory.waitForDeleteSnackbar();
+ bots.directory.assertDocumentsAbsent("file1.png");
- bot.waitForDeleteSnackbarGone();
- assertFalse(bot.hasDocuments("file1.png"));
+ bots.directory.waitForDeleteSnackbarGone();
+ bots.directory.assertDocumentsAbsent("file1.png");
}
public void testSupportsShare() throws Exception {
initTestFiles();
- bot.clickDocument("file1.png");
+ bots.directory.clickDocument("file1.png");
device.waitForIdle();
- assertNotNull(bot.menuShare());
+ assertNotNull(bots.main.menuShare());
}
public void testClosesOnBack() throws Exception {
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
index 95515db..34ea96e 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
@@ -43,11 +43,11 @@
public void testRootsListed() throws Exception {
initTestFiles();
- bot.openRoot(ROOT_0_ID);
+ bots.roots.openRoot(ROOT_0_ID);
// Should also have Drive, but that requires pre-configuration of devices
// We omit for now.
- bot.assertHasRoots(
+ bots.roots.assertHasRoots(
"Images",
"Videos",
"Audio",
@@ -60,93 +60,106 @@
public void testFilesListed() throws Exception {
initTestFiles();
- bot.openRoot(ROOT_0_ID);
- bot.assertHasDocuments("file0.log", "file1.png", "file2.csv");
+ bots.roots.openRoot(ROOT_0_ID);
+ bots.directory.assertDocumentsPresent("file0.log", "file1.png", "file2.csv");
}
public void testLoadsHomeDirectoryByDefault() throws Exception {
initTestFiles();
device.waitForIdle();
- bot.assertWindowTitle("Documents");
+ bots.main.assertWindowTitle("Documents");
}
public void testRootClickSetsWindowTitle() throws Exception {
initTestFiles();
- bot.openRoot("Downloads");
- bot.assertWindowTitle("Downloads");
+ bots.roots.openRoot("Downloads");
+ bots.main.assertWindowTitle("Downloads");
}
public void testFilesList_LiveUpdate() throws Exception {
initTestFiles();
- bot.openRoot(ROOT_0_ID);
+ bots.roots.openRoot(ROOT_0_ID);
mDocsHelper.createDocument(rootDir0, "yummers/sandwich", "Ham & Cheese.sandwich");
- bot.waitForDocument("Ham & Cheese.sandwich");
- bot.assertHasDocuments("file0.log", "file1.png", "file2.csv", "Ham & Cheese.sandwich");
+ bots.directory.waitForDocument("Ham & Cheese.sandwich");
+ bots.directory.assertDocumentsPresent(
+ "file0.log", "file1.png", "file2.csv", "Ham & Cheese.sandwich");
+ }
+
+ public void testCreateDirectory() throws Exception {
+ initTestFiles();
+
+ bots.main.openOverflowMenu();
+ bots.main.menuNewFolder().click();
+ bots.main.setDialogText("Kung Fu Panda");
+
+ bots.keyboard.pressEnter();
+
+ bots.directory.assertDocumentsPresent("Kung Fu Panda");
}
public void testDeleteDocument() throws Exception {
initTestFiles();
- bot.openRoot(ROOT_0_ID);
+ bots.roots.openRoot(ROOT_0_ID);
- bot.clickDocument("file1.png");
+ bots.directory.clickDocument("file1.png");
device.waitForIdle();
- bot.menuDelete().click();
+ bots.main.menuDelete().click();
- bot.waitForDeleteSnackbar();
- assertFalse(bot.hasDocuments("file1.png"));
+ bots.directory.waitForDeleteSnackbar();
+ bots.directory.assertDocumentsAbsent("file1.png");
- bot.waitForDeleteSnackbarGone();
- assertFalse(bot.hasDocuments("file1.png"));
+ bots.directory.waitForDeleteSnackbarGone();
+ bots.directory.assertDocumentsAbsent("file1.png");
// Now delete from another root.
- bot.openRoot(ROOT_1_ID);
+ bots.roots.openRoot(ROOT_1_ID);
- bot.clickDocument("poodles.text");
+ bots.directory.clickDocument("poodles.text");
device.waitForIdle();
- bot.menuDelete().click();
+ bots.main.menuDelete().click();
- bot.waitForDeleteSnackbar();
- assertFalse(bot.hasDocuments("poodles.text"));
+ bots.directory.waitForDeleteSnackbar();
+ bots.directory.assertDocumentsAbsent("poodles.text");
- bot.waitForDeleteSnackbarGone();
- assertFalse(bot.hasDocuments("poodles.text"));
+ bots.directory.waitForDeleteSnackbarGone();
+ bots.directory.assertDocumentsAbsent("poodles.text");
}
// Tests that pressing tab switches focus between the roots and directory listings.
public void testKeyboard_tab() throws Exception {
- bot.pressKey(KeyEvent.KEYCODE_TAB);
- bot.assertHasFocus("com.android.documentsui:id/roots_list");
- bot.pressKey(KeyEvent.KEYCODE_TAB);
- bot.assertHasFocus("com.android.documentsui:id/dir_list");
+ bots.main.pressKey(KeyEvent.KEYCODE_TAB);
+ bots.roots.assertHasFocus();
+ bots.main.pressKey(KeyEvent.KEYCODE_TAB);
+ bots.directory.assertHasFocus();
}
// Tests that arrow keys do not switch focus away from the dir list.
public void testKeyboard_arrowsDirList() throws Exception {
for (int i = 0; i < 10; i++) {
- bot.pressKey(KeyEvent.KEYCODE_DPAD_LEFT);
- bot.assertHasFocus("com.android.documentsui:id/dir_list");
+ bots.main.pressKey(KeyEvent.KEYCODE_DPAD_LEFT);
+ bots.directory.assertHasFocus();
}
for (int i = 0; i < 10; i++) {
- bot.pressKey(KeyEvent.KEYCODE_DPAD_RIGHT);
- bot.assertHasFocus("com.android.documentsui:id/dir_list");
+ bots.main.pressKey(KeyEvent.KEYCODE_DPAD_RIGHT);
+ bots.directory.assertHasFocus();
}
}
// Tests that arrow keys do not switch focus away from the roots list.
public void testKeyboard_arrowsRootsList() throws Exception {
- bot.pressKey(KeyEvent.KEYCODE_TAB);
+ bots.main.pressKey(KeyEvent.KEYCODE_TAB);
for (int i = 0; i < 10; i++) {
- bot.pressKey(KeyEvent.KEYCODE_DPAD_RIGHT);
- bot.assertHasFocus("com.android.documentsui:id/roots_list");
+ bots.main.pressKey(KeyEvent.KEYCODE_DPAD_RIGHT);
+ bots.roots.assertHasFocus();
}
for (int i = 0; i < 10; i++) {
- bot.pressKey(KeyEvent.KEYCODE_DPAD_LEFT);
- bot.assertHasFocus("com.android.documentsui:id/roots_list");
+ bots.main.pressKey(KeyEvent.KEYCODE_DPAD_LEFT);
+ bots.roots.assertHasFocus();
}
}
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/RenameDocumentUiTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/RenameDocumentUiTest.java
index 770bc2c..b866033 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/RenameDocumentUiTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/RenameDocumentUiTest.java
@@ -23,8 +23,6 @@
@LargeTest
public class RenameDocumentUiTest extends ActivityTest<FilesActivity> {
- private static final String TAG = "RenamDocumentUiTest";
-
private final String newName = "kitties.log";
public RenameDocumentUiTest() {
@@ -35,125 +33,119 @@
public void setUp() throws Exception {
super.setUp();
initTestFiles();
- bot.openRoot(ROOT_0_ID);
+ bots.roots.openRoot(ROOT_0_ID);
}
public void testRenameEnabled_SingleSelection() throws Exception {
- bot.selectDocument(fileName1);
- bot.openOverflowMenu();
- bot.assertMenuEnabled(R.string.menu_rename, true);
+ bots.directory.selectDocument(fileName1);
+ bots.main.openOverflowMenu();
+ bots.main.assertMenuEnabled(R.string.menu_rename, true);
// Dismiss more options window
device.pressBack();
}
public void testNoRenameSupport_SingleSelection() throws Exception {
- bot.selectDocument(fileNameNoRename);
- bot.openOverflowMenu();
- bot.assertMenuEnabled(R.string.menu_rename, false);
+ bots.directory.selectDocument(fileNameNoRename);
+ bots.main.openOverflowMenu();
+ bots.main.assertMenuEnabled(R.string.menu_rename, false);
// Dismiss more options window
device.pressBack();
}
public void testOneHasRenameSupport_MultipleSelection() throws Exception {
- bot.selectDocument(fileName1);
- bot.selectDocument(fileNameNoRename);
- bot.openOverflowMenu();
- bot.assertMenuEnabled(R.string.menu_rename, false);
+ bots.directory.selectDocument(fileName1);
+ bots.directory.selectDocument(fileNameNoRename);
+ bots.main.openOverflowMenu();
+ bots.main.assertMenuEnabled(R.string.menu_rename, false);
// Dismiss more options window
device.pressBack();
}
public void testRenameDisabled_MultipleSelection() throws Exception {
- bot.selectDocument(fileName1);
- bot.selectDocument(fileName2);
- bot.openOverflowMenu();
- bot.assertMenuEnabled(R.string.menu_rename, false);
+ bots.directory.selectDocument(fileName1);
+ bots.directory.selectDocument(fileName2);
+ bots.main.openOverflowMenu();
+ bots.main.assertMenuEnabled(R.string.menu_rename, false);
// Dismiss more options window
device.pressBack();
}
public void testRenameFile_OkButton() throws Exception {
- bot.selectDocument(fileName1);
- bot.openOverflowMenu();
- bot.openDialog(R.string.menu_rename);
- bot.setDialogText(newName);
+ bots.directory.selectDocument(fileName1);
+ bots.main.openOverflowMenu();
+ bots.main.menuRename().click();
+ bots.main.setDialogText(newName);
device.waitForIdle(TIMEOUT);
- bot.findRenameDialogOkButton().click();
+ bots.main.findRenameDialogOkButton().click();
device.waitForIdle(TIMEOUT);
- bot.assertDocument(fileName1, false);
- bot.assertDocument(newName, true);
- bot.assertDocumentsCount(4);
+ bots.directory.assertDocumentsAbsent(fileName1);
+ bots.directory.assertDocumentsPresent(newName);
+ bots.directory.assertDocumentsCount(4);
}
public void testRenameFile_Enter() throws Exception {
- bot.selectDocument(fileName1);
- bot.openOverflowMenu();
- bot.openDialog(R.string.menu_rename);
- bot.setDialogText(newName);
+ bots.directory.selectDocument(fileName1);
+ bots.main.openOverflowMenu();
+ bots.main.menuRename().click();
+ bots.main.setDialogText(newName);
- pressEnter();
+ bots.keyboard.pressEnter();
- bot.assertDocument(fileName1, false);
- bot.assertDocument(newName, true);
- bot.assertDocumentsCount(4);
+ bots.directory.assertDocumentsAbsent(fileName1);
+ bots.directory.assertDocumentsPresent(newName);
+ bots.directory.assertDocumentsCount(4);
}
public void testRenameFile_Cancel() throws Exception {
- bot.selectDocument(fileName1);
- bot.openOverflowMenu();
- bot.openDialog(R.string.menu_rename);
- bot.setDialogText(newName);
+ bots.directory.selectDocument(fileName1);
+ bots.main.openOverflowMenu();
+ bots.main.menuRename().click();
+ bots.main.setDialogText(newName);
device.waitForIdle(TIMEOUT);
- bot.findRenameDialogCancelButton().click();
+ bots.main.findRenameDialogCancelButton().click();
device.waitForIdle(TIMEOUT);
- bot.assertDocument(fileName1, true);
- bot.assertDocument(newName, false);
- bot.assertDocumentsCount(4);
+ bots.directory.assertDocumentsPresent(fileName1);
+ bots.directory.assertDocumentsAbsent(newName);
+ bots.directory.assertDocumentsCount(4);
}
public void testRenameDir() throws Exception {
String oldName = "Dir1";
String newName = "Dir123";
- bot.selectDocument(oldName);
- bot.openOverflowMenu();
- bot.openDialog(R.string.menu_rename);
- bot.setDialogText(newName);
+ bots.directory.selectDocument(oldName);
+ bots.main.openOverflowMenu();
+ bots.main.menuRename().click();
+ bots.main.setDialogText(newName);
- pressEnter();
+ bots.keyboard.pressEnter();
- bot.assertDocument(oldName, false);
- bot.assertDocument(newName, true);
- bot.assertDocumentsCount(4);
+ bots.directory.assertDocumentsAbsent(oldName);
+ bots.directory.assertDocumentsPresent(newName);
+ bots.directory.assertDocumentsCount(4);
}
public void testRename_NameExists() throws Exception {
// Check that document with the new name exists
- bot.assertDocument(fileName2, true);
- bot.selectDocument(fileName1);
- bot.openOverflowMenu();
- bot.openDialog(R.string.menu_rename);
- bot.setDialogText(fileName2);
+ bots.directory.assertDocumentsPresent(fileName2);
+ bots.directory.selectDocument(fileName1);
+ bots.main.openOverflowMenu();
+ bots.main.menuRename().click();
+ bots.main.setDialogText(fileName2);
- pressEnter();
+ bots.keyboard.pressEnter();
- bot.assertSnackbar(R.string.rename_error);
- bot.assertDocument(fileName1, true);
- bot.assertDocument(fileName2, true);
- bot.assertDocumentsCount(4);
- }
-
- private void pressEnter() {
- device.waitForIdle(TIMEOUT);
- device.pressEnter();
- device.waitForIdle(TIMEOUT);
+ bots.directory.assertSnackbar(R.string.rename_error);
+ bots.directory.assertDocumentsPresent(fileName1);
+ bots.directory.assertDocumentsPresent(fileName2);
+ bots.directory.assertDocumentsCount(4);
}
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/RootUiTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/RootsUiTest.java
similarity index 68%
rename from packages/DocumentsUI/tests/src/com/android/documentsui/RootUiTest.java
rename to packages/DocumentsUI/tests/src/com/android/documentsui/RootsUiTest.java
index 1d1d3b5..dc41a2c 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/RootUiTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/RootsUiTest.java
@@ -18,19 +18,14 @@
import static com.android.documentsui.StubProvider.ROOT_0_ID;
-import android.support.test.uiautomator.Configurator;
-import android.support.test.uiautomator.UiObject;
-import android.support.test.uiautomator.UiObjectNotFoundException;
-import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.LargeTest;
-import android.view.MotionEvent;
@LargeTest
-public class RootUiTest extends ActivityTest<FilesActivity> {
+public class RootsUiTest extends ActivityTest<FilesActivity> {
private static final String TAG = "RootUiTest";
- public RootUiTest() {
+ public RootsUiTest() {
super(FilesActivity.class);
}
@@ -38,14 +33,14 @@
public void setUp() throws Exception {
super.setUp();
initTestFiles();
- bot.openRoot(ROOT_0_ID);
+ bots.roots.openRoot(ROOT_0_ID);
}
public void testRootTapped_GoToRootFromChildDir() throws Exception {
- bot.openDocument(dirName1);
- bot.assertWindowTitle(dirName1);
- bot.openRoot(ROOT_0_ID);
- bot.assertWindowTitle(ROOT_0_ID);
+ bots.directory.openDocument(dirName1);
+ bots.main.assertWindowTitle(dirName1);
+ bots.roots.openRoot(ROOT_0_ID);
+ bots.main.assertWindowTitle(ROOT_0_ID);
assertDefaultContentOfTestDir0();
}
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/SearchViewUiTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/SearchViewUiTest.java
index b8d8795..478c70c 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/SearchViewUiTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/SearchViewUiTest.java
@@ -24,61 +24,59 @@
@LargeTest
public class SearchViewUiTest extends ActivityTest<FilesActivity> {
- private static final String TAG = "SearchViewUiTest";
-
public SearchViewUiTest() {
super(FilesActivity.class);
}
public void testSearchView_ExpandsOnClick() throws Exception {
- bot.openSearchView();
- bot.assertSearchTextFiledAndIcon(true, false);
+ bots.main.openSearchView();
+ bots.main.assertSearchTextFiledAndIcon(true, false);
}
public void testSearchView_CollapsesOnBack() throws Exception {
- bot.openSearchView();
+ bots.main.openSearchView();
device.pressBack();
- bot.assertSearchTextFiledAndIcon(false, true);
+ bots.main.assertSearchTextFiledAndIcon(false, true);
}
public void testSearchView_ClearsTextOnBack() throws Exception {
String query = "file2";
- bot.openSearchView();
- bot.setSearchQuery(query);
+ bots.main.openSearchView();
+ bots.main.setSearchQuery(query);
device.pressBack();
- bot.assertSearchTextFiledAndIcon(false, true);
+ bots.main.assertSearchTextFiledAndIcon(false, true);
}
public void testSearch_ResultsFound() throws Exception {
initTestFiles();
- bot.openRoot(ROOT_0_ID);
+ bots.roots.openRoot(ROOT_0_ID);
assertDefaultContentOfTestDir0();
String query = "file1";
- bot.openSearchView();
- bot.setSearchQuery(query);
- bot.assertSearchTextField(true, query);
+ bots.main.openSearchView();
+ bots.main.setSearchQuery(query);
+ bots.main.assertSearchTextField(true, query);
device.pressEnter();
- bot.assertDocumentsCountOnList(true, 2);
- bot.assertHasDocuments(fileName1, fileName2);
+ bots.directory.assertDocumentsCountOnList(true, 2);
+ bots.directory.assertDocumentsPresent(fileName1, fileName2);
- bot.assertSearchTextField(false, query);
+ bots.main.assertSearchTextField(false, query);
}
public void testSearchResultsFound_ClearsOnBack() throws Exception {
initTestFiles();
- bot.openRoot(ROOT_0_ID);
+ bots.roots.openRoot(ROOT_0_ID);
assertDefaultContentOfTestDir0();
String query = fileName1;
- bot.openSearchView();
- bot.setSearchQuery(query);
+ bots.main.openSearchView();
+ bots.main.setSearchQuery(query);
device.pressEnter();
device.pressBack();
@@ -88,32 +86,32 @@
public void testSearch_NoResults() throws Exception {
initTestFiles();
- bot.openRoot(ROOT_0_ID);
+ bots.roots.openRoot(ROOT_0_ID);
assertDefaultContentOfTestDir0();
String query = "chocolate";
- bot.openSearchView();
- bot.setSearchQuery(query);
+ bots.main.openSearchView();
+ bots.main.setSearchQuery(query);
device.pressEnter();
- bot.assertDocumentsCountOnList(false, 0);
+ bots.directory.assertDocumentsCountOnList(false, 0);
device.waitForIdle();
String msg = String.valueOf(context.getString(R.string.no_results));
- bot.assertMessageTextView(String.format(msg, "TEST_ROOT_0"));
+ bots.directory.assertMessageTextView(String.format(msg, "TEST_ROOT_0"));
- bot.assertSearchTextField(false, query);
+ bots.main.assertSearchTextField(false, query);
}
public void testSearchNoResults_ClearsOnBack() throws Exception {
initTestFiles();
- bot.openRoot(ROOT_0_ID);
+ bots.roots.openRoot(ROOT_0_ID);
assertDefaultContentOfTestDir0();
String query = "chocolate";
- bot.openSearchView();
- bot.setSearchQuery(query);
+ bots.main.openSearchView();
+ bots.main.setSearchQuery(query);
device.pressEnter();
device.pressBack();
@@ -124,30 +122,30 @@
public void testSearchResultsFound_ClearsOnDirectoryChange() throws Exception {
initTestFiles();
- bot.openRoot(ROOT_0_ID);
+ bots.roots.openRoot(ROOT_0_ID);
assertDefaultContentOfTestDir0();
String query = fileName1;
- bot.openSearchView();
- bot.setSearchQuery(query);
+ bots.main.openSearchView();
+ bots.main.setSearchQuery(query);
device.pressEnter();
- bot.openRoot(ROOT_1_ID);
+ bots.roots.openRoot(ROOT_1_ID);
assertDefaultContentOfTestDir1();
- bot.openRoot(ROOT_0_ID);
+ bots.roots.openRoot(ROOT_0_ID);
assertDefaultContentOfTestDir0();
}
public void testSearchIconVisible_RootWithSearchSupport() throws Exception {
- bot.openRoot(ROOT_0_ID);
- bot.assertSearchTextFiledAndIcon(false, true);
+ bots.roots.openRoot(ROOT_0_ID);
+ bots.main.assertSearchTextFiledAndIcon(false, true);
}
public void testSearchIconHidden_RootNoSearchSupport() throws Exception {
- bot.openRoot(ROOT_1_ID);
- bot.assertSearchTextFiledAndIcon(false, false);
+ bots.roots.openRoot(ROOT_1_ID);
+ bots.main.assertSearchTextFiledAndIcon(false, false);
}
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java b/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java
deleted file mode 100644
index d2f8403..0000000
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java
+++ /dev/null
@@ -1,432 +0,0 @@
-/*
- * Copyright (C) 2015 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.documentsui;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertTrue;
-import static junit.framework.Assert.assertFalse;
-
-import android.app.Activity;
-import android.content.Context;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
-import android.support.test.uiautomator.Configurator;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.UiObjectNotFoundException;
-import android.support.test.uiautomator.UiScrollable;
-import android.support.test.uiautomator.UiSelector;
-import android.support.test.uiautomator.Until;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.inputmethod.InputMethodManager;
-
-import junit.framework.Assert;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
-import java.util.regex.Pattern;
-
-/**
- * A test helper class that provides support for controlling DocumentsUI activities
- * programmatically, and making assertions against the state of the UI.
- */
-class UiBot {
- public static final String TARGET_PKG = "com.android.documentsui";
-
- private static final String TAG = "UiBot";
- private static final String LAUNCHER_PKG = "com.android.launcher";
-
- private static final BySelector SNACK_DELETE =
- By.desc(Pattern.compile("^Deleting [0-9]+ file.+"));
-
- private UiDevice mDevice;
- private Context mContext;
- private int mTimeout;
-
- public UiBot(UiDevice device, Context context, int timeout) {
- mDevice = device;
- mContext = context;
- mTimeout = timeout;
- }
-
- UiObject findRoot(String label) throws UiObjectNotFoundException {
- final UiSelector rootsList = new UiSelector().resourceId(
- "com.android.documentsui:id/container_roots").childSelector(
- new UiSelector().resourceId("com.android.documentsui:id/roots_list"));
-
- // We might need to expand drawer if not visible
- if (!new UiObject(rootsList).waitForExists(mTimeout)) {
- Log.d(TAG, "Failed to find roots list; trying to expand");
- final UiSelector hamburger = new UiSelector().resourceId(
- "com.android.documentsui:id/toolbar").childSelector(
- new UiSelector().className("android.widget.ImageButton").clickable(true));
- new UiObject(hamburger).click();
- }
-
- // Wait for the first list item to appear
- new UiObject(rootsList.childSelector(new UiSelector())).waitForExists(mTimeout);
-
- // Now scroll around to find our item
- new UiScrollable(rootsList).scrollIntoView(new UiSelector().text(label));
- return new UiObject(rootsList.childSelector(new UiSelector().text(label)));
- }
-
- void openRoot(String label) throws UiObjectNotFoundException {
- findRoot(label).click();
- mDevice.waitForIdle();
- }
-
- void assertWindowTitle(String expected) {
- // Turns out the title field on a window does not have
- // an id associated with it at runtime (which confuses the hell out of me)
- // In code we address this via "android.R.id.title".
- UiObject2 o = find(By.text(expected));
- // It's a bit of a conceit that we then *assert* that the title
- // is the value that we used to identify the UiObject2.
- // If the preceeding lookup fails, this'll choke with an NPE.
- // But given the issue described in the comment above, we're
- // going to do it anyway. Because we shouldn't be looking up
- // the uiobject by it's expected content :|
- assertEquals(expected, o.getText());
- }
-
- void assertHasRoots(String... labels) throws UiObjectNotFoundException {
- List<String> missing = new ArrayList<>();
- for (String label : labels) {
- if (!findRoot(label).exists()) {
- missing.add(label);
- }
- }
- if (!missing.isEmpty()) {
- Assert.fail(
- "Expected roots " + Arrays.asList(labels) + ", but missing " + missing);
- }
- }
-
- void assertMenuEnabled(int id, boolean enabled) {
- UiObject2 menu= findMenuWithName(mContext.getString(id));
- assertNotNull(menu);
- assertEquals(enabled, menu.isEnabled());
- }
-
- void assertDocumentsCount(int count) throws UiObjectNotFoundException {
- UiObject docsList = findDocumentsList();
- assertEquals(count, docsList.getChildCount());
- }
-
- void assertDocumentsCount(String dir, int count) throws UiObjectNotFoundException {
- openRoot(dir);
- UiObject docsList = findDocumentsList();
- assertEquals(count, docsList.getChildCount());
- }
-
- void assertSearchTextField(boolean isFocused, String query)
- throws UiObjectNotFoundException {
- UiObject textField = findSearchViewTextField();
- UiObject searchIcon = findSearchViewIcon();
-
- assertFalse(searchIcon.exists());
- assertTrue(textField.exists());
- assertEquals(isFocused, textField.isFocused());
- if(query != null) {
- assertEquals(query, textField.getText());
- }
- }
-
- void assertSearchTextFiledAndIcon(boolean searchTextFieldExists, boolean searchIconExists) {
- assertEquals(searchTextFieldExists, findSearchViewTextField().exists());
- assertEquals(searchIconExists, findSearchViewIcon().exists());
- }
-
- void assertHasDocuments(String... labels) throws UiObjectNotFoundException {
- List<String> missing = new ArrayList<>();
- for (String label : labels) {
- if (!findDocument(label).exists()) {
- missing.add(label);
- }
- }
- if (!missing.isEmpty()) {
- Assert.fail(
- "Expected documents " + Arrays.asList(labels) + ", but missing " + missing);
- }
- }
-
- void assertDocument(String name, boolean exists) throws UiObjectNotFoundException {
- UiObject doc = findDocument(name);
- assertEquals(exists, doc.exists());
- }
-
- void assertDocumentsCountOnList(boolean exists, int count) throws UiObjectNotFoundException {
- UiObject docsList = findDocumentsList();
- assertEquals(exists, docsList.exists());
- if(docsList.exists()) {
- assertEquals(count, docsList.getChildCount());
- }
- }
-
- void assertMessageTextView(String message) throws UiObjectNotFoundException {
- UiObject messageTextView = findMessageTextView();
- assertTrue(messageTextView.exists());
-
- String msg = String.valueOf(message);
- assertEquals(String.format(msg, "TEST_ROOT_0"), messageTextView.getText());
-
- }
- void assertSnackbar(int id) {
- assertNotNull(getSnackbar(mContext.getString(id)));
- }
-
- /**
- * Asserts that the specified view or one of its descendents has focus.
- */
- void assertHasFocus(String resourceName) {
- UiObject2 candidate = mDevice.findObject(By.res(resourceName));
- assertNotNull("Expected " + resourceName + " to have focus, but it didn't.",
- candidate.findObject(By.focused(true)));
- }
-
- void openDocument(String label) throws UiObjectNotFoundException {
- int toolType = Configurator.getInstance().getToolType();
- Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_FINGER);
- UiObject doc = findDocument(label);
- doc.click();
- Configurator.getInstance().setToolType(toolType);
- }
-
- void clickDocument(String label) throws UiObjectNotFoundException {
- findDocument(label).click();
- }
-
- void openSearchView() throws UiObjectNotFoundException {
- UiObject searchView = findSearchView();
- searchView.click();
- assertTrue(searchView.exists());
- }
-
- void setSearchQuery(String query) throws UiObjectNotFoundException {
- UiObject searchView = findSearchView();
- assertTrue(searchView.exists());
- UiObject searchTextField = findSearchViewTextField();
- searchTextField.setText(query);
- assertSearchTextField(true, query);
- }
-
- UiObject openOverflowMenu() throws UiObjectNotFoundException {
- UiObject obj = findMenuMoreOptions();
- obj.click();
- mDevice.waitForIdle(mTimeout);
- return obj;
- }
-
- void openDialog(int id) {
- UiObject2 menu= findMenuWithName(mContext.getString(id));
- assertNotNull(menu);
- assertEquals(true, menu.isEnabled());
- menu.click();
- }
-
- void setDialogText(String text) throws UiObjectNotFoundException {
- findDialogEditText().setText(text);
- }
-
- UiObject selectDocument(String label) throws UiObjectNotFoundException {
- UiObject doc = findDocument(label);
- doc.longClick();
- return doc;
- }
-
- UiObject2 getSnackbar(String message) {
- return mDevice.wait(Until.findObject(By.text(message)), mTimeout);
- }
-
- void waitForDeleteSnackbar() {
- mDevice.wait(Until.findObject(SNACK_DELETE), mTimeout);
- }
-
- void waitForDeleteSnackbarGone() {
- // wait a little longer for snackbar to go away, as it disappears after a timeout.
- mDevice.wait(Until.gone(SNACK_DELETE), mTimeout * 2);
- }
-
- void waitForDocument(String label) throws UiObjectNotFoundException {
- findDocument(label).waitForExists(mTimeout);
- }
-
- void switchViewMode() {
- UiObject2 mode = menuGridMode();
- if (mode != null) {
- mode.click();
- } else {
- menuListMode().click();
- }
- }
-
- UiObject2 menuGridMode() {
- // Note that we're using By.desc rather than By.res, because of b/25285770
- return find(By.desc("Grid view"));
- }
-
- UiObject2 menuListMode() {
- // Note that we're using By.desc rather than By.res, because of b/25285770
- return find(By.desc("List view"));
- }
-
- UiObject2 menuDelete() {
- return find(By.res("com.android.documentsui:id/menu_delete"));
- }
-
- UiObject2 menuShare() {
- return find(By.res("com.android.documentsui:id/menu_share"));
- }
-
- private UiObject2 find(BySelector selector) {
- mDevice.wait(Until.findObject(selector), mTimeout);
- return mDevice.findObject(selector);
- }
-
- private UiObject findObject(String resourceId) {
- final UiSelector object = new UiSelector().resourceId(resourceId);
- return mDevice.findObject(object);
- }
-
- private UiObject findObject(String parentResourceId, String childResourceId) {
- final UiSelector selector = new UiSelector()
- .resourceId(parentResourceId)
- .childSelector(new UiSelector().resourceId(childResourceId));
- return mDevice.findObject(selector);
- }
-
- UiObject findDocument(String label) throws UiObjectNotFoundException {
- final UiSelector docList = new UiSelector().resourceId(
- "com.android.documentsui:id/container_directory").childSelector(
- new UiSelector().resourceId("com.android.documentsui:id/dir_list"));
-
- // Wait for the first list item to appear
- new UiObject(docList.childSelector(new UiSelector())).waitForExists(mTimeout);
-
- // new UiScrollable(docList).scrollIntoView(new UiSelector().text(label));
- return mDevice.findObject(docList.childSelector(new UiSelector().text(label)));
- }
-
- boolean hasDocuments(String... labels) throws UiObjectNotFoundException {
- for (String label : labels) {
- if (!findDocument(label).exists()) {
- return false;
- }
- }
- return true;
- }
-
- UiObject findDocumentsList() {
- return findObject(
- "com.android.documentsui:id/container_directory",
- "com.android.documentsui:id/dir_list");
- }
-
- UiObject findSearchView() {
- return findObject("com.android.documentsui:id/menu_search");
- }
-
- UiObject findSearchViewTextField() {
- return findObject("com.android.documentsui:id/menu_search", "android:id/search_src_text");
- }
-
- UiObject findSearchViewIcon() {
- return findObject("com.android.documentsui:id/menu_search", "android:id/search_button");
- }
-
- UiObject findMessageTextView() {
- return findObject(
- "com.android.documentsui:id/container_directory",
- "com.android.documentsui:id/message");
- }
-
- UiObject findActionModeBar() {
- return findObject("android:id/action_mode_bar");
- }
-
- UiObject findDialogEditText() {
- return findObject("android:id/content", "android:id/text1");
- }
-
- UiObject findRenameDialogOkButton() {
- return findObject("android:id/content", "android:id/button1");
- }
-
- UiObject findRenameDialogCancelButton() {
- return findObject("android:id/content", "android:id/button2");
- }
-
- UiObject findMenuLabelWithName(String label) {
- UiSelector selector = new UiSelector().text(label);
- return mDevice.findObject(selector);
- }
-
- UiObject2 findMenuWithName(String label) {
- List<UiObject2> menuItems = mDevice.findObjects(By.clazz("android.widget.LinearLayout"));
- Iterator<UiObject2> it = menuItems.iterator();
-
- UiObject2 menuItem = null;
- while(it.hasNext()) {
- menuItem = it.next();
- UiObject2 text = menuItem.findObject(By.text(label));
- if(text != null) {
- break;
- }
- }
- return menuItem;
- }
-
- UiObject findMenuMoreOptions() {
- UiSelector selector = new UiSelector().className("android.widget.ImageButton")
- .descriptionContains("More options");
- //TODO: use the system string ? android.R.string.action_menu_overflow_description
- return mDevice.findObject(selector);
- }
-
- // Indirect way to detect the keyboard.
- boolean isKeyboardPresent() {
- InputMethodManager inputManager = (InputMethodManager) mContext
- .getSystemService(Context.INPUT_METHOD_SERVICE);
- return inputManager.isAcceptingText();
- }
-
- void dismissKeyboardIfPresent() {
- if(isKeyboardPresent()) {
- mDevice.pressBack();
- }
- }
-
- void revealLauncher() {
- mDevice.pressHome();
- mDevice.wait(Until.hasObject(By.pkg(LAUNCHER_PKG).depth(0)), mTimeout);
- }
-
- void revealApp() {
- mDevice.wait(Until.hasObject(By.pkg(TARGET_PKG).depth(0)), mTimeout);
- mDevice.waitForIdle();
- }
-
- void pressKey(int keyCode) {
- mDevice.pressKeyCode(keyCode);
- }
-}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/bots/BaseBot.java b/packages/DocumentsUI/tests/src/com/android/documentsui/bots/BaseBot.java
new file mode 100644
index 0000000..1d2b47f
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/bots/BaseBot.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2015 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.documentsui.bots;
+
+import static junit.framework.Assert.assertNotNull;
+
+import android.content.Context;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+
+/**
+ * A test helper class that provides support for controlling directory list
+ * and making assertions against the state of it.
+ */
+abstract class BaseBot {
+ final UiDevice mDevice;
+ final Context mContext;
+ final int mTimeout;
+
+ BaseBot(UiDevice device, Context context, int timeout) {
+ mDevice = device;
+ mContext = context;
+ mTimeout = timeout;
+ }
+
+ /**
+ * Asserts that the specified view or one of its descendents has focus.
+ */
+ protected void assertHasFocus(String resourceName) {
+ UiObject2 candidate = mDevice.findObject(By.res(resourceName));
+ assertNotNull("Expected " + resourceName + " to have focus, but it didn't.",
+ candidate.findObject(By.focused(true)));
+ }
+
+ protected UiObject2 find(BySelector selector) {
+ mDevice.wait(Until.findObject(selector), mTimeout);
+ return mDevice.findObject(selector);
+ }
+
+ protected UiObject findObject(String resourceId) {
+ final UiSelector object = new UiSelector().resourceId(resourceId);
+ return mDevice.findObject(object);
+ }
+
+ protected UiObject findObject(String parentResourceId, String childResourceId) {
+ final UiSelector selector = new UiSelector()
+ .resourceId(parentResourceId)
+ .childSelector(new UiSelector().resourceId(childResourceId));
+ return mDevice.findObject(selector);
+ }
+
+ protected void waitForIdle() {
+ mDevice.waitForIdle(mTimeout);
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/bots/DirectoryListBot.java b/packages/DocumentsUI/tests/src/com/android/documentsui/bots/DirectoryListBot.java
new file mode 100644
index 0000000..da9f9c3
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/bots/DirectoryListBot.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2015 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.documentsui.bots;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Configurator;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+import android.view.MotionEvent;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * A test helper class that provides support for controlling directory list
+ * and making assertions against the state of it.
+ */
+public class DirectoryListBot extends BaseBot {
+ private static final String DIR_LIST_ID = "com.android.documentsui:id/dir_list";
+
+ private static final BySelector SNACK_DELETE =
+ By.desc(Pattern.compile("^Deleting [0-9]+ file.+"));
+
+ public DirectoryListBot(UiDevice device, Context context, int timeout) {
+ super(device, context, timeout);
+ }
+
+ public void assertDocumentsCount(int count) throws UiObjectNotFoundException {
+ UiObject docsList = findDocumentsList();
+ assertEquals(count, docsList.getChildCount());
+ }
+
+ public void assertDocumentsPresent(String... labels) throws UiObjectNotFoundException {
+ List<String> absent = new ArrayList<>();
+ for (String label : labels) {
+ if (!findDocument(label).exists()) {
+ absent.add(label);
+ }
+ }
+ if (!absent.isEmpty()) {
+ Assert.fail("Expected documents " + Arrays.asList(labels)
+ + ", but missing " + absent);
+ }
+ }
+
+ public void assertDocumentsAbsent(String... labels) throws UiObjectNotFoundException {
+ List<String> found = new ArrayList<>();
+ for (String label : labels) {
+ if (findDocument(label).exists()) {
+ found.add(label);
+ }
+ }
+ if (!found.isEmpty()) {
+ Assert.fail("Expected documents not present" + Arrays.asList(labels)
+ + ", but present " + found);
+ }
+ }
+
+ public void assertDocumentsCountOnList(boolean exists, int count) throws UiObjectNotFoundException {
+ UiObject docsList = findDocumentsList();
+ assertEquals(exists, docsList.exists());
+ if(docsList.exists()) {
+ assertEquals(count, docsList.getChildCount());
+ }
+ }
+
+ public void assertMessageTextView(String message) throws UiObjectNotFoundException {
+ UiObject messageTextView = findMessageTextView();
+ assertTrue(messageTextView.exists());
+
+ String msg = String.valueOf(message);
+ assertEquals(String.format(msg, "TEST_ROOT_0"), messageTextView.getText());
+
+ }
+
+ private UiObject findMessageTextView() {
+ return findObject(
+ "com.android.documentsui:id/container_directory",
+ "com.android.documentsui:id/message");
+ }
+
+ public void assertSnackbar(int id) {
+ assertNotNull(getSnackbar(mContext.getString(id)));
+ }
+
+ public void openDocument(String label) throws UiObjectNotFoundException {
+ int toolType = Configurator.getInstance().getToolType();
+ Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_FINGER);
+ UiObject doc = findDocument(label);
+ doc.click();
+ Configurator.getInstance().setToolType(toolType);
+ }
+
+ public void clickDocument(String label) throws UiObjectNotFoundException {
+ findDocument(label).click();
+ }
+
+ public UiObject selectDocument(String label) throws UiObjectNotFoundException {
+ UiObject doc = findDocument(label);
+ doc.longClick();
+ return doc;
+ }
+
+ public UiObject2 getSnackbar(String message) {
+ return mDevice.wait(Until.findObject(By.text(message)), mTimeout);
+ }
+
+ public void waitForDeleteSnackbar() {
+ mDevice.wait(Until.findObject(SNACK_DELETE), mTimeout);
+ }
+
+ public void waitForDeleteSnackbarGone() {
+ // wait a little longer for snackbar to go away, as it disappears after a timeout.
+ mDevice.wait(Until.gone(SNACK_DELETE), mTimeout * 2);
+ }
+
+ public void waitForDocument(String label) throws UiObjectNotFoundException {
+ findDocument(label).waitForExists(mTimeout);
+ }
+
+ public UiObject findDocument(String label) throws UiObjectNotFoundException {
+ final UiSelector docList = new UiSelector().resourceId(
+ "com.android.documentsui:id/container_directory").childSelector(
+ new UiSelector().resourceId(DIR_LIST_ID));
+
+ // Wait for the first list item to appear
+ new UiObject(docList.childSelector(new UiSelector())).waitForExists(mTimeout);
+
+ // new UiScrollable(docList).scrollIntoView(new UiSelector().text(label));
+ return mDevice.findObject(docList.childSelector(new UiSelector().text(label)));
+ }
+
+ public boolean hasDocuments(String... labels) throws UiObjectNotFoundException {
+ for (String label : labels) {
+ if (!findDocument(label).exists()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private UiObject findDocumentsList() {
+ return findObject(
+ "com.android.documentsui:id/container_directory",
+ DIR_LIST_ID);
+ }
+
+ public void assertHasFocus() {
+ assertHasFocus(DIR_LIST_ID);
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/bots/KeyboardBot.java b/packages/DocumentsUI/tests/src/com/android/documentsui/bots/KeyboardBot.java
new file mode 100644
index 0000000..4c47cfa
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/bots/KeyboardBot.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 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.documentsui.bots;
+
+import android.content.Context;
+import android.support.test.uiautomator.UiDevice;
+import android.view.inputmethod.InputMethodManager;
+
+/**
+ * A test helper class that provides support for keyboard manipulation.
+ */
+public class KeyboardBot extends BaseBot {
+
+ public KeyboardBot(UiDevice device, Context context, int timeout) {
+ super(device, context, timeout);
+ }
+
+ public void dismissKeyboardIfPresent() {
+ if(isKeyboardPresent()) {
+ mDevice.pressBack();
+ }
+ }
+
+ // Indirect way to detect the keyboard.
+ private boolean isKeyboardPresent() {
+ InputMethodManager inputManager = (InputMethodManager) mContext
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ return inputManager.isAcceptingText();
+ }
+
+ public void pressEnter() {
+ waitForIdle();
+ mDevice.pressEnter();
+ waitForIdle();
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/bots/RootsListBot.java b/packages/DocumentsUI/tests/src/com/android/documentsui/bots/RootsListBot.java
new file mode 100644
index 0000000..356fd01
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/bots/RootsListBot.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2015 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.documentsui.bots;
+
+import android.content.Context;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A test helper class that provides support for controlling and asserting against
+ * the roots list drawer.
+ */
+public class RootsListBot extends BaseBot {
+ private static final String ROOTS_LIST_ID = "com.android.documentsui:id/roots_list";
+ private static final String TAG = "RootsListBot";
+
+ public RootsListBot(UiDevice device, Context context, int timeout) {
+ super(device, context, timeout);
+ }
+
+ private UiObject findRoot(String label) throws UiObjectNotFoundException {
+ final UiSelector rootsList = new UiSelector().resourceId(
+ "com.android.documentsui:id/container_roots").childSelector(
+ new UiSelector().resourceId(ROOTS_LIST_ID));
+
+ // We might need to expand drawer if not visible
+ if (!new UiObject(rootsList).waitForExists(mTimeout)) {
+ Log.d(TAG, "Failed to find roots list; trying to expand");
+ final UiSelector hamburger = new UiSelector().resourceId(
+ "com.android.documentsui:id/toolbar").childSelector(
+ new UiSelector().className("android.widget.ImageButton").clickable(true));
+ new UiObject(hamburger).click();
+ }
+
+ // Wait for the first list item to appear
+ new UiObject(rootsList.childSelector(new UiSelector())).waitForExists(mTimeout);
+
+ // Now scroll around to find our item
+ new UiScrollable(rootsList).scrollIntoView(new UiSelector().text(label));
+ return new UiObject(rootsList.childSelector(new UiSelector().text(label)));
+ }
+
+ public void openRoot(String label) throws UiObjectNotFoundException {
+ findRoot(label).click();
+ mDevice.waitForIdle();
+ }
+
+ public void assertHasRoots(String... labels) throws UiObjectNotFoundException {
+ List<String> missing = new ArrayList<>();
+ for (String label : labels) {
+ if (!findRoot(label).exists()) {
+ missing.add(label);
+ }
+ }
+ if (!missing.isEmpty()) {
+ Assert.fail(
+ "Expected roots " + Arrays.asList(labels) + ", but missing " + missing);
+ }
+ }
+
+ public void assertHasFocus() {
+ assertHasFocus(ROOTS_LIST_ID);
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/bots/UiBot.java b/packages/DocumentsUI/tests/src/com/android/documentsui/bots/UiBot.java
new file mode 100644
index 0000000..fe2a3c3
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/bots/UiBot.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2015 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.documentsui.bots;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+
+import com.android.documentsui.R;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A test helper class that provides support for controlling DocumentsUI activities
+ * programmatically, and making assertions against the state of the UI.
+ *
+ * <p>Support for working directly with Roots and Directory view can be found
+ * in the respective bots.
+ */
+public class UiBot extends BaseBot {
+ public static final String TARGET_PKG = "com.android.documentsui";
+ private static final String LAUNCHER_PKG = "com.android.launcher";
+
+ public UiBot(UiDevice device, Context context, int timeout) {
+ super(device, context, timeout);
+ }
+
+ public void assertWindowTitle(String expected) {
+ // Turns out the title field on a window does not have
+ // an id associated with it at runtime (which confuses the hell out of me)
+ // In code we address this via "android.R.id.title".
+ UiObject2 o = find(By.text(expected));
+ // It's a bit of a conceit that we then *assert* that the title
+ // is the value that we used to identify the UiObject2.
+ // If the preceeding lookup fails, this'll choke with an NPE.
+ // But given the issue described in the comment above, we're
+ // going to do it anyway. Because we shouldn't be looking up
+ // the uiobject by it's expected content :|
+ assertEquals(expected, o.getText());
+ }
+
+ public void assertMenuEnabled(int id, boolean enabled) {
+ UiObject2 menu= findMenuWithName(mContext.getString(id));
+ assertNotNull(menu);
+ assertEquals(enabled, menu.isEnabled());
+ }
+
+ public void assertSearchTextField(boolean isFocused, String query)
+ throws UiObjectNotFoundException {
+ UiObject textField = findSearchViewTextField();
+ UiObject searchIcon = findSearchViewIcon();
+
+ assertFalse(searchIcon.exists());
+ assertTrue(textField.exists());
+ assertEquals(isFocused, textField.isFocused());
+ if(query != null) {
+ assertEquals(query, textField.getText());
+ }
+ }
+
+ public void assertSearchTextFiledAndIcon(boolean searchTextFieldExists, boolean searchIconExists) {
+ assertEquals(searchTextFieldExists, findSearchViewTextField().exists());
+ assertEquals(searchIconExists, findSearchViewIcon().exists());
+ }
+
+ public void openSearchView() throws UiObjectNotFoundException {
+ UiObject searchView = findSearchView();
+ searchView.click();
+ assertTrue(searchView.exists());
+ }
+
+ public void setSearchQuery(String query) throws UiObjectNotFoundException {
+ UiObject searchView = findSearchView();
+ assertTrue(searchView.exists());
+ UiObject searchTextField = findSearchViewTextField();
+ searchTextField.setText(query);
+ assertSearchTextField(true, query);
+ }
+
+ public UiObject openOverflowMenu() throws UiObjectNotFoundException {
+ UiObject obj = findMenuMoreOptions();
+ obj.click();
+ mDevice.waitForIdle(mTimeout);
+ return obj;
+ }
+
+ public void setDialogText(String text) throws UiObjectNotFoundException {
+ findDialogEditText().setText(text);
+ }
+
+ void switchViewMode() {
+ UiObject2 mode = menuGridMode();
+ if (mode != null) {
+ mode.click();
+ } else {
+ menuListMode().click();
+ }
+ }
+
+ UiObject2 menuGridMode() {
+ // Note that we're using By.desc rather than By.res, because of b/25285770
+ return find(By.desc("Grid view"));
+ }
+
+ UiObject2 menuListMode() {
+ // Note that we're using By.desc rather than By.res, because of b/25285770
+ return find(By.desc("List view"));
+ }
+
+ public UiObject2 menuDelete() {
+ return find(By.res("com.android.documentsui:id/menu_delete"));
+ }
+
+ public UiObject2 menuShare() {
+ return find(By.res("com.android.documentsui:id/menu_share"));
+ }
+
+ public UiObject2 menuRename() {
+ return findMenuWithName(mContext.getString(R.string.menu_rename));
+ }
+
+ public UiObject2 menuNewFolder() {
+ return findMenuWithName(mContext.getString(R.string.menu_create_dir));
+ }
+
+ UiObject findSearchView() {
+ return findObject("com.android.documentsui:id/menu_search");
+ }
+
+ UiObject findSearchViewTextField() {
+ return findObject("com.android.documentsui:id/menu_search", "android:id/search_src_text");
+ }
+
+ UiObject findSearchViewIcon() {
+ return findObject("com.android.documentsui:id/menu_search", "android:id/search_button");
+ }
+
+ UiObject findActionModeBar() {
+ return findObject("android:id/action_mode_bar");
+ }
+
+ public UiObject findDialogEditText() {
+ return findObject("android:id/content", "android:id/text1");
+ }
+
+ public UiObject findRenameDialogOkButton() {
+ return findObject("android:id/content", "android:id/button1");
+ }
+
+ public UiObject findRenameDialogCancelButton() {
+ return findObject("android:id/content", "android:id/button2");
+ }
+
+ UiObject findMenuLabelWithName(String label) {
+ UiSelector selector = new UiSelector().text(label);
+ return mDevice.findObject(selector);
+ }
+
+ UiObject2 findMenuWithName(String label) {
+ List<UiObject2> menuItems = mDevice.findObjects(By.clazz("android.widget.LinearLayout"));
+ Iterator<UiObject2> it = menuItems.iterator();
+
+ UiObject2 menuItem = null;
+ while(it.hasNext()) {
+ menuItem = it.next();
+ UiObject2 text = menuItem.findObject(By.text(label));
+ if(text != null) {
+ break;
+ }
+ }
+ return menuItem;
+ }
+
+ UiObject findMenuMoreOptions() {
+ UiSelector selector = new UiSelector().className("android.widget.ImageButton")
+ .descriptionContains("More options");
+ //TODO: use the system string ? android.R.string.action_menu_overflow_description
+ return mDevice.findObject(selector);
+ }
+
+ public void revealLauncher() {
+ mDevice.pressHome();
+ mDevice.wait(Until.hasObject(By.pkg(LAUNCHER_PKG).depth(0)), mTimeout);
+ }
+
+ public void revealApp() {
+ mDevice.wait(Until.hasObject(By.pkg(TARGET_PKG).depth(0)), mTimeout);
+ mDevice.waitForIdle();
+ }
+
+ public void pressKey(int keyCode) {
+ mDevice.pressKeyCode(keyCode);
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
index a1c6dab..9147a57 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
@@ -21,7 +21,6 @@
import android.app.Notification;
import android.app.Notification.Builder;
import android.content.Context;
-import android.os.RemoteException;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
@@ -38,7 +37,7 @@
}
@Override
- void start() throws RemoteException {
+ void start() {
mStarted = true;
}
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java
index ec2a173..434631e 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java
@@ -413,7 +413,7 @@
* @return true if the menu key should be enabled
*/
private static final String ENABLE_MENU_KEY_FILE = "/data/local/enable_menu_key";
- private boolean shouldEnableMenuKey() {
+ public boolean shouldEnableMenuKey() {
final Resources res = getResources();
final boolean configDisabled = res.getBoolean(R.bool.config_disableMenuKeyInLockScreen);
final boolean isTestHarness = ActivityManager.isRunningInTestHarness();
@@ -421,15 +421,6 @@
return !configDisabled || isTestHarness || fileOverride;
}
- public boolean handleMenuKey() {
- // The following enables the MENU key to work for testing automation
- if (shouldEnableMenuKey()) {
- dismiss();
- return true;
- }
- return false;
- }
-
public void setViewMediatorCallback(ViewMediatorCallback viewMediatorCallback) {
mViewMediatorCallback = viewMediatorCallback;
// Update ViewMediator with the current input method requirements
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardPinBasedInputView.java
index cedd88d..fe98cb8 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardPinBasedInputView.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardPinBasedInputView.java
@@ -89,7 +89,12 @@
return true;
}
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
- int number = keyCode - KeyEvent.KEYCODE_0 ;
+ int number = keyCode - KeyEvent.KEYCODE_0;
+ performNumberClick(number);
+ return true;
+ }
+ if (keyCode >= KeyEvent.KEYCODE_NUMPAD_0 && keyCode <= KeyEvent.KEYCODE_NUMPAD_9) {
+ int number = keyCode - KeyEvent.KEYCODE_NUMPAD_0;
performNumberClick(number);
return true;
}
diff --git a/packages/SettingsLib/res/values/arrays.xml b/packages/SettingsLib/res/values/arrays.xml
index 1bce7f9..525d6f4 100644
--- a/packages/SettingsLib/res/values/arrays.xml
+++ b/packages/SettingsLib/res/values/arrays.xml
@@ -344,28 +344,6 @@
<item>midi</item>
</string-array>
- <!-- Possible values for user theme in Display Settings.
- Do not translate. -->
- <string-array name="night_mode_entries" translatable="false">
- <!-- Do not translate. -->
- <item>@string/night_mode_no</item>
- <!-- Do not translate. -->
- <item>@string/night_mode_yes</item>
- <!-- Do not translate. -->
- <item>@string/night_mode_auto</item>
- </string-array>
-
- <!-- These values should match up with the MODE_NIGHT constants in UiModeManager.
- Do not translate. -->
- <string-array name="night_mode_values" translatable="false">
- <!-- Do not translate. -->
- <item>1</item>
- <!-- Do not translate. -->
- <item>2</item>
- <!-- Do not translate. -->
- <item>0</item>
- </string-array>
-
<!-- Display color space adjustment modes for developers -->
<string-array name="simulate_color_space_entries" translatable="false">
<item>@string/daltonizer_mode_disabled</item>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 57c5684..676bf5f 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -236,10 +236,14 @@
<string name="tts_settings">Text-to-speech settings</string>
<!-- TTS option item name in the main settings screen -->
<string name="tts_settings_title">Text-to-speech output</string>
- <!-- On main TTS Settings screen, in default settings section, setting default speech rate for synthesized voice -->
+ <!-- On main TTS Settings screen, in default settings section, setting default speech rate for synthesized voice -->
<string name="tts_default_rate_title">Speech rate</string>
<!-- On main TTS Settings screen, summary for default speech rate for synthesized voice -->
<string name="tts_default_rate_summary">Speed at which the text is spoken</string>
+ <!-- On main TTS Settings screen, in default settings section, setting default pitch for synthesized voice -->
+ <string name="tts_default_pitch_title">Pitch</string>
+ <!-- On main TTS Settings screen, summary for default pitch for synthesized voice -->
+ <string name="tts_default_pitch_summary">Affects the tone of the synthesized speech</string>
<!-- On main TTS Settings screen, in default settings section, setting default language for synthesized voice -->
<string name="tts_default_lang_title">Language</string>
<!-- Entry in the TTS engine language/locale picker, when selected will try to default to the system language [CHAR LIMIT=50] -->
@@ -673,17 +677,6 @@
<!-- Services settings screen, setting option summary for the user to go to the screen to view running services -->
<string name="runningservices_settings_summary">View and control currently running services</string>
- <!-- Sound & display settings screen, setting option name to change the user interface theme [CHAR LIMIT=30] -->
- <string name="night_mode_title">Night mode</string>
- <!-- Sound & display settings screen, setting option summary to change the user interface theme [CHAR LIMIT=100] -->
- <string name="night_mode_summary">%s</string>
- <!-- Sound & display settings screen, theme setting value to prefer a light-colored user interface [CHAR LIMIT=30] -->
- <string name="night_mode_no">Disabled</string>
- <!-- Sound & display settings screen, theme setting value to prefer a dark-colored user interface [CHAR LIMIT=30] -->
- <string name="night_mode_yes">Always on</string>
- <!-- Sound & display settings screen, theme setting value to automatically switch between a light- or dark-colored user interface [CHAR LIMIT=30] -->
- <string name="night_mode_auto">Automatic</string>
-
<!-- Developer settings: enable WebView multiprocess name [CHAR LIMIT=30] -->
<string name="enable_webview_multiprocess">Enable multiprocess WebView</string>
<!-- Developer settings: enable WebView multiprocess summary [CHAR LIMIT=60] -->
diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java
index 9926ae5..20aca0e 100644
--- a/packages/Shell/src/com/android/shell/BugreportProgressService.java
+++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java
@@ -41,6 +41,8 @@
import libcore.io.Streams;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.MetricsProto.MetricsEvent;
import com.google.android.collect.Lists;
import android.accounts.Account;
@@ -506,6 +508,7 @@
* Cancels a bugreport upon user's request.
*/
private void cancel(int id) {
+ MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_CANCEL);
Log.v(TAG, "cancel: ID=" + id);
final BugreportInfo info = getInfo(id);
if (info != null && !info.finished) {
@@ -582,6 +585,7 @@
* change its values.
*/
private void launchBugreportInfoDialog(int id) {
+ MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_DETAILS);
// Copy values so it doesn't lock mProcesses while UI is being updated
final String name, title, description;
final BugreportInfo info = getInfo(id);
@@ -610,6 +614,7 @@
* upon receiving a {@link #INTENT_BUGREPORT_STARTED}.
*/
private void takeScreenshot(int id, boolean delayed) {
+ MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SCREENSHOT);
if (getInfo(id) == null) {
// Most likely am killed Shell before user tapped the notification. Since system might
// be too busy anwyays, it's better to ignore the notification and switch back to the
@@ -764,6 +769,12 @@
info.renameScreenshots(mScreenshotsDir);
info.bugreportFile = bugreportFile;
+ final int max = intent.getIntExtra(EXTRA_MAX, -1);
+ if (max != -1) {
+ MetricsLogger.histogram(this, "dumpstate_duration", max);
+ info.max = max;
+ }
+
final File screenshot = getFileExtra(intent, EXTRA_SCREENSHOT);
if (screenshot != null) {
info.addScreenshot(screenshot);
@@ -859,6 +870,7 @@
* intent, but issuing a warning dialog the first time.
*/
private void shareBugreport(int id, BugreportInfo sharedInfo) {
+ MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SHARE);
BugreportInfo info = getInfo(id);
if (info == null) {
// Service was terminated but notification persisted
@@ -1139,9 +1151,16 @@
if (info == null) {
return;
}
+ if (title != null && !title.equals(info.title)) {
+ MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_TITLE_CHANGED);
+ }
info.title = title;
+ if (description != null && !description.equals(info.description)) {
+ MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_DESCRIPTION_CHANGED);
+ }
info.description = description;
if (name != null && !name.equals(info.name)) {
+ MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_NAME_CHANGED);
info.name = name;
updateProgress(info);
}
@@ -1229,7 +1248,7 @@
/**
* Sets its internal state and displays the dialog.
*/
- private void initialize(Context context, BugreportInfo info) {
+ private void initialize(final Context context, BugreportInfo info) {
// First initializes singleton.
if (mDialog == null) {
@SuppressLint("InflateParams")
@@ -1263,6 +1282,8 @@
@Override
public void onClick(DialogInterface dialog, int id)
{
+ MetricsLogger.action(context,
+ MetricsEvent.ACTION_BUGREPORT_DETAILS_CANCELED);
if (!mTempName.equals(mSavedName)) {
// Must restore dumpstate's name since it was changed
// before user clicked OK.
@@ -1307,6 +1328,7 @@
@Override
public void onClick(View view) {
+ MetricsLogger.action(context, MetricsEvent.ACTION_BUGREPORT_DETAILS_SAVED);
sanitizeName();
final String name = mInfoName.getText().toString();
final String title = mInfoTitle.getText().toString();
@@ -1525,7 +1547,7 @@
return "id: " + id + ", pid: " + pid + ", name: " + name + ", finished: " + finished
+ "\n\ttitle: " + title + "\n\tdescription: " + description
+ "\n\tfile: " + bugreportFile + "\n\tscreenshots: " + screenshotFiles
- + "\n\tprogress: " + progress + "/" + max + "(" + percent + ")"
+ + "\n\tprogress: " + progress + "/" + max + " (" + percent + ")"
+ "\n\tlast_update: " + getFormattedLastUpdate()
+ "\naddingDetailsToZip: " + addingDetailsToZip
+ " addedDetailsToZip: " + addedDetailsToZip;
diff --git a/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java b/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java
index 712347a..57809ac 100644
--- a/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java
+++ b/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java
@@ -106,6 +106,10 @@
try {
ArrayList<String> sourceAssets = new ArrayList<String>();
for (String host : hostList) {
+ // "*.example.tld" is validated via https://example.tld
+ if (host.startsWith("*.")) {
+ host = host.substring(2);
+ }
sourceAssets.add(createWebAssetString(scheme, host));
}
extras.putStringArrayList(DirectStatementService.EXTRA_SOURCE_ASSET_DESCRIPTORS,
diff --git a/packages/SystemUI/res/layout/status_bar_notification_keyguard_overflow.xml b/packages/SystemUI/res/layout/status_bar_notification_keyguard_overflow.xml
index f699fce..7df6bc6 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_keyguard_overflow.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_keyguard_overflow.xml
@@ -22,6 +22,7 @@
android:focusable="true"
android:clickable="true"
>
+
<com.android.systemui.statusbar.NotificationBackgroundView android:id="@+id/backgroundNormal"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -65,4 +66,9 @@
/>
</com.android.keyguard.AlphaOptimizedLinearLayout>
+ <com.android.systemui.statusbar.notification.FakeShadowView
+ android:id="@+id/fake_shadow"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
</com.android.systemui.statusbar.NotificationOverflowContainer>
diff --git a/packages/SystemUI/res/layout/status_bar_notification_row.xml b/packages/SystemUI/res/layout/status_bar_notification_row.xml
index c4c45bb..e456984 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_row.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_row.xml
@@ -76,4 +76,9 @@
android:layout_height="wrap_content"
/>
+ <com.android.systemui.statusbar.notification.FakeShadowView
+ android:id="@+id/fake_shadow"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
</com.android.systemui.statusbar.ExpandableNotificationRow>
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index e4effd4..baec8ef 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -13,25 +13,15 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/volume_dialog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/volume_dialog_margin_bottom"
android:background="@drawable/volume_dialog_background"
- android:translationZ="4dp" >
-
- <com.android.keyguard.AlphaOptimizedImageButton
- android:id="@+id/volume_expand_button"
- style="@style/VolumeButtons"
- android:layout_width="@dimen/volume_button_size"
- android:layout_height="@dimen/volume_button_size"
- android:layout_alignParentLeft="true"
- android:clickable="true"
- android:soundEffectsEnabled="false"
- android:src="@drawable/ic_volume_collapse_animation"
- tools:ignore="RtlHardcoded" />
+ android:translationZ="4dp"
+ android:paddingTop="8dp">
<LinearLayout
android:id="@+id/volume_dialog_content"
@@ -39,9 +29,15 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="8dp"
- android:paddingTop="8dp" >
+ android:paddingStart="8dp">
<!-- volume rows added and removed here! :-) -->
+ <LinearLayout
+ android:id="@+id/volume_row_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/volume_button_size"
+ android:orientation="vertical"/>
<include layout="@layout/volume_zen_footer" />
@@ -49,4 +45,18 @@
<include layout="@layout/tuner_zen_mode_panel" />
</LinearLayout>
+ <com.android.keyguard.AlphaOptimizedImageButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/volume_expand_button"
+ style="@style/VolumeButtons"
+ android:layout_width="@dimen/volume_button_size"
+ android:layout_height="@dimen/volume_button_size"
+ android:clickable="true"
+ android:soundEffectsEnabled="false"
+ android:src="@drawable/ic_volume_collapse_animation"
+ tools:ignore="RtlHardcoded"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentTop="true"/>
+
</RelativeLayout>
diff --git a/packages/SystemUI/res/layout/volume_dialog_row.xml b/packages/SystemUI/res/layout/volume_dialog_row.xml
index 91e931d..57bac41 100644
--- a/packages/SystemUI/res/layout/volume_dialog_row.xml
+++ b/packages/SystemUI/res/layout/volume_dialog_row.xml
@@ -13,13 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
- android:id="@+id/volume_dialog_row"
- android:paddingEnd="8dp"
- android:paddingStart="8dp" >
+ android:id="@+id/volume_dialog_row" >
<TextView
android:id="@+id/volume_row_header"
@@ -31,7 +30,8 @@
android:paddingBottom="0dp"
android:paddingEnd="12dp"
android:paddingStart="12dp"
- android:paddingTop="4dp" />
+ android:paddingTop="4dp"
+ android:visibility="gone" />
<com.android.keyguard.AlphaOptimizedImageButton
android:id="@+id/volume_row_icon"
@@ -55,12 +55,4 @@
android:paddingEnd="8dp"
android:paddingStart="8dp" />
- <com.android.keyguard.AlphaOptimizedImageButton
- android:id="@+id/volume_settings_button"
- style="@style/VolumeButtons"
- android:layout_width="@dimen/volume_button_size"
- android:layout_height="@dimen/volume_button_size"
- android:layout_alignParentEnd="true"
- android:layout_below="@id/volume_row_header" />
-
</RelativeLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/volume_zen_footer.xml b/packages/SystemUI/res/layout/volume_zen_footer.xml
index 28447d7..f30023d 100644
--- a/packages/SystemUI/res/layout/volume_zen_footer.xml
+++ b/packages/SystemUI/res/layout/volume_zen_footer.xml
@@ -32,9 +32,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
- android:orientation="horizontal"
- android:paddingEnd="8dp"
- android:paddingStart="8dp" >
+ android:orientation="horizontal" >
<ImageView
android:id="@+id/volume_zen_icon"
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
index 681b39e..41cce74 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
@@ -18,11 +18,14 @@
import android.content.Context;
import android.util.Log;
+import android.view.View;
import android.view.ViewGroup;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.ViewMediatorCallback;
+import com.android.systemui.statusbar.ScrimView;
import com.android.systemui.statusbar.phone.KeyguardBouncer;
+import com.android.systemui.statusbar.phone.ScrimController;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.StatusBarWindowManager;
@@ -66,4 +69,9 @@
ViewGroup container) {
return new KeyguardBouncer(context, callback, lockPatternUtils, windowManager, container);
}
+
+ public ScrimController createScrimController(ScrimView scrimBehind, ScrimView scrimInFront,
+ View headsUpScrim, boolean scrimSrcEnabled) {
+ return new ScrimController(scrimBehind, scrimInFront, headsUpScrim, scrimSrcEnabled);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
index 81e1581..d3536a4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -17,6 +17,7 @@
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.GridLayoutManager;
@@ -42,7 +43,7 @@
* This adds itself to the status bar window, so it can appear on top of quick settings and
* *someday* do fancy animations to get into/out of it.
*/
-public class QSCustomizer extends LinearLayout implements AnimatorListener, OnClickListener {
+public class QSCustomizer extends LinearLayout implements OnClickListener {
private final QSDetailClipper mClipper;
@@ -94,7 +95,7 @@
isShown = true;
mPhoneStatusBar.getStatusBarWindow().addView(this);
setTileSpecs();
- mClipper.animateCircularClip(x, y, true, this);
+ mClipper.animateCircularClip(x, y, true, null);
new TileQueryHelper(mContext, mHost).setListener(mTileAdapter);
}
}
@@ -102,7 +103,7 @@
public void hide(int x, int y) {
if (isShown) {
isShown = false;
- mClipper.animateCircularClip(x, y, false, this);
+ mClipper.animateCircularClip(x, y, false, mCollapseAnimationListener);
}
}
@@ -144,27 +145,19 @@
}
}
- @Override
- public void onAnimationEnd(Animator animation) {
- if (!isShown) {
- mPhoneStatusBar.getStatusBarWindow().removeView(this);
+ private final AnimatorListener mCollapseAnimationListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!isShown) {
+ mPhoneStatusBar.getStatusBarWindow().removeView(QSCustomizer.this);
+ }
}
- }
- @Override
- public void onAnimationCancel(Animator animation) {
- if (!isShown) {
- mPhoneStatusBar.getStatusBarWindow().removeView(this);
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (!isShown) {
+ mPhoneStatusBar.getStatusBarWindow().removeView(QSCustomizer.this);
+ }
}
- }
-
- @Override
- public void onAnimationStart(Animator animation) {
- // Don't care.
- }
-
- @Override
- public void onAnimationRepeat(Animator animation) {
- // Don't care.
- }
+ };
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
index 315c509..effe581 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
@@ -35,6 +35,7 @@
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.statusbar.notification.FakeShadowView;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
/**
@@ -155,6 +156,7 @@
}
};
private float mShadowAlpha = 1.0f;
+ private FakeShadowView mFakeShadow;
public ActivatableNotificationView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -180,6 +182,7 @@
protected void onFinishInflate() {
super.onFinishInflate();
mBackgroundNormal = (NotificationBackgroundView) findViewById(R.id.backgroundNormal);
+ mFakeShadow = (FakeShadowView) findViewById(R.id.fake_shadow);
mBackgroundDimmed = (NotificationBackgroundView) findViewById(R.id.backgroundDimmed);
mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg);
mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim);
@@ -852,6 +855,14 @@
}
}
+ @Override
+ public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
+ int outlineTranslation) {
+ mFakeShadow.setFakeShadowTranslationZ(shadowIntensity * (getTranslationZ()
+ + FakeShadowView.SHADOW_SIBLING_TRESHOLD), outlineAlpha, shadowYEnd,
+ outlineTranslation);
+ }
+
public interface OnActivatedListener {
void onActivated(ActivatableNotificationView view);
void onActivationReset(ActivatableNotificationView view);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index ea8b75e..a0c63be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -2139,15 +2139,21 @@
protected void updatePublicContentView(Entry entry,
StatusBarNotification sbn) {
final RemoteViews publicContentView = entry.cachedPublicContentView;
- if (publicContentView != null && entry.getPublicContentView() != null) {
+ View inflatedView = entry.getPublicContentView();
+ if (entry.autoRedacted && publicContentView != null && inflatedView != null) {
final boolean disabledByPolicy =
!adminAllowsUnredactedNotifications(entry.notification.getUserId());
- publicContentView.setTextViewText(android.R.id.title,
- mContext.getString(disabledByPolicy
- ? com.android.internal.R.string.notification_hidden_by_policy_text
- : com.android.internal.R.string.notification_hidden_text));
- publicContentView.reapply(sbn.getPackageContext(mContext),
- entry.getPublicContentView(), mOnClickHandler);
+ String notificationHiddenText = mContext.getString(disabledByPolicy
+ ? com.android.internal.R.string.notification_hidden_by_policy_text
+ : com.android.internal.R.string.notification_hidden_text);
+ TextView titleView = (TextView) inflatedView.findViewById(android.R.id.title);
+ if (titleView != null
+ && !titleView.getText().toString().equals(notificationHiddenText)) {
+ publicContentView.setTextViewText(android.R.id.title, notificationHiddenText);
+ publicContentView.reapply(sbn.getPackageContext(mContext),
+ inflatedView, mOnClickHandler);
+ entry.row.onNotificationUpdated(entry);
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index c73e115..c72cec3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -80,6 +80,7 @@
/** Are we showing the "public" version */
private boolean mShowingPublic;
private boolean mSensitive;
+ private boolean mSensitiveHiddenInGeneral;
private boolean mShowingPublicInitialized;
private boolean mHideSensitiveForIntrinsicHeight;
@@ -1041,8 +1042,9 @@
getShowingLayout().requestSelectLayout(needsAnimation || isUserLocked());
}
- public void setSensitive(boolean sensitive) {
+ public void setSensitive(boolean sensitive, boolean hideSensitive) {
mSensitive = sensitive;
+ mSensitiveHiddenInGeneral = hideSensitive;
}
public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
@@ -1114,7 +1116,8 @@
private void updateClearability() {
// public versions cannot be dismissed
- mVetoButton.setVisibility(isClearable() && !mShowingPublic ? View.VISIBLE : View.GONE);
+ mVetoButton.setVisibility(isClearable() && (!mShowingPublic
+ || !mSensitiveHiddenInGeneral) ? View.VISIBLE : View.GONE);
}
public void setChildrenExpanded(boolean expanded, boolean animate) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java
index 782a38c..f98e87d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java
@@ -70,6 +70,11 @@
}
}
+ @Override
+ public float getOutlineAlpha() {
+ return mOutlineAlpha;
+ }
+
protected void setOutlineRect(RectF rect) {
if (rect != null) {
setOutlineRect(rect.left, rect.top, rect.right, rect.bottom);
@@ -80,6 +85,11 @@
}
}
+ @Override
+ public int getOutlineTranslation() {
+ return mCustomOutline ? mOutlineRect.left : 0;
+ }
+
protected void setOutlineRect(float left, float top, float right, float bottom) {
setOutlineRect(true, left, top, right, bottom);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
index 3176e2e..c9e1cff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
@@ -399,6 +399,18 @@
return false;
}
+ public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
+ int outlineTranslation) {
+ }
+
+ public float getOutlineAlpha() {
+ return 0.0f;
+ }
+
+ public int getOutlineTranslation() {
+ return 0;
+ }
+
/**
* A listener notifying when {@link #getActualHeight} changes.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
index 647f0bf..bc85922 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
@@ -523,7 +523,13 @@
}
public void setDark(boolean dark, boolean fade, long delay) {
- if (mDark == dark || mContractedChild == null) return;
+ setDark(dark, fade, delay, false /* force */);
+ }
+
+ public void setDark(boolean dark, boolean fade, long delay, boolean force) {
+ if ((!force && mDark == dark) || mContractedChild == null) {
+ return;
+ }
mDark = dark;
dark = dark && !mShowingLegacyBackground;
if (mVisibleType == VISIBLE_TYPE_CONTRACTED || !dark) {
@@ -571,7 +577,6 @@
selectLayout(false /* animate */, true /* force */);
if (mContractedChild != null) {
mContractedWrapper.notifyContentUpdated(entry.notification);
- mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
}
if (mExpandedChild != null) {
mExpandedWrapper.notifyContentUpdated(entry.notification);
@@ -579,6 +584,7 @@
if (mHeadsUpChild != null) {
mHeadsUpWrapper.notifyContentUpdated(entry.notification);
}
+ setDark(mDark, false /* animate */, 0 /* delay */, true /* force */);
}
private void updateSingleLineView() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/FakeShadowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/FakeShadowView.java
new file mode 100644
index 0000000..32c26ba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/FakeShadowView.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.notification;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Outline;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.widget.LinearLayout;
+
+import com.android.systemui.statusbar.AlphaOptimizedFrameLayout;
+
+/**
+ * A view used to cast a shadow of a certain size on another view
+ */
+public class FakeShadowView extends AlphaOptimizedFrameLayout {
+ public static final float SHADOW_SIBLING_TRESHOLD = 0.1f;
+
+ private View mFakeShadow;
+ private float mOutlineAlpha;
+
+ public FakeShadowView(Context context) {
+ this(context, null);
+ }
+
+ public FakeShadowView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FakeShadowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public FakeShadowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mFakeShadow = new View(context);
+ mFakeShadow.setVisibility(INVISIBLE);
+ mFakeShadow.setLayoutParams(new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ (int) (48 * getResources().getDisplayMetrics().density)));
+ mFakeShadow.setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRect(0, 0, getWidth(), mFakeShadow.getHeight());
+ outline.setAlpha(mOutlineAlpha);
+ }
+ });
+ addView(mFakeShadow);
+ }
+
+ public void setFakeShadowTranslationZ(float fakeShadowTranslationZ, float outlineAlpha,
+ int shadowYEnd, int outlineTranslation) {
+ if (fakeShadowTranslationZ == 0.0f) {
+ mFakeShadow.setVisibility(INVISIBLE);
+ } else {
+ mFakeShadow.setVisibility(VISIBLE);
+ mFakeShadow.setTranslationZ(fakeShadowTranslationZ);
+ mFakeShadow.setTranslationX(outlineTranslation);
+ mFakeShadow.setTranslationY(shadowYEnd - mFakeShadow.getHeight());
+ if (outlineAlpha != mOutlineAlpha) {
+ mOutlineAlpha = outlineAlpha;
+ mFakeShadow.invalidateOutline();
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index b9e1ad2..ee88b00 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -16,7 +16,11 @@
package com.android.systemui.statusbar.phone;
+import android.app.ActivityManager;
import android.content.Context;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Slog;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
@@ -42,6 +46,8 @@
*/
public class KeyguardBouncer {
+ final static private String TAG = "KeyguardBouncer";
+
protected Context mContext;
protected ViewMediatorCallback mCallback;
protected LockPatternUtils mLockPatternUtils;
@@ -73,6 +79,11 @@
}
public void show(boolean resetSecuritySelection) {
+ final int keyguardUserId = KeyguardUpdateMonitor.getCurrentUser();
+ if (keyguardUserId == UserHandle.USER_SYSTEM && UserManager.isSplitSystemUser()) {
+ // In split system user mode, we never unlock system user.
+ return;
+ }
mFalsingManager.onBouncerShown();
ensureView();
if (resetSecuritySelection) {
@@ -84,14 +95,25 @@
return;
}
- // Try to dismiss the Keyguard. If no security pattern is set, this will dismiss the whole
- // Keyguard. If we need to authenticate, show the bouncer.
- if (!mKeyguardView.dismiss()) {
- mShowingSoon = true;
-
- // Split up the work over multiple frames.
- DejankUtils.postAfterTraversal(mShowRunnable);
+ final int activeUserId = ActivityManager.getCurrentUser();
+ final boolean allowDismissKeyguard =
+ !(UserManager.isSplitSystemUser() && activeUserId == UserHandle.USER_SYSTEM)
+ && activeUserId == keyguardUserId;
+ // If allowed, try to dismiss the Keyguard. If no security auth (password/pin/pattern) is
+ // set, this will dismiss the whole Keyguard. Otherwise, show the bouncer.
+ if (allowDismissKeyguard && mKeyguardView.dismiss()) {
+ return;
}
+
+ // This condition may indicate an error on Android, so log it.
+ if (!allowDismissKeyguard) {
+ Slog.w(TAG, "User can't dismiss keyguard: " + activeUserId + " != " + keyguardUserId);
+ }
+
+ mShowingSoon = true;
+
+ // Split up the work over multiple frames.
+ DejankUtils.postAfterTraversal(mShowRunnable);
}
private final Runnable mShowRunnable = new Runnable() {
@@ -258,19 +280,8 @@
return mKeyguardView == null || mKeyguardView.getSecurityMode() != SecurityMode.None;
}
- public boolean onMenuPressed() {
- ensureView();
- if (mKeyguardView.handleMenuKey()) {
-
- // We need to show it in case it is secure. If not, it will get dismissed in any case.
- mRoot.setVisibility(View.VISIBLE);
- mFalsingManager.onBouncerShown();
- mKeyguardView.requestFocus();
- mKeyguardView.onResume();
- return true;
- } else {
- return false;
- }
+ public boolean shouldDismissOnMenuPressed() {
+ return mKeyguardView.shouldEnableMenuKey();
}
public boolean interceptMediaKey(KeyEvent event) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 1a0acbe..05ae41b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -94,7 +94,7 @@
private TextView mClockView;
private View mReserveNotificationSpace;
private View mQsNavbarScrim;
- private NotificationsQuickSettingsContainer mNotificationContainerParent;
+ protected NotificationsQuickSettingsContainer mNotificationContainerParent;
protected NotificationStackScrollLayout mNotificationStackScroller;
private boolean mAnimateNextTopPaddingChange;
@@ -879,7 +879,11 @@
mQsTracking = false;
mTrackingPointer = -1;
trackMovement(event);
- flingQsWithCurrentVelocity(y, event.getActionMasked() == MotionEvent.ACTION_CANCEL);
+ float fraction = getQsExpansionFraction();
+ if (fraction != 0f || y >= mInitialTouchY) {
+ flingQsWithCurrentVelocity(y,
+ event.getActionMasked() == MotionEvent.ACTION_CANCEL);
+ }
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index d60ea20..e344df2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -111,6 +111,7 @@
import com.android.systemui.Interpolators;
import com.android.systemui.Prefs;
import com.android.systemui.R;
+import com.android.systemui.SystemUIFactory;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.classifier.FalsingManager;
import com.android.systemui.doze.DozeHost;
@@ -771,8 +772,8 @@
ScrimView scrimBehind = (ScrimView) mStatusBarWindow.findViewById(R.id.scrim_behind);
ScrimView scrimInFront = (ScrimView) mStatusBarWindow.findViewById(R.id.scrim_in_front);
View headsUpScrim = mStatusBarWindow.findViewById(R.id.heads_up_scrim);
- mScrimController = new ScrimController(scrimBehind, scrimInFront, headsUpScrim,
- mScrimSrcModeEnabled);
+ mScrimController = SystemUIFactory.getInstance().createScrimController(
+ scrimBehind, scrimInFront, headsUpScrim, mScrimSrcModeEnabled);
mHeadsUpManager.addListener(mScrimController);
mStackScroller.setScrimController(mScrimController);
mScrimController.setBackDropView(mBackdrop);
@@ -1415,7 +1416,7 @@
if (showingPublic) {
updatePublicContentView(ent, ent.notification);
}
- ent.row.setSensitive(sensitive);
+ ent.row.setSensitive(sensitive, hideSensitive);
if (ent.autoRedacted && ent.legacy) {
// TODO: Also fade this? Or, maybe easier (and better), provide a dark redacted form
// for legacy auto redacted notifications.
@@ -3878,7 +3879,13 @@
}
public boolean onMenuPressed() {
- return mState == StatusBarState.KEYGUARD && mStatusBarKeyguardViewManager.onMenuPressed();
+ if (mDeviceInteractive && mState != StatusBarState.SHADE
+ && mStatusBarKeyguardViewManager.shouldDismissOnMenuPressed()) {
+ animateCollapsePanels(
+ CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL /* flags */, true /* force */);
+ return true;
+ }
+ return false;
}
public void endAffordanceLaunch() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index f310c2c2..fe76ae7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -56,16 +56,16 @@
private static final int TAG_START_ALPHA = R.id.scrim_alpha_start;
private static final int TAG_END_ALPHA = R.id.scrim_alpha_end;
- private final ScrimView mScrimBehind;
+ protected final ScrimView mScrimBehind;
private final ScrimView mScrimInFront;
private final UnlockMethodCache mUnlockMethodCache;
private final View mHeadsUpScrim;
- private boolean mKeyguardShowing;
+ protected boolean mKeyguardShowing;
private float mFraction;
private boolean mDarkenWhileDragging;
- private boolean mBouncerShowing;
+ protected boolean mBouncerShowing;
private boolean mWakeAndUnlocking;
private boolean mAnimateChange;
private boolean mUpdatePending;
@@ -203,7 +203,7 @@
mUpdatePending = true;
}
- private void updateScrims() {
+ protected void updateScrims() {
if (mAnimateKeyguardFadingOut || mForceHideScrims) {
setScrimInFrontColor(0f);
setScrimBehindColor(0f);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 7a05b8f..0e84f733 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -509,8 +509,8 @@
return !(mLastShowing && !mLastOccluded) || mLastBouncerShowing || mLastRemoteInputActive;
}
- public boolean onMenuPressed() {
- return mBouncer.onMenuPressed();
+ public boolean shouldDismissOnMenuPressed() {
+ return mBouncer.shouldDismissOnMenuPressed();
}
public boolean interceptMediaKey(KeyEvent event) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index f078b53..fe06c3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -63,6 +63,7 @@
import com.android.systemui.statusbar.NotificationSettingsIconRow.SettingsIconRowListener;
import com.android.systemui.statusbar.StackScrollerDecorView;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.FakeShadowView;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
import com.android.systemui.statusbar.phone.ScrimController;
@@ -70,6 +71,8 @@
import com.android.systemui.statusbar.policy.ScrollAdapter;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.HashSet;
/**
@@ -93,7 +96,7 @@
private static final int INVALID_POINTER = -1;
private ExpandHelper mExpandHelper;
- private SwipeHelper mSwipeHelper;
+ private NotificationSwipeHelper mSwipeHelper;
private boolean mSwipingInProgress;
private int mCurrentStackHeight = Integer.MAX_VALUE;
private final Paint mBackgroundPaint = new Paint();
@@ -277,6 +280,7 @@
private int mBgColor;
private float mDimAmount;
private ValueAnimator mDimAnimator;
+ private ArrayList<ExpandableView> mTmpSortedChildren = new ArrayList<>();
private Animator.AnimatorListener mDimEndListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
@@ -292,6 +296,31 @@
}
};
private ViewGroup mQsContainer;
+ private boolean mContinuousShadowUpdate;
+ private ViewTreeObserver.OnPreDrawListener mShadowUpdater
+ = new ViewTreeObserver.OnPreDrawListener() {
+
+ @Override
+ public boolean onPreDraw() {
+ updateViewShadows();
+ return true;
+ }
+ };
+ private Comparator<ExpandableView> mViewPositionComparator = new Comparator<ExpandableView>() {
+ @Override
+ public int compare(ExpandableView view, ExpandableView otherView) {
+ float endY = view.getTranslationY() + view.getActualHeight();
+ float otherEndY = otherView.getTranslationY() + otherView.getActualHeight();
+ if (endY < otherEndY) {
+ return -1;
+ } else if (endY > otherEndY) {
+ return 1;
+ } else {
+ // The two notifications end at the same location
+ return 0;
+ }
+ }
+ };
public NotificationStackScrollLayout(Context context) {
this(context, null);
@@ -667,6 +696,7 @@
}
mSwipedOutViews.add(v);
mAmbientState.onDragFinished(v);
+ updateContinuousShadowDrawing();
if (v instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) v;
if (row.isHeadsUp()) {
@@ -689,6 +719,7 @@
@Override
public void onChildSnappedBack(View animView, float targetLeft) {
mAmbientState.onDragFinished(animView);
+ updateContinuousShadowDrawing();
if (!mDragAnimPendingChildren.contains(animView)) {
if (mAnimationsEnabled) {
mSnappedBackChildren.add(animView);
@@ -721,6 +752,7 @@
mFalsingManager.onNotificatonStartDismissing();
setSwipingInProgress(true);
mAmbientState.onBeginDrag(v);
+ updateContinuousShadowDrawing();
if (mAnimationsEnabled && (mIsExpanded || !isPinnedHeadsUp(v))) {
mDragAnimPendingChildren.add(v);
mNeedsAnimation = true;
@@ -2242,6 +2274,7 @@
setAnimationRunning(true);
mAnimationEvents.clear();
updateBackground();
+ updateViewShadows();
} else {
applyCurrentState();
}
@@ -2824,6 +2857,43 @@
}
runAnimationFinishedRunnables();
updateBackground();
+ updateViewShadows();
+ }
+
+ private void updateViewShadows() {
+ // we need to work around an issue where the shadow would not cast between siblings when
+ // their z difference is between 0 and 0.1
+
+ // Lefts first sort by Z difference
+ for (int i = 0; i < getChildCount(); i++) {
+ ExpandableView child = (ExpandableView) getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ mTmpSortedChildren.add(child);
+ }
+ }
+ Collections.sort(mTmpSortedChildren, mViewPositionComparator);
+
+ // Now lets update the shadow for the views
+ ExpandableView previous = null;
+ for (int i = 0; i < mTmpSortedChildren.size(); i++) {
+ ExpandableView expandableView = mTmpSortedChildren.get(i);
+ float translationZ = expandableView.getTranslationZ();
+ float otherZ = previous == null ? translationZ : previous.getTranslationZ();
+ float diff = otherZ - translationZ;
+ if (diff <= 0.0f || diff >= FakeShadowView.SHADOW_SIBLING_TRESHOLD) {
+ // There is no fake shadow to be drawn
+ expandableView.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
+ } else {
+ float yLocation = previous.getTranslationY() + previous.getActualHeight() -
+ expandableView.getTranslationY();
+ expandableView.setFakeShadowIntensity(diff / FakeShadowView.SHADOW_SIBLING_TRESHOLD,
+ previous.getOutlineAlpha(), (int) yLocation,
+ previous.getOutlineTranslation());
+ }
+ previous = expandableView;
+ }
+
+ mTmpSortedChildren.clear();
}
public void goToFullShade(long delay) {
@@ -3045,7 +3115,7 @@
disableClipOptimization();
}
handleDismissAllClipping();
- if (mCurrIconRow != null & mCurrIconRow.isVisible()) {
+ if (mCurrIconRow != null && mCurrIconRow.isVisible()) {
mCurrIconRow.getNotificationParent().animateTranslateNotification(0 /* left target */);
}
}
@@ -3262,6 +3332,7 @@
getViewTreeObserver().removeOnPreDrawListener(mBackgroundUpdater);
}
mAnimationRunning = animationRunning;
+ updateContinuousShadowDrawing();
}
}
@@ -3549,6 +3620,18 @@
}
}
+ private void updateContinuousShadowDrawing() {
+ boolean continuousShadowUpdate = mAnimationRunning
+ || !mAmbientState.getDraggedViews().isEmpty();
+ if (continuousShadowUpdate != mContinuousShadowUpdate) {
+ if (continuousShadowUpdate) {
+ getViewTreeObserver().addOnPreDrawListener(mShadowUpdater);
+ } else {
+ getViewTreeObserver().removeOnPreDrawListener(mShadowUpdater);
+ }
+ }
+ }
+
static class AnimationEvent {
static AnimationFilter[] FILTERS = new AnimationFilter[] {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
index e87b363..d78d626 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -24,6 +24,7 @@
import com.android.systemui.R;
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.notification.FakeShadowView;
import java.util.ArrayList;
import java.util.HashSet;
@@ -605,20 +606,40 @@
private void updateZValuesForState(StackScrollState resultState,
StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
int childCount = algorithmState.visibleChildren.size();
- int childrenOnTop = 0;
+ float childrenOnTop = 0.0f;
for (int i = childCount - 1; i >= 0; i--) {
ExpandableView child = algorithmState.visibleChildren.get(i);
StackViewState childViewState = resultState.getViewStateForView(child);
if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
// We are in the bottom stack
float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
- childViewState.zTranslation = mZBasicHeight
- - numItemsAbove * mZDistanceBetweenElements;
+ float zSubtraction;
+ if (numItemsAbove <= 1.0f) {
+ float factor = 0.2f;
+ // Lets fade in slower to the threshold to make the shadow fade in look nicer
+ if (numItemsAbove <= factor) {
+ zSubtraction = FakeShadowView.SHADOW_SIBLING_TRESHOLD
+ * numItemsAbove * (1.0f / factor);
+ } else {
+ zSubtraction = FakeShadowView.SHADOW_SIBLING_TRESHOLD
+ + (numItemsAbove - factor) * (1.0f / (1.0f - factor))
+ * (mZDistanceBetweenElements
+ - FakeShadowView.SHADOW_SIBLING_TRESHOLD);
+ }
+ } else {
+ zSubtraction = numItemsAbove * mZDistanceBetweenElements;
+ }
+ childViewState.zTranslation = mZBasicHeight - zSubtraction;
} else if (child.mustStayOnScreen()
&& childViewState.yTranslation < ambientState.getTopPadding()
+ ambientState.getStackTranslation()) {
- // TODO; do this more cleanly
- childrenOnTop++;
+ if (childrenOnTop != 0.0f) {
+ childrenOnTop++;
+ } else {
+ float overlap = ambientState.getTopPadding()
+ + ambientState.getStackTranslation() - childViewState.yTranslation;
+ childrenOnTop += Math.min(1.0f, overlap / childViewState.height);
+ }
childViewState.zTranslation = mZBasicHeight
+ childrenOnTop * mZDistanceBetweenElements;
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java b/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java
index fac6338..0089fa9 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java
@@ -101,7 +101,26 @@
@Override
public void run() {
if (mState != STATE_NO_PIP) {
- // TODO: check whether PIP task is closed.
+ StackInfo stackInfo = null;
+ try {
+ stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
+ if (stackInfo == null) {
+ Log.w(TAG, "There is no pinned stack");
+ closePipInternal(false);
+ return;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "getStackInfo failed", e);
+ return;
+ }
+ for (int i = stackInfo.taskIds.length - 1; i >= 0; --i) {
+ if (stackInfo.taskIds[i] == mPipTaskId) {
+ // PIP task is still alive.
+ return;
+ }
+ }
+ // PIP task doesn't exist anymore in PINNED_STACK.
+ closePipInternal(true);
}
}
};
@@ -203,12 +222,18 @@
* Closes PIP (PIPed activity and PIP system UI).
*/
public void closePip() {
+ closePipInternal(true);
+ }
+
+ private void closePipInternal(boolean removePipStack) {
mState = STATE_NO_PIP;
mPipTaskId = TASK_ID_NO_PIP;
- try {
- mActivityManager.removeStack(PINNED_STACK_ID);
- } catch (RemoteException e) {
- Log.e(TAG, "removeStack failed", e);
+ if (removePipStack) {
+ try {
+ mActivityManager.removeStack(PINNED_STACK_ID);
+ } catch (RemoteException e) {
+ Log.e(TAG, "removeStack failed", e);
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java
index 1810c1c..1d5ca04 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java
@@ -25,6 +25,7 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
+import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.PixelFormat;
@@ -42,6 +43,7 @@
import android.provider.Settings.Global;
import android.util.DisplayMetrics;
import android.util.Log;
+import android.util.Slog;
import android.util.SparseBooleanArray;
import android.view.Gravity;
import android.view.MotionEvent;
@@ -49,7 +51,6 @@
import android.view.View.AccessibilityDelegate;
import android.view.View.OnAttachStateChangeListener;
import android.view.View.OnClickListener;
-import android.view.View.OnLayoutChangeListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
@@ -91,39 +92,40 @@
public static final String SHOW_FULL_ZEN = "sysui_show_full_zen";
private static final long USER_ATTEMPT_GRACE_PERIOD = 1000;
- private static final int WAIT_FOR_RIPPLE = 200;
private final Context mContext;
private final H mHandler = new H();
private final VolumeDialogController mController;
- private final CustomDialog mDialog;
- private final ViewGroup mDialogView;
- private final ViewGroup mDialogContentView;
- private final ImageButton mExpandButton;
- private final View mSettingsButton;
- private final List<VolumeRow> mRows = new ArrayList<VolumeRow>();
+ private CustomDialog mDialog;
+ private ViewGroup mDialogView;
+ private ViewGroup mDialogContentView;
+ private ViewGroup mVolumeRowContainer;
+ private ImageButton mExpandButton;
+ private final List<VolumeRow> mRows = new ArrayList<>();
private final SpTexts mSpTexts;
private final SparseBooleanArray mDynamic = new SparseBooleanArray();
private final KeyguardManager mKeyguard;
private final AudioManager mAudioManager;
- private final int mExpandButtonAnimationDuration;
- private final ZenFooter mZenFooter;
+ private int mExpandButtonAnimationDuration;
+ private ZenFooter mZenFooter;
private final LayoutTransition mLayoutTransition;
private final Object mSafetyWarningLock = new Object();
private final Accessibility mAccessibility = new Accessibility();
private final ColorStateList mActiveSliderTint;
private final ColorStateList mInactiveSliderTint;
- private final VolumeDialogMotion mMotion;
+ private VolumeDialogMotion mMotion;
+ private final int mWindowType;
+ private final ZenModeController mZenModeController;
private boolean mShowing;
private boolean mExpanded;
+
private int mActiveStream;
private boolean mShowHeaders = VolumePrefs.DEFAULT_SHOW_HEADERS;
private boolean mAutomute = VolumePrefs.DEFAULT_ENABLE_AUTOMUTE;
private boolean mSilentMode = VolumePrefs.DEFAULT_ENABLE_SILENT_MODE;
private State mState;
- private int mExpandButtonRes;
private boolean mExpandButtonAnimationRunning;
private SafetyWarningDialog mSafetyWarning;
private Callback mCallback;
@@ -131,22 +133,43 @@
private boolean mPendingRecheckAll;
private long mCollapseTime;
private boolean mHovering = false;
- private int mLastActiveStream;
+ private int mDensity;
private boolean mShowFullZen;
- private final TunerZenModePanel mZenPanel;
+ private TunerZenModePanel mZenPanel;
public VolumeDialog(Context context, int windowType, VolumeDialogController controller,
ZenModeController zenModeController, Callback callback) {
mContext = context;
mController = controller;
mCallback = callback;
+ mWindowType = windowType;
+ mZenModeController = zenModeController;
mSpTexts = new SpTexts(mContext);
mKeyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mActiveSliderTint = loadColorStateList(R.color.system_accent_color);
+ mInactiveSliderTint = loadColorStateList(R.color.volume_slider_inactive);
+ mLayoutTransition = new LayoutTransition();
+ mLayoutTransition.setDuration(new ValueAnimator().getDuration() / 2);
+ initDialog();
+
+ mAccessibility.init();
+
+ controller.addCallback(mControllerCallbackH, mHandler);
+ controller.getState();
+ TunerService.get(mContext).addTunable(this, SHOW_FULL_ZEN);
+
+ final Configuration currentConfig = mContext.getResources().getConfiguration();
+ mDensity = currentConfig.densityDpi;
+ }
+
+ private void initDialog() {
mDialog = new CustomDialog(mContext);
+ mHovering = false;
+ mShowing = false;
final Window window = mDialog.getWindow();
window.requestFeature(Window.FEATURE_NO_TITLE);
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
@@ -160,7 +183,7 @@
mDialog.setCanceledOnTouchOutside(true);
final Resources res = mContext.getResources();
final WindowManager.LayoutParams lp = window.getAttributes();
- lp.type = windowType;
+ lp.type = mWindowType;
lp.format = PixelFormat.TRANSLUCENT;
lp.setTitle(VolumeDialog.class.getSimpleName());
lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
@@ -170,8 +193,7 @@
window.setAttributes(lp);
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
- mActiveSliderTint = loadColorStateList(R.color.system_accent_color);
- mInactiveSliderTint = loadColorStateList(R.color.volume_slider_inactive);
+
mDialog.setContentView(R.layout.volume_dialog);
mDialogView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog);
mDialogView.setOnHoverListener(new View.OnHoverListener() {
@@ -185,56 +207,53 @@
}
});
mDialogContentView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog_content);
+ mVolumeRowContainer =
+ (ViewGroup) mDialogContentView.findViewById(R.id.volume_row_container);
+ mExpanded = false;
mExpandButton = (ImageButton) mDialogView.findViewById(R.id.volume_expand_button);
mExpandButton.setOnClickListener(mClickExpand);
updateWindowWidthH();
updateExpandButtonH();
- mLayoutTransition = new LayoutTransition();
- mLayoutTransition.setDuration(new ValueAnimator().getDuration() / 2);
+
mDialogContentView.setLayoutTransition(mLayoutTransition);
mMotion = new VolumeDialogMotion(mDialog, mDialogView, mDialogContentView, mExpandButton,
new VolumeDialogMotion.Callback() {
- @Override
- public void onAnimatingChanged(boolean animating) {
- if (animating) return;
- if (mPendingStateChanged) {
- mHandler.sendEmptyMessage(H.STATE_CHANGED);
- mPendingStateChanged = false;
- }
- if (mPendingRecheckAll) {
- mHandler.sendEmptyMessage(H.RECHECK_ALL);
- mPendingRecheckAll = false;
- }
- }
- });
+ @Override
+ public void onAnimatingChanged(boolean animating) {
+ if (animating) return;
+ if (mPendingStateChanged) {
+ mHandler.sendEmptyMessage(H.STATE_CHANGED);
+ mPendingStateChanged = false;
+ }
+ if (mPendingRecheckAll) {
+ mHandler.sendEmptyMessage(H.RECHECK_ALL);
+ mPendingRecheckAll = false;
+ }
+ }
+ });
- addRow(AudioManager.STREAM_RING,
- R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true);
- addRow(AudioManager.STREAM_MUSIC,
- R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true);
- addRow(AudioManager.STREAM_ALARM,
- R.drawable.ic_volume_alarm, R.drawable.ic_volume_alarm_mute, false);
- addRow(AudioManager.STREAM_VOICE_CALL,
- R.drawable.ic_volume_voice, R.drawable.ic_volume_voice, false);
- addRow(AudioManager.STREAM_BLUETOOTH_SCO,
- R.drawable.ic_volume_bt_sco, R.drawable.ic_volume_bt_sco, false);
- addRow(AudioManager.STREAM_SYSTEM,
- R.drawable.ic_volume_system, R.drawable.ic_volume_system_mute, false);
-
- mSettingsButton = mDialog.findViewById(R.id.volume_settings_button);
- mSettingsButton.setOnClickListener(mClickSettings);
+ if (mRows.isEmpty()) {
+ addRow(AudioManager.STREAM_RING,
+ R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true);
+ addRow(AudioManager.STREAM_MUSIC,
+ R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true);
+ addRow(AudioManager.STREAM_ALARM,
+ R.drawable.ic_volume_alarm, R.drawable.ic_volume_alarm_mute, false);
+ addRow(AudioManager.STREAM_VOICE_CALL,
+ R.drawable.ic_volume_voice, R.drawable.ic_volume_voice, false);
+ addRow(AudioManager.STREAM_BLUETOOTH_SCO,
+ R.drawable.ic_volume_bt_sco, R.drawable.ic_volume_bt_sco, false);
+ addRow(AudioManager.STREAM_SYSTEM,
+ R.drawable.ic_volume_system, R.drawable.ic_volume_system_mute, false);
+ } else {
+ addExistingRows();
+ }
mExpandButtonAnimationDuration = res.getInteger(R.integer.volume_expand_animation_duration);
mZenFooter = (ZenFooter) mDialog.findViewById(R.id.volume_zen_footer);
- mZenFooter.init(zenModeController);
+ mZenFooter.init(mZenModeController);
mZenPanel = (TunerZenModePanel) mDialog.findViewById(R.id.tuner_zen_mode_panel);
- mZenPanel.init(zenModeController);
+ mZenPanel.init(mZenModeController);
mZenPanel.setCallback(mZenPanelCallback);
-
- mAccessibility.init();
-
- controller.addCallback(mControllerCallbackH, mHandler);
- controller.getState();
- TunerService.get(mContext).addTunable(this, SHOW_FULL_ZEN);
}
@Override
@@ -285,46 +304,38 @@
}
private void addRow(int stream, int iconRes, int iconMuteRes, boolean important) {
- final VolumeRow row = initRow(stream, iconRes, iconMuteRes, important);
+ VolumeRow row = new VolumeRow();
+ initRow(row, stream, iconRes, iconMuteRes, important);
if (!mRows.isEmpty()) {
- final View v = new View(mContext);
- v.setId(android.R.id.background);
- final int h = mContext.getResources()
- .getDimensionPixelSize(R.dimen.volume_slider_interspacing);
- final LinearLayout.LayoutParams lp =
- new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, h);
- mDialogContentView.addView(v, mDialogContentView.getChildCount() - 2, lp);
- row.space = v;
+ addSpacer(row);
}
- row.settingsButton.addOnLayoutChangeListener(new OnLayoutChangeListener() {
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom,
- int oldLeft, int oldTop, int oldRight, int oldBottom) {
- final boolean moved = mLastActiveStream != mActiveStream ||
- oldLeft != left || oldTop != top;
- if (D.BUG) Log.d(TAG, "onLayoutChange moved=" + moved
- + " old=" + new Rect(oldLeft, oldTop, oldRight, oldBottom).toShortString()
- + "," + mLastActiveStream
- + " new=" + new Rect(left,top,right,bottom).toShortString()
- + "," + mActiveStream);
- mLastActiveStream = mActiveStream;
- if (moved) {
- for (int i = 0; i < mDialogContentView.getChildCount(); i++) {
- final View c = mDialogContentView.getChildAt(i);
- if (!c.isShown()) continue;
- if (c == row.view) {
- repositionExpandAnim(row);
- }
- return;
- }
- }
- }
- });
- // add new row just before the footer
- mDialogContentView.addView(row.view, mDialogContentView.getChildCount() - 2);
+ mVolumeRowContainer.addView(row.view);
mRows.add(row);
}
+ private void addExistingRows() {
+ int N = mRows.size();
+ for (int i = 0; i < N; i++) {
+ final VolumeRow row = mRows.get(i);
+ initRow(row, row.stream, row.iconRes, row.iconMuteRes, row.important);
+ if (i > 0) {
+ addSpacer(row);
+ }
+ mVolumeRowContainer.addView(row.view);
+ }
+ }
+
+ private void addSpacer(VolumeRow row) {
+ final View v = new View(mContext);
+ v.setId(android.R.id.background);
+ final int h = mContext.getResources()
+ .getDimensionPixelSize(R.dimen.volume_slider_interspacing);
+ final LinearLayout.LayoutParams lp =
+ new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, h);
+ mVolumeRowContainer.addView(v, lp);
+ row.space = v;
+ }
+
private boolean isAttached() {
return mDialogContentView != null && mDialogContentView.isAttachedToWindow();
}
@@ -345,18 +356,6 @@
return null;
}
- private void repositionExpandAnim(VolumeRow row) {
- final int[] loc = new int[2];
- row.settingsButton.getLocationInWindow(loc);
- final MarginLayoutParams mlp = (MarginLayoutParams) mDialogView.getLayoutParams();
- final int x = loc[0] - mlp.leftMargin;
- final int y = loc[1] - mlp.topMargin;
- if (D.BUG) Log.d(TAG, "repositionExpandAnim x=" + x + " y=" + y);
- mExpandButton.setTranslationX(x);
- mExpandButton.setTranslationY(y);
- mExpandButton.setTag((Integer) y);
- }
-
public void dump(PrintWriter writer) {
writer.println(VolumeDialog.class.getSimpleName() + " state:");
writer.print(" mShowing: "); writer.println(mShowing);
@@ -374,8 +373,8 @@
}
@SuppressLint("InflateParams")
- private VolumeRow initRow(final int stream, int iconRes, int iconMuteRes, boolean important) {
- final VolumeRow row = new VolumeRow();
+ private void initRow(final VolumeRow row, final int stream, int iconRes, int iconMuteRes,
+ boolean important) {
row.stream = stream;
row.iconRes = iconRes;
row.iconMuteRes = iconMuteRes;
@@ -442,9 +441,6 @@
row.userAttempt = 0; // reset the grace period, slider should update immediately
}
});
- row.settingsButton = (ImageButton) row.view.findViewById(R.id.volume_settings_button);
- row.settingsButton.setOnClickListener(mClickSettings);
- return row;
}
public void destroy() {
@@ -573,8 +569,6 @@
if (mExpandButtonAnimationRunning && isAttached()) return;
final int res = mExpanded ? R.drawable.ic_volume_collapse_animation
: R.drawable.ic_volume_expand_animation;
- if (res == mExpandButtonRes) return;
- mExpandButtonRes = res;
if (hasTouchFeature()) {
mExpandButton.setImageResource(res);
} else {
@@ -606,16 +600,6 @@
final boolean visible = isVisibleH(row, isActive);
Util.setVisOrGone(row.view, visible);
Util.setVisOrGone(row.space, visible && mExpanded);
- final int expandButtonRes = mExpanded ? R.drawable.ic_volume_settings : 0;
- if (expandButtonRes != row.cachedExpandButtonRes) {
- row.cachedExpandButtonRes = expandButtonRes;
- if (expandButtonRes == 0) {
- row.settingsButton.setImageDrawable(null);
- } else {
- row.settingsButton.setImageResource(expandButtonRes);
- }
- }
- Util.setVisOrInvis(row.settingsButton, false);
updateVolumeRowHeaderVisibleH(row);
row.header.setAlpha(mExpanded && isActive ? 1 : 0.5f);
updateVolumeRowSliderTintH(row, isActive);
@@ -629,8 +613,8 @@
if (row.ss == null || !row.ss.dynamic) continue;
if (!mDynamic.get(row.stream)) {
mRows.remove(i);
- mDialogContentView.removeView(row.view);
- mDialogContentView.removeView(row.space);
+ mVolumeRowContainer.removeView(row.view);
+ mVolumeRowContainer.removeView(row.space);
}
}
}
@@ -911,6 +895,12 @@
@Override
public void onConfigurationChanged() {
+ Configuration newConfig = mContext.getResources().getConfiguration();
+ final int density = newConfig.densityDpi;
+ if (density != mDensity) {
+ mDialog.dismiss();
+ initDialog();
+ }
updateWindowWidthH();
mSpTexts.update();
mZenFooter.onConfigurationChanged();
@@ -963,21 +953,6 @@
}
};
- private final OnClickListener mClickSettings = new OnClickListener() {
- @Override
- public void onClick(View v) {
- mSettingsButton.postDelayed(new Runnable() {
- @Override
- public void run() {
- Events.writeEvent(mContext, Events.EVENT_SETTINGS_CLICK);
- if (mCallback != null) {
- mCallback.onSettingsClicked();
- }
- }
- }, WAIT_FOR_RIPPLE);
- }
- };
-
private final class H extends Handler {
private static final int SHOW = 1;
private static final int DISMISS = 2;
@@ -1155,7 +1130,6 @@
private TextView header;
private ImageButton icon;
private SeekBar slider;
- private ImageButton settingsButton;
private int stream;
private StreamState ss;
private long userAttempt; // last user-driven slider change
@@ -1168,12 +1142,10 @@
private ColorStateList cachedSliderTint;
private int iconState; // from Events
private boolean cachedShowHeaders = VolumePrefs.DEFAULT_SHOW_HEADERS;
- private int cachedExpandButtonRes;
private int lastAudibleLevel = 1;
}
public interface Callback {
- void onSettingsClicked();
void onZenSettingsClicked();
void onZenPrioritySettingsClicked();
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
index d7635ad..3d33809 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
@@ -168,11 +168,6 @@
private final VolumeDialog.Callback mVolumeDialogCallback = new VolumeDialog.Callback() {
@Override
- public void onSettingsClicked() {
- startSettings(new Intent(Settings.ACTION_NOTIFICATION_SETTINGS));
- }
-
- @Override
public void onZenSettingsClicked() {
startSettings(ZenModePanel.ZEN_SETTINGS);
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePrefs.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePrefs.java
index 04339eb..bbb70ed 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumePrefs.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePrefs.java
@@ -43,7 +43,7 @@
public static final String PREF_ADJUST_ALARMS = "pref_adjust_alarms";
public static final String PREF_ADJUST_NOTIFICATION = "pref_adjust_notification";
- public static final boolean DEFAULT_SHOW_HEADERS = true;
+ public static final boolean DEFAULT_SHOW_HEADERS = false;
public static final boolean DEFAULT_ENABLE_AUTOMUTE = true;
public static final boolean DEFAULT_ENABLE_SILENT_MODE = true;
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java
index a03e7f7..c06b63b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java
@@ -139,9 +139,8 @@
}
public void onConfigurationChanged() {
- mEndNowButton.setText(mContext.getString(R.string.volume_zen_end_now));
- mSpTexts.update();
Util.setText(mEndNowButton, mContext.getString(R.string.volume_zen_end_now));
+ mSpTexts.update();
}
}
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index 3f3f851..cd31b17 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -369,5 +369,44 @@
// Logged when the user saves a modification to notification importance. Negative numbers
// indicate the user lowered the importance; positive means they increased it.
ACTION_SAVE_IMPORTANCE = 291;
+
+ // Interactive bug report initiated from power menu.
+ ACTION_BUGREPORT_FROM_POWER_MENU_INTERACTIVE = 292;
+
+ // Full bug report initiated from power menu.
+ ACTION_BUGREPORT_FROM_POWER_MENU_FULL = 293;
+
+ // Interactive bug report initiated from Settings.
+ ACTION_BUGREPORT_FROM_SETTINGS_INTERACTIVE = 294;
+
+ // Full bug report initiated from Settings.
+ ACTION_BUGREPORT_FROM_SETTINGS_FULL = 295;
+
+ // Bug report canceled using system notification.
+ ACTION_BUGREPORT_NOTIFICATION_ACTION_CANCEL = 296;
+
+ // Bug report details screen open using system notification.
+ ACTION_BUGREPORT_NOTIFICATION_ACTION_DETAILS = 297;
+
+ // Additional Bug report screen shot taken using system notification.
+ ACTION_BUGREPORT_NOTIFICATION_ACTION_SCREENSHOT = 298;
+
+ // Bug report shared by user using system notification.
+ ACTION_BUGREPORT_NOTIFICATION_ACTION_SHARE = 299;
+
+ // User changed bug report name using the details screen.
+ ACTION_BUGREPORT_DETAILS_NAME_CHANGED = 300;
+
+ // User changed bug report title using the details screen.
+ ACTION_BUGREPORT_DETAILS_TITLE_CHANGED = 301;
+
+ // User changed bug report description using the details screen.
+ ACTION_BUGREPORT_DETAILS_DESCRIPTION_CHANGED = 302;
+
+ // Changes made on bug report details screen were saved by user.
+ ACTION_BUGREPORT_DETAILS_SAVED = 303;
+
+ // Changes made on bug report details screen were canceled by user.
+ ACTION_BUGREPORT_DETAILS_CANCELED = 304;
}
}
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index 2b52799..f537d18 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -353,7 +353,10 @@
}
private void onPackageBroadcastReceived(Intent intent, int userId) {
- if (!mUserManager.isUserUnlocked(userId)) return;
+ if (!mUserManager.isUserUnlocked(userId) ||
+ isProfileWithLockedParent(userId)) {
+ return;
+ }
final String action = intent.getAction();
boolean added = false;
@@ -435,7 +438,10 @@
* due to user not being available and package suspension.
*/
private void reloadWidgetsMaskedStateForUser(int userId) {
- if (!mUserManager.isUserUnlocked(userId)) return;
+ if (!mUserManager.isUserUnlocked(userId) ||
+ isProfileWithLockedParent(userId)) {
+ return;
+ }
synchronized (mLock) {
reloadWidgetPackageSuspensionMaskedStateLocked(userId);
List<UserInfo> profiles = mUserManager.getEnabledProfiles(userId);
@@ -606,7 +612,10 @@
throw new IllegalStateException(
"User " + userId + " must be unlocked for widgets to be available");
}
-
+ if (isProfileWithLockedParent(userId)) {
+ throw new IllegalStateException(
+ "Profile " + userId + " must have unlocked parent");
+ }
final int[] profileIds = mSecurityPolicy.getEnabledGroupProfileIds(userId);
// Careful lad, we may have already loaded the state for some
@@ -2458,6 +2467,9 @@
}
private void onUserUnlocked(int userId) {
+ if (isProfileWithLockedParent(userId)) {
+ return;
+ }
synchronized (mLock) {
ensureGroupStateLoadedLocked(userId);
reloadWidgetsMaskedStateForUser(userId);
@@ -3306,6 +3318,23 @@
}
}
+ private boolean isProfileWithLockedParent(int userId) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ UserInfo userInfo = mUserManager.getUserInfo(userId);
+ if (userInfo != null && userInfo.isManagedProfile()) {
+ UserInfo parentInfo = mUserManager.getProfileParent(userId);
+ if (parentInfo != null
+ && !mUserManager.isUserUnlocked(parentInfo.getUserHandle())) {
+ return true;
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ return false;
+ }
+
private boolean isProfileWithUnlockedParent(int userId) {
UserInfo userInfo = mUserManager.getUserInfo(userId);
if (userInfo != null && userInfo.isManagedProfile()) {
diff --git a/services/core/java/com/android/server/DeviceIdleController.java b/services/core/java/com/android/server/DeviceIdleController.java
index 6c19c38..62fa7d5 100644
--- a/services/core/java/com/android/server/DeviceIdleController.java
+++ b/services/core/java/com/android/server/DeviceIdleController.java
@@ -132,7 +132,8 @@
private Intent mLightIdleIntent;
private Display mCurDisplay;
private AnyMotionDetector mAnyMotionDetector;
- private boolean mEnabled;
+ private boolean mLightEnabled;
+ private boolean mDeepEnabled;
private boolean mForceIdle;
private boolean mScreenOn;
private boolean mCharging;
@@ -179,7 +180,7 @@
private static final int LIGHT_STATE_IDLE = 2;
/** Device is in the light idle state, but temporarily out of idle to do regular maintenance. */
private static final int LIGHT_STATE_IDLE_MAINTENANCE = 3;
- /** Device light idle state is overriden, now applying full doze state. */
+ /** Device light idle state is overriden, now applying deep doze state. */
private static final int LIGHT_STATE_OVERRIDE = 4;
private static String lightStateToString(int state) {
switch (state) {
@@ -288,8 +289,8 @@
private static final int EVENT_NORMAL = 1;
private static final int EVENT_LIGHT_IDLE = 2;
private static final int EVENT_LIGHT_MAINTENANCE = 3;
- private static final int EVENT_FULL_IDLE = 4;
- private static final int EVENT_FULL_MAINTENANCE = 5;
+ private static final int EVENT_DEEP_IDLE = 4;
+ private static final int EVENT_DEEP_MAINTENANCE = 5;
private int[] mEventCmds = new int[EVENT_BUFFER_SIZE];
private long[] mEventTimes = new long[EVENT_BUFFER_SIZE];
@@ -835,13 +836,13 @@
case MSG_REPORT_IDLE_ON:
case MSG_REPORT_IDLE_ON_LIGHT: {
EventLogTags.writeDeviceIdleOnStart();
- final boolean fullChanged;
+ final boolean deepChanged;
final boolean lightChanged;
if (msg.what == MSG_REPORT_IDLE_ON) {
- fullChanged = mLocalPowerManager.setDeviceIdleMode(true);
+ deepChanged = mLocalPowerManager.setDeviceIdleMode(true);
lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false);
} else {
- fullChanged = mLocalPowerManager.setDeviceIdleMode(false);
+ deepChanged = mLocalPowerManager.setDeviceIdleMode(false);
lightChanged = mLocalPowerManager.setLightDeviceIdleMode(true);
}
try {
@@ -851,7 +852,7 @@
: BatteryStats.DEVICE_IDLE_MODE_LIGHT, null, Process.myUid());
} catch (RemoteException e) {
}
- if (fullChanged) {
+ if (deepChanged) {
getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL);
}
if (lightChanged) {
@@ -861,7 +862,7 @@
} break;
case MSG_REPORT_IDLE_OFF: {
EventLogTags.writeDeviceIdleOffStart("unknown");
- final boolean fullChanged = mLocalPowerManager.setDeviceIdleMode(false);
+ final boolean deepChanged = mLocalPowerManager.setDeviceIdleMode(false);
final boolean lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false);
try {
mNetworkPolicyManager.setDeviceIdleMode(false);
@@ -869,7 +870,7 @@
null, Process.myUid());
} catch (RemoteException e) {
}
- if (fullChanged) {
+ if (deepChanged) {
incActiveIdleOps();
getContext().sendOrderedBroadcastAsUser(mIdleIntent, UserHandle.ALL,
null, mIdleStartedDoneReceiver, null, 0, null, null);
@@ -889,7 +890,7 @@
int activeUid = msg.arg1;
EventLogTags.writeDeviceIdleOffStart(
activeReason != null ? activeReason : "unknown");
- final boolean fullChanged = mLocalPowerManager.setDeviceIdleMode(false);
+ final boolean deepChanged = mLocalPowerManager.setDeviceIdleMode(false);
final boolean lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false);
try {
mNetworkPolicyManager.setDeviceIdleMode(false);
@@ -897,7 +898,7 @@
activeReason, activeUid);
} catch (RemoteException e) {
}
- if (fullChanged) {
+ if (deepChanged) {
getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL);
}
if (lightChanged) {
@@ -1102,7 +1103,7 @@
final PackageManager pm = getContext().getPackageManager();
synchronized (this) {
- mEnabled = getContext().getResources().getBoolean(
+ mLightEnabled = mDeepEnabled = getContext().getResources().getBoolean(
com.android.internal.R.bool.config_enableAutoPowerModes);
SystemConfig sysConfig = SystemConfig.getInstance();
ArraySet<String> allowPowerExceptIdle = sysConfig.getAllowInPowerSaveExceptIdle();
@@ -1550,17 +1551,17 @@
void becomeInactiveIfAppropriateLocked() {
if (DEBUG) Slog.d(TAG, "becomeInactiveIfAppropriateLocked()");
- if (((!mScreenOn && !mCharging) || mForceIdle) && mEnabled) {
+ if ((!mScreenOn && !mCharging) || mForceIdle) {
// Screen has turned off; we are now going to become inactive and start
// waiting to see if we will ultimately go idle.
- if (mState == STATE_ACTIVE) {
+ if (mState == STATE_ACTIVE && mDeepEnabled) {
mState = STATE_INACTIVE;
if (DEBUG) Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE");
resetIdleManagementLocked();
scheduleAlarmLocked(mInactiveTimeout, false);
EventLogTags.writeDeviceIdle(mState, "no activity");
}
- if (mLightState == LIGHT_STATE_ACTIVE) {
+ if (mLightState == LIGHT_STATE_ACTIVE && mLightEnabled) {
mLightState = LIGHT_STATE_INACTIVE;
if (DEBUG) Slog.d(TAG, "Moved from LIGHT_STATE_ACTIVE to LIGHT_STATE_INACTIVE");
resetLightIdleManagementLocked();
@@ -1594,7 +1595,7 @@
void stepLightIdleStateLocked(String reason) {
if (mLightState == LIGHT_STATE_OVERRIDE) {
- // If we are already in full device idle mode, then
+ // If we are already in deep device idle mode, then
// there is nothing left to do for light mode.
return;
}
@@ -1731,7 +1732,7 @@
cancelLightAlarmLocked();
}
EventLogTags.writeDeviceIdle(mState, reason);
- addEvent(EVENT_FULL_IDLE);
+ addEvent(EVENT_DEEP_IDLE);
mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON);
break;
case STATE_IDLE:
@@ -1744,7 +1745,7 @@
(long)(mNextIdlePendingDelay * mConstants.IDLE_PENDING_FACTOR));
mState = STATE_IDLE_MAINTENANCE;
EventLogTags.writeDeviceIdle(mState, reason);
- addEvent(EVENT_FULL_MAINTENANCE);
+ addEvent(EVENT_DEEP_MAINTENANCE);
mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);
break;
}
@@ -1877,7 +1878,7 @@
becomeInactive = true;
}
if (mLightState == LIGHT_STATE_OVERRIDE) {
- // We went out of light idle mode because we had started full idle mode... let's
+ // We went out of light idle mode because we had started deep idle mode... let's
// now go back and reset things so we resume light idling if appropriate.
mLightState = STATE_ACTIVE;
EventLogTags.writeDeviceIdleLight(mLightState, type);
@@ -2183,11 +2184,11 @@
pw.println(" force-idle");
pw.println(" Force directly into idle mode, regardless of other device state.");
pw.println(" Use \"step\" to get out.");
- pw.println(" disable");
+ pw.println(" disable [light|deep|all]");
pw.println(" Completely disable device idle mode.");
- pw.println(" enable");
+ pw.println(" enable [light|deep|all]");
pw.println(" Re-enable device idle mode after it had previously been disabled.");
- pw.println(" enabled");
+ pw.println(" enabled [light|deep|all]");
pw.println(" Print 1 if device idle mode is currently enabled, else 0.");
pw.println(" whitelist");
pw.println(" Print currently whitelisted apps.");
@@ -2247,7 +2248,7 @@
synchronized (this) {
long token = Binder.clearCallingIdentity();
try {
- if (!mEnabled) {
+ if (!mDeepEnabled) {
pw.println("Unable to go idle; not enabled");
return -1;
}
@@ -2274,11 +2275,32 @@
null);
synchronized (this) {
long token = Binder.clearCallingIdentity();
+ String arg = shell.getNextArg();
try {
- if (mEnabled) {
- mEnabled = false;
- becomeActiveLocked("disabled", Process.myUid());
- pw.println("Idle mode disabled");
+ boolean becomeActive = false;
+ boolean valid = false;
+ if (arg == null || "deep".equals(arg) || "all".equals(arg)) {
+ valid = true;
+ if (mDeepEnabled) {
+ mDeepEnabled = false;
+ becomeActive = true;
+ pw.println("Deep idle mode disabled");
+ }
+ }
+ if (arg == null || "light".equals(arg) || "all".equals(arg)) {
+ valid = true;
+ if (mLightEnabled) {
+ mLightEnabled = false;
+ becomeActive = true;
+ pw.println("Light idle mode disabled");
+ }
+ }
+ if (becomeActive) {
+ becomeActiveLocked((arg == null ? "all" : arg) + "-disabled",
+ Process.myUid());
+ }
+ if (!valid) {
+ pw.println("Unknown idle mode: " + arg);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -2289,12 +2311,31 @@
null);
synchronized (this) {
long token = Binder.clearCallingIdentity();
+ String arg = shell.getNextArg();
try {
- exitForceIdleLocked();
- if (!mEnabled) {
- mEnabled = true;
+ boolean becomeInactive = false;
+ boolean valid = false;
+ if (arg == null || "deep".equals(arg) || "all".equals(arg)) {
+ valid = true;
+ if (!mDeepEnabled) {
+ mDeepEnabled = true;
+ becomeInactive = true;
+ pw.println("Deep idle mode enabled");
+ }
+ }
+ if (arg == null || "light".equals(arg) || "all".equals(arg)) {
+ valid = true;
+ if (!mLightEnabled) {
+ mLightEnabled = true;
+ becomeInactive = true;
+ pw.println("Light idle mode enable");
+ }
+ }
+ if (becomeInactive) {
becomeInactiveIfAppropriateLocked();
- pw.println("Idle mode enabled");
+ }
+ if (!valid) {
+ pw.println("Unknown idle mode: " + arg);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -2302,7 +2343,16 @@
}
} else if ("enabled".equals(cmd)) {
synchronized (this) {
- pw.println(mEnabled ? "1" : " 0");
+ String arg = shell.getNextArg();
+ if (arg == null || "all".equals(arg)) {
+ pw.println(mDeepEnabled && mLightEnabled ? "1" : 0);
+ } else if ("deep".equals(arg)) {
+ pw.println(mDeepEnabled ? "1" : 0);
+ } else if ("light".equals(arg)) {
+ pw.println(mLightEnabled ? "1" : 0);
+ } else {
+ pw.println("Unknown idle mode: " + arg);
+ }
}
} else if ("whitelist".equals(cmd)) {
long token = Binder.clearCallingIdentity();
@@ -2441,8 +2491,8 @@
case EVENT_NORMAL: label = " normal"; break;
case EVENT_LIGHT_IDLE: label = " light-idle"; break;
case EVENT_LIGHT_MAINTENANCE: label = "light-maint"; break;
- case EVENT_FULL_IDLE: label = " full-idle"; break;
- case EVENT_FULL_MAINTENANCE: label = " full-maint"; break;
+ case EVENT_DEEP_IDLE: label = " deep-idle"; break;
+ case EVENT_DEEP_MAINTENANCE: label = " deep-maint"; break;
default: label = " ??"; break;
}
pw.print(" ");
@@ -2519,7 +2569,8 @@
}
}
- pw.print(" mEnabled="); pw.println(mEnabled);
+ pw.print(" mLightEnabled="); pw.print(mLightEnabled);
+ pw.print(" mDeepEnabled="); pw.println(mDeepEnabled);
pw.print(" mForceIdle="); pw.println(mForceIdle);
pw.print(" mMotionSensor="); pw.println(mMotionSensor);
pw.print(" mCurDisplay="); pw.println(mCurDisplay);
diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java
index d1de7e5..5ba8bd5 100644
--- a/services/core/java/com/android/server/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/InputMethodManagerService.java
@@ -1458,8 +1458,7 @@
return null;
}
- @Override
- public InputBindResult startInput(
+ private InputBindResult startInput(
/* @InputMethodClient.StartInputReason */ final int startInputReason,
IInputMethodClient client, IInputContext inputContext, EditorInfo attribute,
int controlFlags) {
@@ -2197,7 +2196,19 @@
}
@Override
- public InputBindResult windowGainedFocus(
+ public InputBindResult startInputOrWindowGainedFocus(
+ /* @InputMethodClient.StartInputReason */ final int startInputReason,
+ IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode,
+ int windowFlags, EditorInfo attribute, IInputContext inputContext) {
+ if (windowToken != null) {
+ return windowGainedFocus(startInputReason, client, windowToken, controlFlags,
+ softInputMode, windowFlags, attribute, inputContext);
+ } else {
+ return startInput(startInputReason, client, inputContext, attribute, controlFlags);
+ }
+ }
+
+ private InputBindResult windowGainedFocus(
/* @InputMethodClient.StartInputReason */ final int startInputReason,
IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode,
int windowFlags, EditorInfo attribute, IInputContext inputContext) {
diff --git a/services/core/java/com/android/server/RecoverySystemService.java b/services/core/java/com/android/server/RecoverySystemService.java
new file mode 100644
index 0000000..d237fe7
--- /dev/null
+++ b/services/core/java/com/android/server/RecoverySystemService.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.content.Context;
+import android.os.IRecoverySystem;
+import android.os.IRecoverySystemProgressListener;
+import android.os.RecoverySystem;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Slog;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * The recovery system service is responsible for coordinating recovery related
+ * functions on the device. It sets up (or clears) the bootloader control block
+ * (BCB), which will be read by the bootloader and the recovery image. It also
+ * triggers /system/bin/uncrypt via init to de-encrypt an OTA package on the
+ * /data partition so that it can be accessed under the recovery image.
+ */
+public final class RecoverySystemService extends SystemService {
+ private static final String TAG = "RecoverySystemService";
+ private static final boolean DEBUG = false;
+
+ // A pipe file to monitor the uncrypt progress.
+ private static final String UNCRYPT_STATUS_FILE = "/cache/recovery/uncrypt_status";
+ // Temporary command file to communicate between the system server and uncrypt.
+ private static final String COMMAND_FILE = "/cache/recovery/command";
+
+ private Context mContext;
+
+ public RecoverySystemService(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService(Context.RECOVERY_SERVICE, new BinderService());
+ }
+
+ private final class BinderService extends IRecoverySystem.Stub {
+ @Override // Binder call
+ public boolean uncrypt(String filename, IRecoverySystemProgressListener listener) {
+ if (DEBUG) Slog.d(TAG, "uncrypt: " + filename);
+
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
+
+ // Write the filename into UNCRYPT_PACKAGE_FILE to be read by
+ // uncrypt.
+ RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
+
+ try (FileWriter uncryptFile = new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE)) {
+ uncryptFile.write(filename + "\n");
+ } catch (IOException e) {
+ Slog.e(TAG, "IOException when writing \"" + RecoverySystem.UNCRYPT_PACKAGE_FILE +
+ "\": " + e.getMessage());
+ return false;
+ }
+
+ // Create the status pipe file to communicate with uncrypt.
+ new File(UNCRYPT_STATUS_FILE).delete();
+ try {
+ Os.mkfifo(UNCRYPT_STATUS_FILE, 0600);
+ } catch (ErrnoException e) {
+ Slog.e(TAG, "ErrnoException when creating named pipe \"" + UNCRYPT_STATUS_FILE +
+ "\": " + e.getMessage());
+ return false;
+ }
+
+ // Trigger uncrypt via init.
+ SystemProperties.set("ctl.start", "uncrypt");
+
+ // Read the status from the pipe.
+ try (BufferedReader reader = new BufferedReader(new FileReader(UNCRYPT_STATUS_FILE))) {
+ int lastStatus = Integer.MIN_VALUE;
+ while (true) {
+ String str = reader.readLine();
+ try {
+ int status = Integer.parseInt(str);
+
+ // Avoid flooding the log with the same message.
+ if (status == lastStatus && lastStatus != Integer.MIN_VALUE) {
+ continue;
+ }
+ lastStatus = status;
+
+ if (status >= 0 && status <= 100) {
+ // Update status
+ Slog.i(TAG, "uncrypt read status: " + status);
+ if (listener != null) {
+ try {
+ listener.onProgress(status);
+ } catch (RemoteException unused) {
+ Slog.w(TAG, "RemoteException when posting progress");
+ }
+ }
+ if (status == 100) {
+ Slog.i(TAG, "uncrypt successfully finished.");
+ break;
+ }
+ } else {
+ // Error in /system/bin/uncrypt.
+ Slog.e(TAG, "uncrypt failed with status: " + status);
+ return false;
+ }
+ } catch (NumberFormatException unused) {
+ Slog.e(TAG, "uncrypt invalid status received: " + str);
+ return false;
+ }
+ }
+ } catch (IOException unused) {
+ Slog.e(TAG, "IOException when reading \"" + UNCRYPT_STATUS_FILE + "\".");
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override // Binder call
+ public boolean clearBcb() {
+ if (DEBUG) Slog.d(TAG, "clearBcb");
+ return setupOrClearBcb(false, null);
+ }
+
+ @Override // Binder call
+ public boolean setupBcb(String command) {
+ if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]");
+ return setupOrClearBcb(true, command);
+ }
+
+ private boolean setupOrClearBcb(boolean isSetup, String command) {
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
+
+ if (isSetup) {
+ // Set up the command file to be read by uncrypt.
+ try (FileWriter commandFile = new FileWriter(COMMAND_FILE)) {
+ commandFile.write(command + "\n");
+ } catch (IOException e) {
+ Slog.e(TAG, "IOException when writing \"" + COMMAND_FILE +
+ "\": " + e.getMessage());
+ return false;
+ }
+ }
+
+ // Create the status pipe file to communicate with uncrypt.
+ new File(UNCRYPT_STATUS_FILE).delete();
+ try {
+ Os.mkfifo(UNCRYPT_STATUS_FILE, 0600);
+ } catch (ErrnoException e) {
+ Slog.e(TAG, "ErrnoException when creating named pipe \"" +
+ UNCRYPT_STATUS_FILE + "\": " + e.getMessage());
+ return false;
+ }
+
+ if (isSetup) {
+ SystemProperties.set("ctl.start", "setup-bcb");
+ } else {
+ SystemProperties.set("ctl.start", "clear-bcb");
+ }
+
+ // Read the status from the pipe.
+ try (BufferedReader reader = new BufferedReader(new FileReader(UNCRYPT_STATUS_FILE))) {
+ while (true) {
+ String str = reader.readLine();
+ try {
+ int status = Integer.parseInt(str);
+
+ if (status == 100) {
+ Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear") +
+ " bcb successfully finished.");
+ break;
+ } else {
+ // Error in /system/bin/uncrypt.
+ Slog.e(TAG, "uncrypt failed with status: " + status);
+ return false;
+ }
+ } catch (NumberFormatException unused) {
+ Slog.e(TAG, "uncrypt invalid status received: " + str);
+ return false;
+ }
+ }
+ } catch (IOException unused) {
+ Slog.e(TAG, "IOException when reading \"" + UNCRYPT_STATUS_FILE + "\".");
+ return false;
+ }
+
+ // Delete the command file as we don't need it anymore.
+ new File(COMMAND_FILE).delete();
+ return true;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 8c04fbc..29608dd 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1894,7 +1894,7 @@
case SYSTEM_USER_UNLOCK_MSG: {
final int userId = msg.arg1;
mSystemServiceManager.unlockUser(userId);
- mRecentTasks.cleanupLocked(userId);
+ mRecentTasks.loadUserRecentsLocked(userId);
installEncryptionUnawareProviders(userId);
break;
}
@@ -2541,7 +2541,7 @@
}
void onUserStoppedLocked(int userId) {
- mRecentTasks.unloadUserRecentsLocked(userId);
+ mRecentTasks.unloadUserDataFromMemoryLocked(userId);
}
public void initPowerManagement() {
@@ -8739,6 +8739,10 @@
android.Manifest.permission.GET_DETAILED_TASKS)
== PackageManager.PERMISSION_GRANTED;
+ if (!isUserRunning(userId, ActivityManager.FLAG_AND_UNLOCKED)) {
+ Slog.i(TAG, "user " + userId + " is still locked. Cannot load recents");
+ return Collections.emptyList();
+ }
mRecentTasks.loadUserRecentsLocked(userId);
final int recentsCount = mRecentTasks.size();
@@ -12560,7 +12564,7 @@
// Make sure we have the current profile info, since it is needed for security checks.
mUserController.onSystemReady();
- mRecentTasks.onSystemReady();
+ mRecentTasks.onSystemReadyLocked();
// Check to see if there are any update receivers to run.
if (!mDidUpdate) {
if (mWaitingUpdate) {
@@ -17212,6 +17216,15 @@
ProxyInfo proxy = intent.getParcelableExtra(Proxy.EXTRA_PROXY_INFO);
mHandler.sendMessage(mHandler.obtainMessage(UPDATE_HTTP_PROXY_MSG, proxy));
break;
+ case android.hardware.Camera.ACTION_NEW_PICTURE:
+ case android.hardware.Camera.ACTION_NEW_VIDEO:
+ // These broadcasts are no longer allowed by the system, since they can
+ // cause significant thrashing at a crictical point (using the camera).
+ // Apps should use JobScehduler to monitor for media provider changes.
+ Slog.w(TAG, action + " no longer allowed; dropping from "
+ + UserHandle.formatUid(callingUid));
+ // Lie; we don't want to crash the app.
+ return ActivityManager.BROADCAST_SUCCESS;
}
}
@@ -17807,36 +17820,42 @@
final long origId = Binder.clearCallingIdentity();
final ActivityStack stack = mStackSupervisor.getStack(fromStackId);
if (stack != null) {
- if (fromStackId == DOCKED_STACK_ID) {
+ mWindowManager.deferSurfaceLayout();
+ try {
+ if (fromStackId == DOCKED_STACK_ID) {
- // We are moving all tasks from the docked stack to the fullscreen stack, which
- // is dismissing the docked stack, so resize all other stacks to fullscreen here
- // already so we don't end up with resize trashing.
- for (int i = FIRST_STATIC_STACK_ID; i <= LAST_STATIC_STACK_ID; i++) {
- if (StackId.isResizeableByDockedStack(i)) {
- ActivityStack otherStack = mStackSupervisor.getStack(i);
- if (otherStack != null) {
- mStackSupervisor.resizeStackLocked(i,
- null, null, null, PRESERVE_WINDOWS,
- true /* allowResizeInDockedMode */);
+ // We are moving all tasks from the docked stack to the fullscreen stack,
+ // which is dismissing the docked stack, so resize all other stacks to
+ // fullscreen here already so we don't end up with resize trashing.
+ for (int i = FIRST_STATIC_STACK_ID; i <= LAST_STATIC_STACK_ID; i++) {
+ if (StackId.isResizeableByDockedStack(i)) {
+ ActivityStack otherStack = mStackSupervisor.getStack(i);
+ if (otherStack != null) {
+ mStackSupervisor.resizeStackLocked(i,
+ null, null, null, PRESERVE_WINDOWS,
+ true /* allowResizeInDockedMode */);
+ }
}
}
}
- }
- final ArrayList<TaskRecord> tasks = stack.getAllTasks();
- final int size = tasks.size();
- if (onTop) {
- for (int i = 0; i < size; i++) {
- mStackSupervisor.moveTaskToStackLocked(tasks.get(i).taskId,
- FULLSCREEN_WORKSPACE_STACK_ID, ON_TOP, !FORCE_FOCUS,
- "moveTasksToFullscreenStack", ANIMATE);
+ final ArrayList<TaskRecord> tasks = stack.getAllTasks();
+ final int size = tasks.size();
+ if (onTop) {
+ for (int i = 0; i < size; i++) {
+ mStackSupervisor.moveTaskToStackLocked(tasks.get(i).taskId,
+ FULLSCREEN_WORKSPACE_STACK_ID, onTop, !FORCE_FOCUS,
+ "moveTasksToFullscreenStack", ANIMATE);
+ }
+ } else {
+ for (int i = size - 1; i >= 0; i--) {
+ mStackSupervisor.positionTaskInStackLocked(tasks.get(i).taskId,
+ FULLSCREEN_WORKSPACE_STACK_ID, 0);
+ }
}
- } else {
- for (int i = size - 1; i >= 0; i--) {
- mStackSupervisor.positionTaskInStackLocked(tasks.get(i).taskId,
- FULLSCREEN_WORKSPACE_STACK_ID, 0);
- }
+ } finally {
+ mWindowManager.continueSurfaceLayout();
}
+
}
Binder.restoreCallingIdentity(origId);
}
@@ -18629,8 +18648,15 @@
}
}
if ((cr.flags&Context.BIND_NOT_FOREGROUND) == 0) {
- if (client.curSchedGroup == Process.THREAD_GROUP_DEFAULT) {
- schedGroup = Process.THREAD_GROUP_DEFAULT;
+ // This will treat important bound services identically to
+ // the top app, which may behave differently than generic
+ // foreground work.
+ if (client.curSchedGroup > schedGroup) {
+ if ((cr.flags&Context.BIND_IMPORTANT) != 0) {
+ schedGroup = client.curSchedGroup;
+ } else {
+ schedGroup = Process.THREAD_GROUP_DEFAULT;
+ }
}
if (clientProcState <= ActivityManager.PROCESS_STATE_TOP) {
if (clientProcState == ActivityManager.PROCESS_STATE_TOP) {
@@ -18694,11 +18720,15 @@
final ActivityRecord a = cr.activity;
if ((cr.flags&Context.BIND_ADJUST_WITH_ACTIVITY) != 0) {
if (a != null && adj > ProcessList.FOREGROUND_APP_ADJ &&
- (a.visible || a.state == ActivityState.RESUMED
- || a.state == ActivityState.PAUSING)) {
+ (a.visible || a.state == ActivityState.RESUMED ||
+ a.state == ActivityState.PAUSING)) {
adj = ProcessList.FOREGROUND_APP_ADJ;
if ((cr.flags&Context.BIND_NOT_FOREGROUND) == 0) {
- schedGroup = Process.THREAD_GROUP_DEFAULT;
+ if ((cr.flags&Context.BIND_IMPORTANT) != 0) {
+ schedGroup = Process.THREAD_GROUP_TOP_APP;
+ } else {
+ schedGroup = Process.THREAD_GROUP_DEFAULT;
+ }
}
app.cached = false;
app.adjType = "service";
@@ -18778,7 +18808,7 @@
if (procState > clientProcState) {
procState = clientProcState;
}
- if (client.curSchedGroup == Process.THREAD_GROUP_DEFAULT) {
+ if (client.curSchedGroup > schedGroup) {
schedGroup = Process.THREAD_GROUP_DEFAULT;
}
}
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 9562f94..2394842 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -719,14 +719,14 @@
}
int getNextTaskIdForUserLocked(int userId) {
- mRecentTasks.loadUserRecentsLocked(userId);
final int currentTaskId = mCurTaskIdForUser.get(userId, userId * MAX_TASK_IDS_PER_USER);
// for a userId u, a taskId can only be in the range
// [u*MAX_TASK_IDS_PER_USER, (u+1)*MAX_TASK_IDS_PER_USER-1], so if MAX_TASK_IDS_PER_USER
// was 10, user 0 could only have taskIds 0 to 9, user 1: 10 to 19, user 2: 20 to 29, so on.
int candidateTaskId = currentTaskId;
- while (anyTaskForIdLocked(candidateTaskId, !RESTORE_FROM_RECENTS,
- INVALID_STACK_ID) != null) {
+ while (mRecentTasks.taskIdTakenForUserLocked(candidateTaskId, userId)
+ || anyTaskForIdLocked(candidateTaskId, !RESTORE_FROM_RECENTS,
+ INVALID_STACK_ID) != null) {
candidateTaskId++;
if (candidateTaskId == (userId + 1) * MAX_TASK_IDS_PER_USER) {
// Wrap around as there will be smaller task ids that are available now.
@@ -2362,6 +2362,8 @@
stack.positionTask(task, position);
// The task might have already been running and its visibility needs to be synchronized with
// the visibility of the stack / windows.
+ stack.ensureActivityConfigurationLocked(task.topRunningActivityLocked(), 0,
+ !PRESERVE_WINDOWS);
stack.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
resumeFocusedStackTopActivityLocked();
}
diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java
index 1166ae1..6614e63 100644
--- a/services/core/java/com/android/server/am/ActivityStarter.java
+++ b/services/core/java/com/android/server/am/ActivityStarter.java
@@ -1037,6 +1037,9 @@
// make sure it becomes visible as it starts (this will also trigger entry
// animation). An example of this are PIP activities.
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.
+ mWindowManager.executeAppTransition();
}
} else {
mTargetStack.addRecentActivityLocked(mStartActivity);
diff --git a/services/core/java/com/android/server/am/RecentTasks.java b/services/core/java/com/android/server/am/RecentTasks.java
index 9c139d5..7209814 100644
--- a/services/core/java/com/android/server/am/RecentTasks.java
+++ b/services/core/java/com/android/server/am/RecentTasks.java
@@ -37,8 +37,12 @@
import android.graphics.Bitmap;
import android.os.Environment;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.UserHandle;
+import android.provider.Settings.System;
+import android.util.ArraySet;
import android.util.Slog;
+import android.util.SparseArray;
import android.util.SparseBooleanArray;
import java.io.File;
@@ -59,12 +63,22 @@
// Maximum number recent bitmaps to keep in memory.
private static final int MAX_RECENT_BITMAPS = 3;
+ private static final int DEFAULT_INITIAL_CAPACITY = 5;
/**
* Save recent tasks information across reboots.
*/
private final TaskPersister mTaskPersister;
- private final SparseBooleanArray mUsersWithRecentsLoaded = new SparseBooleanArray(5);
+ private final ActivityManagerService mService;
+ private final SparseBooleanArray mUsersWithRecentsLoaded = new SparseBooleanArray(
+ DEFAULT_INITIAL_CAPACITY);
+
+ /**
+ * Stores for each user task ids that are taken by tasks residing in persistent storage. These
+ * tasks may or may not currently be in memory.
+ */
+ final SparseArray<SparseBooleanArray> mPersistedTaskIds = new SparseArray<>(
+ DEFAULT_INITIAL_CAPACITY);
// Mainly to avoid object recreation on multiple calls.
private final ArrayList<TaskRecord> mTmpRecents = new ArrayList<TaskRecord>();
@@ -75,6 +89,7 @@
RecentTasks(ActivityManagerService service, ActivityStackSupervisor mStackSupervisor) {
File systemDir = Environment.getDataSystemDirectory();
+ mService = service;
mTaskPersister = new TaskPersister(systemDir, mStackSupervisor, service, this);
mStackSupervisor.setRecentTasks(this);
}
@@ -87,6 +102,8 @@
*/
void loadUserRecentsLocked(int userId) {
if (!mUsersWithRecentsLoaded.get(userId)) {
+ // Load the task ids if not loaded.
+ loadPersistedTaskIdsForUserLocked(userId);
Slog.i(TAG, "Loading recents for user " + userId + " into memory.");
addAll(mTaskPersister.restoreTasksForUserLocked(userId));
cleanupLocked(userId);
@@ -94,21 +111,49 @@
}
}
+ private void loadPersistedTaskIdsForUserLocked(int userId) {
+ // An empty instead of a null set here means that no persistent taskIds were present
+ // on file when we loaded them.
+ if (mPersistedTaskIds.get(userId) == null) {
+ mPersistedTaskIds.put(userId, mTaskPersister.loadPersistedTaskIdsForUser(userId));
+ }
+ }
+
+ boolean taskIdTakenForUserLocked(int taskId, int userId) {
+ loadPersistedTaskIdsForUserLocked(userId);
+ return mPersistedTaskIds.get(userId).get(taskId);
+ }
+
void notifyTaskPersisterLocked(TaskRecord task, boolean flush) {
if (task != null && task.stack != null && task.stack.isHomeStack()) {
// Never persist the home stack.
return;
}
+ syncPersistentTaskIdsLocked();
mTaskPersister.wakeup(task, flush);
}
- void onSystemReady() {
- clear();
- loadUserRecentsLocked(UserHandle.USER_SYSTEM);
- startPersisting();
+ private void syncPersistentTaskIdsLocked() {
+ for (int i = mPersistedTaskIds.size() - 1; i >= 0; i--) {
+ int userId = mPersistedTaskIds.keyAt(i);
+ if (mUsersWithRecentsLoaded.get(userId)) {
+ // Recents are loaded only after task ids are loaded. Therefore, the set of taskids
+ // referenced here should not be null.
+ mPersistedTaskIds.valueAt(i).clear();
+ }
+ }
+ for (int i = size() - 1; i >= 0; i--) {
+ TaskRecord task = get(i);
+ if (task.isPersistable && (task.stack == null || !task.stack.isHomeStack())) {
+ // Set of persisted taskIds for task.userId should not be null here
+ mPersistedTaskIds.get(task.userId).put(task.taskId, true);
+ }
+ }
}
- void startPersisting() {
+
+ void onSystemReadyLocked() {
+ clear();
mTaskPersister.startPersisting();
}
@@ -125,11 +170,14 @@
}
void flush() {
+ synchronized (mService) {
+ syncPersistentTaskIdsLocked();
+ }
mTaskPersister.flush();
}
/**
- * Returns all userIds for which recents from storage are loaded
+ * Returns all userIds for which recents from persistent storage are loaded into this list.
*
* @return an array of userIds.
*/
@@ -149,12 +197,7 @@
return usersWithRecentsLoaded;
}
- /**
- * Removes recent tasks for this user if they are loaded, does not do anything otherwise.
- *
- * @param userId the user id.
- */
- void unloadUserRecentsLocked(int userId) {
+ private void unloadUserRecentsLocked(int userId) {
if (mUsersWithRecentsLoaded.get(userId)) {
Slog.i(TAG, "Unloading recents for user " + userId + " from memory.");
mUsersWithRecentsLoaded.delete(userId);
@@ -162,6 +205,18 @@
}
}
+ /**
+ * Removes recent tasks and any other state kept in memory for the passed in user. Does not
+ * touch the information present on persistent storage.
+ *
+ * @param userId the id of the user
+ */
+ void unloadUserDataFromMemoryLocked(int userId) {
+ unloadUserRecentsLocked(userId);
+ mPersistedTaskIds.delete(userId);
+ mTaskPersister.unloadUserDataFromMemory(userId);
+ }
+
TaskRecord taskForIdLocked(int id) {
final int recentsCount = size();
for (int i = 0; i < recentsCount; i++) {
diff --git a/services/core/java/com/android/server/am/TaskPersister.java b/services/core/java/com/android/server/am/TaskPersister.java
index 283939e..11fd3bc 100644
--- a/services/core/java/com/android/server/am/TaskPersister.java
+++ b/services/core/java/com/android/server/am/TaskPersister.java
@@ -16,6 +16,9 @@
package com.android.server.am;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Debug;
@@ -26,11 +29,13 @@
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.util.Xml;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.XmlUtils;
-
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParser;
@@ -38,9 +43,12 @@
import org.xmlpull.v1.XmlSerializer;
import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
+import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
@@ -71,6 +79,7 @@
private static final String TASKS_DIRNAME = "recent_tasks";
private static final String TASK_EXTENSION = ".xml";
private static final String IMAGES_DIRNAME = "recent_images";
+ private static final String PERSISTED_TASK_IDS_FILENAME = "persisted_taskIds.txt";
static final String IMAGE_EXTENSION = ".png";
private static final String TAG_TASK = "task";
@@ -78,6 +87,7 @@
private final ActivityManagerService mService;
private final ActivityStackSupervisor mStackSupervisor;
private final RecentTasks mRecentTasks;
+ private final SparseArray<SparseBooleanArray> mTaskIdsInFile = new SparseArray<>();
/**
* Value determines write delay mode as follows: < 0 We are Flushing. No delays between writes
@@ -170,6 +180,64 @@
}
}
+ @NonNull
+ SparseBooleanArray loadPersistedTaskIdsForUser(int userId) {
+ if (mTaskIdsInFile.get(userId) != null) {
+ return mTaskIdsInFile.get(userId).clone();
+ }
+ final SparseBooleanArray persistedTaskIds = new SparseBooleanArray();
+ BufferedReader reader = null;
+ String line;
+ try {
+ reader = new BufferedReader(new FileReader(getUserPersistedTaskIdsFile(userId)));
+ while ((line = reader.readLine()) != null) {
+ for (String taskIdString : line.split("\\s+")) {
+ int id = Integer.parseInt(taskIdString);
+ persistedTaskIds.put(id, true);
+ }
+ }
+ } catch (FileNotFoundException e) {
+ // File doesn't exist. Ignore.
+ } catch (Exception e) {
+ Slog.e(TAG, "Error while reading taskIds file for user " + userId, e);
+ } finally {
+ IoUtils.closeQuietly(reader);
+ }
+ mTaskIdsInFile.put(userId, persistedTaskIds);
+ return persistedTaskIds.clone();
+ }
+
+ private void maybeWritePersistedTaskIdsForUser(@NonNull SparseBooleanArray taskIds,
+ int userId) {
+ if (userId < 0) {
+ return;
+ }
+ SparseBooleanArray persistedIdsInFile = mTaskIdsInFile.get(userId);
+ if (persistedIdsInFile != null && persistedIdsInFile.equals(taskIds)) {
+ return;
+ }
+ final File persistedTaskIdsFile = getUserPersistedTaskIdsFile(userId);
+ BufferedWriter writer = null;
+ try {
+ writer = new BufferedWriter(new FileWriter(persistedTaskIdsFile));
+ for (int i = 0; i < taskIds.size(); i++) {
+ if (taskIds.valueAt(i)) {
+ writer.write(String.valueOf(taskIds.keyAt(i)));
+ writer.newLine();
+ }
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, "Error while writing taskIds file for user " + userId, e);
+ } finally {
+ IoUtils.closeQuietly(writer);
+ }
+ mTaskIdsInFile.put(userId, taskIds.clone());
+ }
+
+ void unloadUserDataFromMemory(int userId) {
+ mTaskIdsInFile.delete(userId);
+ }
+
void wakeup(TaskRecord task, boolean flush) {
synchronized (this) {
if (task != null) {
@@ -336,14 +404,16 @@
File[] recentFiles = userTasksDir.listFiles();
if (recentFiles == null) {
- Slog.e(TAG, "restoreTasksForUser: Unable to list files from " + userTasksDir);
+ Slog.e(TAG, "restoreTasksForUserLocked: Unable to list files from " + userTasksDir);
return tasks;
}
for (int taskNdx = 0; taskNdx < recentFiles.length; ++taskNdx) {
File taskFile = recentFiles[taskNdx];
- if (DEBUG) Slog.d(TAG, "restoreTasksForUser: userId=" + userId
- + ", taskFile=" + taskFile.getName());
+ if (DEBUG) {
+ Slog.d(TAG, "restoreTasksForUserLocked: userId=" + userId
+ + ", taskFile=" + taskFile.getName());
+ }
BufferedReader reader = null;
boolean deleteFile = false;
try {
@@ -366,20 +436,29 @@
// out the stuff we just read, if we don't write it we will
// read the same thing again.
// mWriteQueue.add(new TaskWriteQueueItem(task));
+
final int taskId = task.taskId;
- mStackSupervisor.setNextTaskIdForUserLocked(taskId, userId);
- // Check if it's a valid user id. Don't add tasks for removed users.
- if (userId == task.userId) {
+ if (mStackSupervisor.anyTaskForIdLocked(taskId,
+ /* restoreFromRecents= */ false, 0) != null) {
+ // Should not happen.
+ Slog.wtf(TAG, "Existing task with taskId " + taskId + "found");
+ } else if (userId != task.userId) {
+ // Should not happen.
+ Slog.wtf(TAG, "Task with userId " + task.userId + " found in "
+ + userTasksDir.getAbsolutePath());
+ } else {
+ // Looks fine.
+ mStackSupervisor.setNextTaskIdForUserLocked(taskId, userId);
task.isPersistable = true;
tasks.add(task);
recoveredTaskIds.add(taskId);
}
} else {
- Slog.e(TAG, "restoreTasksForUser: Unable to restore taskFile="
+ Slog.e(TAG, "restoreTasksForUserLocked: Unable to restore taskFile="
+ taskFile + ": " + fileToString(taskFile));
}
} else {
- Slog.wtf(TAG, "restoreTasksForUser: Unknown xml event=" + event
+ Slog.wtf(TAG, "restoreTasksForUserLocked: Unknown xml event=" + event
+ " name=" + name);
}
}
@@ -454,6 +533,20 @@
}
}
+ private void writeTaskIdsFiles() {
+ int candidateUserIds[];
+ synchronized (mService) {
+ candidateUserIds = mRecentTasks.usersWithRecentsLoadedLocked();
+ }
+ SparseBooleanArray taskIdsToSave;
+ for (int userId : candidateUserIds) {
+ synchronized (mService) {
+ taskIdsToSave = mRecentTasks.mPersistedTaskIds.get(userId).clone();
+ }
+ maybeWritePersistedTaskIdsForUser(taskIdsToSave, userId);
+ }
+ }
+
private void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds) {
int[] candidateUserIds;
synchronized (mService) {
@@ -472,8 +565,12 @@
return BitmapFactory.decodeFile(filename);
}
+ static File getUserPersistedTaskIdsFile(int userId) {
+ return new File(Environment.getDataSystemDeDirectory(userId), PERSISTED_TASK_IDS_FILENAME);
+ }
+
static File getUserTasksDir(int userId) {
- File userTasksDir = new File(Environment.getUserSystemDirectory(userId), TASKS_DIRNAME);
+ File userTasksDir = new File(Environment.getDataSystemCeDirectory(userId), TASKS_DIRNAME);
if (!userTasksDir.exists()) {
if (!userTasksDir.mkdir()) {
@@ -485,7 +582,7 @@
}
static File getUserImagesDir(int userId) {
- File userImagesDir = new File(Environment.getUserSystemDirectory(userId), IMAGES_DIRNAME);
+ File userImagesDir = new File(Environment.getDataSystemCeDirectory(userId), IMAGES_DIRNAME);
if (!userImagesDir.exists()) {
if (!userImagesDir.mkdir()) {
@@ -535,6 +632,7 @@
}
removeObsoleteFiles(persistentTaskIds);
}
+ writeTaskIdsFiles();
// If mNextWriteTime, then don't delay between each call to saveToXml().
final WriteQueueItem item;
diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java
index 0d97434..f7db1f7 100644
--- a/services/core/java/com/android/server/content/SyncManager.java
+++ b/services/core/java/com/android/server/content/SyncManager.java
@@ -453,48 +453,10 @@
}
}
}
- ensureDefaultPeriodicSyncsH();
}
});
}
- private void ensureDefaultPeriodicSyncsH() {
-
- long defaultPeriod = SyncStorageEngine.DEFAULT_POLL_FREQUENCY_SECONDS;
- long defaultFlex = SyncStorageEngine.calculateDefaultFlexTime(defaultPeriod);
-
- List<AuthorityInfo> authorities = mSyncStorageEngine.getAllAuthorities();
- List<SyncOperation> syncs = getAllPendingSyncsFromCache();
- for (AuthorityInfo authority: authorities) {
- boolean foundPeriodicSync = false;
- for (SyncOperation op: syncs) {
- if (op.isPeriodic && authority.target.matchesSpec(op.target)) {
- foundPeriodicSync = true;
- break;
- }
- }
- if (!foundPeriodicSync) {
- EndPoint target = authority.target;
- final RegisteredServicesCache.ServiceInfo<SyncAdapterType>
- syncAdapterInfo = mSyncAdapters.getServiceInfo(
- SyncAdapterType.newKey(
- target.provider, target.account.type),
- target.userId);
- if (syncAdapterInfo == null) {
- continue;
- }
- scheduleSyncOperationH(
- new SyncOperation(target, syncAdapterInfo.uid,
- syncAdapterInfo.componentName.getPackageName(),
- SyncOperation.REASON_PERIODIC, SyncStorageEngine.SOURCE_PERIODIC,
- new Bundle(), syncAdapterInfo.type.allowParallelSyncs(),
- true /* periodic */, SyncOperation.NO_JOB_ID, defaultPeriod * 1000L,
- defaultFlex * 1000L)
- );
- }
- }
- }
-
private synchronized void verifyJobScheduler() {
if (mJobScheduler != null) {
return;
@@ -2271,6 +2233,7 @@
void checkIfDeviceReady() {
if (mProvisioned && mBootCompleted) {
synchronized(this) {
+ mSyncStorageEngine.restoreAllPeriodicSyncs();
// Dispatch any stashed messages.
obtainMessage(MESSAGE_RELEASE_MESSAGES_FROM_QUEUE).sendToTarget();
}
@@ -2622,7 +2585,6 @@
}
if (mBootCompleted) {
doDatabaseCleanup();
- mSyncStorageEngine.restoreAllPeriodicSyncs();
}
AccountAndUser[] accounts = mRunningAccounts;
diff --git a/services/core/java/com/android/server/content/SyncStorageEngine.java b/services/core/java/com/android/server/content/SyncStorageEngine.java
index 05aabf1..bc3fc6a 100644
--- a/services/core/java/com/android/server/content/SyncStorageEngine.java
+++ b/services/core/java/com/android/server/content/SyncStorageEngine.java
@@ -80,7 +80,7 @@
private static final String XML_TAG_LISTEN_FOR_TICKLES = "listenForTickles";
/** Default time for a periodic sync. */
- static final long DEFAULT_POLL_FREQUENCY_SECONDS = 60 * 60 * 24; // One day
+ private static final long DEFAULT_POLL_FREQUENCY_SECONDS = 60 * 60 * 24; // One day
/** Percentage of period that is flex by default, if no flexMillis is set. */
private static final double DEFAULT_FLEX_PERCENT_SYNC = 0.04;
@@ -857,16 +857,6 @@
}
}
- List<AuthorityInfo> getAllAuthorities() {
- List<AuthorityInfo> authorities = new ArrayList<AuthorityInfo>();
- synchronized (mAuthorities) {
- for (int i = 0; i < mAuthorities.size(); i++) {
- authorities.add(mAuthorities.valueAt(i));
- }
- }
- return authorities;
- }
-
/**
* Returns true if there is currently a sync operation being actively processed for the given
* target.
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index 811c947..42ecc06 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -49,14 +49,13 @@
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
-import android.os.Process;
-import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.app.IBatteryStats;
import com.android.server.DeviceIdleController;
import com.android.server.LocalServices;
+import com.android.server.job.JobStore.JobStatusFunctor;
import com.android.server.job.controllers.AppIdleController;
import com.android.server.job.controllers.BatteryController;
import com.android.server.job.controllers.ConnectivityController;
@@ -80,11 +79,15 @@
*/
public final class JobSchedulerService extends com.android.server.SystemService
implements StateChangedListener, JobCompletedListener {
+ static final String TAG = "JobSchedulerService";
public static final boolean DEBUG = false;
+
/** The number of concurrent jobs we run at one time. */
private static final int MAX_JOB_CONTEXTS_COUNT
= ActivityManager.isLowRamDeviceStatic() ? 3 : 6;
- static final String TAG = "JobSchedulerService";
+ /** The maximum number of jobs that we allow an unprivileged app to schedule */
+ private static final int MAX_JOBS_PER_APP = 100;
+
/** Global local for all job scheduler state. */
final Object mLock = new Object();
/** Master list of jobs. */
@@ -250,6 +253,15 @@
if (DEBUG) Slog.d(TAG, "SCHEDULE: " + jobStatus.toShortString());
JobStatus toCancel;
synchronized (mLock) {
+ // Jobs on behalf of others don't apply to the per-app job cap
+ if (packageName == null) {
+ if (mJobs.countJobsForUid(uId) > MAX_JOBS_PER_APP) {
+ Slog.w(TAG, "Too many jobs for uid " + uId);
+ throw new IllegalStateException("Apps may not schedule more than "
+ + MAX_JOBS_PER_APP + " distinct jobs");
+ }
+ }
+
toCancel = mJobs.getJobByUidAndJobId(uId, job.getId());
}
startTrackingJob(jobStatus, toCancel);
@@ -261,17 +273,15 @@
}
public List<JobInfo> getPendingJobs(int uid) {
- ArrayList<JobInfo> outList = new ArrayList<JobInfo>();
synchronized (mLock) {
- ArraySet<JobStatus> jobs = mJobs.getJobs();
- for (int i=0; i<jobs.size(); i++) {
- JobStatus job = jobs.valueAt(i);
- if (job.getUid() == uid) {
- outList.add(job.getJob());
- }
+ List<JobStatus> jobs = mJobs.getJobsByUid(uid);
+ ArrayList<JobInfo> outList = new ArrayList<JobInfo>(jobs.size());
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ JobStatus job = jobs.get(i);
+ outList.add(job.getJob());
}
+ return outList;
}
- return outList;
}
void cancelJobsForUser(int userHandle) {
@@ -471,14 +481,16 @@
getContext().getMainLooper()));
}
// Attach jobs to their controllers.
- ArraySet<JobStatus> jobs = mJobs.getJobs();
- for (int i=0; i<jobs.size(); i++) {
- JobStatus job = jobs.valueAt(i);
- for (int controller=0; controller<mControllers.size(); controller++) {
- mControllers.get(controller).deviceIdleModeChanged(mDeviceIdleMode);
- mControllers.get(controller).maybeStartTrackingJobLocked(job, null);
+ mJobs.forEachJob(new JobStatusFunctor() {
+ @Override
+ public void process(JobStatus job) {
+ for (int controller = 0; controller < mControllers.size(); controller++) {
+ final StateController sc = mControllers.get(controller);
+ sc.deviceIdleModeChanged(mDeviceIdleMode);
+ sc.maybeStartTrackingJobLocked(job, null);
+ }
}
- }
+ });
// GO GO GO!
mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
}
@@ -741,23 +753,13 @@
* as many as we can.
*/
private void queueReadyJobsForExecutionLockedH() {
- ArraySet<JobStatus> jobs = mJobs.getJobs();
- mPendingJobs.clear();
if (DEBUG) {
Slog.d(TAG, "queuing all ready jobs for execution:");
}
- for (int i=0; i<jobs.size(); i++) {
- JobStatus job = jobs.valueAt(i);
- if (isReadyToBeExecutedLocked(job)) {
- if (DEBUG) {
- Slog.d(TAG, " queued " + job.toShortString());
- }
- mPendingJobs.add(job);
- } else if (areJobConstraintsNotSatisfiedLocked(job)) {
- stopJobOnServiceContextLocked(job,
- JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED);
- }
- }
+ mPendingJobs.clear();
+ mJobs.forEachJob(mReadyQueueFunctor);
+ mReadyQueueFunctor.postProcess();
+
if (DEBUG) {
final int queuedJobs = mPendingJobs.size();
if (queuedJobs == 0) {
@@ -768,6 +770,34 @@
}
}
+ class ReadyJobQueueFunctor implements JobStatusFunctor {
+ ArrayList<JobStatus> newReadyJobs;
+
+ @Override
+ public void process(JobStatus job) {
+ if (isReadyToBeExecutedLocked(job)) {
+ if (DEBUG) {
+ Slog.d(TAG, " queued " + job.toShortString());
+ }
+ if (newReadyJobs == null) {
+ newReadyJobs = new ArrayList<JobStatus>();
+ }
+ newReadyJobs.add(job);
+ } else if (areJobConstraintsNotSatisfiedLocked(job)) {
+ stopJobOnServiceContextLocked(job,
+ JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED);
+ }
+ }
+
+ public void postProcess() {
+ if (newReadyJobs != null) {
+ mPendingJobs.addAll(newReadyJobs);
+ }
+ newReadyJobs = null;
+ }
+ }
+ private final ReadyJobQueueFunctor mReadyQueueFunctor = new ReadyJobQueueFunctor();
+
/**
* The state of at least one job has changed. Here is where we could enforce various
* policies on when we want to execute jobs.
@@ -777,18 +807,21 @@
* If more than 4 jobs total are ready we send them all off.
* TODO: It would be nice to consolidate these sort of high-level policies somewhere.
*/
- private void maybeQueueReadyJobsForExecutionLockedH() {
- mPendingJobs.clear();
- int chargingCount = 0;
- int idleCount = 0;
- int backoffCount = 0;
- int connectivityCount = 0;
- int contentCount = 0;
- List<JobStatus> runnableJobs = null;
- ArraySet<JobStatus> jobs = mJobs.getJobs();
- if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs...");
- for (int i=0; i<jobs.size(); i++) {
- JobStatus job = jobs.valueAt(i);
+ class MaybeReadyJobQueueFunctor implements JobStatusFunctor {
+ int chargingCount;
+ int idleCount;
+ int backoffCount;
+ int connectivityCount;
+ int contentCount;
+ List<JobStatus> runnableJobs;
+
+ public MaybeReadyJobQueueFunctor() {
+ reset();
+ }
+
+ // Functor method invoked for each job via JobStore.forEachJob()
+ @Override
+ public void process(JobStatus job) {
if (isReadyToBeExecutedLocked(job)) {
try {
if (ActivityManagerNative.getDefault().getAppStartMode(job.getUid(),
@@ -797,7 +830,7 @@
Slog.w(TAG, "Aborting job " + job.getUid() + ":"
+ job.getJob().toString() + " -- package not allowed to start");
mHandler.obtainMessage(MSG_STOP_JOB, job).sendToTarget();
- continue;
+ return;
}
} catch (RemoteException e) {
}
@@ -825,23 +858,45 @@
JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED);
}
}
- if (backoffCount > 0 ||
- idleCount >= MIN_IDLE_COUNT ||
- connectivityCount >= MIN_CONNECTIVITY_COUNT ||
- chargingCount >= MIN_CHARGING_COUNT ||
- contentCount >= MIN_CONTENT_COUNT ||
- (runnableJobs != null && runnableJobs.size() >= MIN_READY_JOBS_COUNT)) {
- if (DEBUG) {
- Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Running jobs.");
+
+ public void postProcess() {
+ if (backoffCount > 0 ||
+ idleCount >= MIN_IDLE_COUNT ||
+ connectivityCount >= MIN_CONNECTIVITY_COUNT ||
+ chargingCount >= MIN_CHARGING_COUNT ||
+ contentCount >= MIN_CONTENT_COUNT ||
+ (runnableJobs != null && runnableJobs.size() >= MIN_READY_JOBS_COUNT)) {
+ if (DEBUG) {
+ Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Running jobs.");
+ }
+ mPendingJobs.addAll(runnableJobs);
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Not running anything.");
+ }
}
- for (int i=0; i<runnableJobs.size(); i++) {
- mPendingJobs.add(runnableJobs.get(i));
- }
- } else {
- if (DEBUG) {
- Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Not running anything.");
- }
+
+ // Be ready for next time
+ reset();
}
+
+ private void reset() {
+ chargingCount = 0;
+ idleCount = 0;
+ backoffCount = 0;
+ connectivityCount = 0;
+ contentCount = 0;
+ runnableJobs = null;
+ }
+ }
+ private final MaybeReadyJobQueueFunctor mMaybeQueueFunctor = new MaybeReadyJobQueueFunctor();
+
+ private void maybeQueueReadyJobsForExecutionLockedH() {
+ if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs...");
+
+ mPendingJobs.clear();
+ mJobs.forEachJob(mMaybeQueueFunctor);
+ mMaybeQueueFunctor.postProcess();
}
/**
@@ -1090,17 +1145,27 @@
@Override
public int scheduleAsPackage(JobInfo job, String packageName, int userId)
throws RemoteException {
+ final int callerUid = Binder.getCallingUid();
if (DEBUG) {
- Slog.d(TAG, "Scheduling job: " + job.toString() + " on behalf of " + packageName);
+ Slog.d(TAG, "Caller uid " + callerUid + " scheduling job: " + job.toString()
+ + " on behalf of " + packageName);
}
- final int uid = Binder.getCallingUid();
- if (uid != Process.SYSTEM_UID) {
- throw new IllegalArgumentException("Only system process is allowed"
- + "to set packageName");
+
+ if (packageName == null) {
+ throw new NullPointerException("Must specify a package for scheduleAsPackage()");
}
+
+ int mayScheduleForOthers = getContext().checkCallingOrSelfPermission(
+ android.Manifest.permission.UPDATE_DEVICE_STATS);
+ if (mayScheduleForOthers != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Caller uid " + callerUid
+ + " not permitted to schedule jobs for other apps");
+ }
+
long ident = Binder.clearCallingIdentity();
try {
- return JobSchedulerService.this.scheduleAsPackage(job, uid, packageName, userId);
+ return JobSchedulerService.this.scheduleAsPackage(job, callerUid,
+ packageName, userId);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -1183,7 +1248,7 @@
return s.toString();
}
- void dumpInternal(PrintWriter pw) {
+ void dumpInternal(final PrintWriter pw) {
final long now = SystemClock.elapsedRealtime();
synchronized (mLock) {
pw.print("Started users: ");
@@ -1193,24 +1258,27 @@
pw.println();
pw.println("Registered jobs:");
if (mJobs.size() > 0) {
- ArraySet<JobStatus> jobs = mJobs.getJobs();
- for (int i=0; i<jobs.size(); i++) {
- JobStatus job = jobs.valueAt(i);
- pw.print(" Job #"); pw.print(i); pw.print(": ");
- pw.println(job.toShortString());
- job.dump(pw, " ");
- pw.print(" Ready: ");
- pw.print(mHandler.isReadyToBeExecutedLocked(job));
- pw.print(" (job=");
- pw.print(job.isReady());
- pw.print(" pending=");
- pw.print(mPendingJobs.contains(job));
- pw.print(" active=");
- pw.print(isCurrentlyActiveLocked(job));
- pw.print(" user=");
- pw.print(mStartedUsers.contains(job.getUserId()));
- pw.println(")");
- }
+ mJobs.forEachJob(new JobStatusFunctor() {
+ private int index = 0;
+
+ @Override
+ public void process(JobStatus job) {
+ pw.print(" Job #"); pw.print(index++); pw.print(": ");
+ pw.println(job.toShortString());
+ job.dump(pw, " ");
+ pw.print(" Ready: ");
+ pw.print(mHandler.isReadyToBeExecutedLocked(job));
+ pw.print(" (job=");
+ pw.print(job.isReady());
+ pw.print(" pending=");
+ pw.print(mPendingJobs.contains(job));
+ pw.print(" active=");
+ pw.print(isCurrentlyActiveLocked(job));
+ pw.print(" user=");
+ pw.print(mStartedUsers.contains(job.getUserId()));
+ pw.println(")");
+ }
+ });
} else {
pw.println(" None.");
}
diff --git a/services/core/java/com/android/server/job/JobStore.java b/services/core/java/com/android/server/job/JobStore.java
index 6020247..f8753d6 100644
--- a/services/core/java/com/android/server/job/JobStore.java
+++ b/services/core/java/com/android/server/job/JobStore.java
@@ -29,6 +29,7 @@
import android.util.ArraySet;
import android.util.Pair;
import android.util.Slog;
+import android.util.SparseArray;
import android.util.Xml;
import com.android.internal.annotations.VisibleForTesting;
@@ -68,8 +69,8 @@
/** Threshold to adjust how often we want to write to the db. */
private static final int MAX_OPS_BEFORE_WRITE = 1;
- final ArraySet<JobStatus> mJobSet;
final Object mLock;
+ final JobSet mJobSet; // per-caller-uid tracking
final Context mContext;
private int mDirtyOperations;
@@ -114,7 +115,7 @@
jobDir.mkdirs();
mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
- mJobSet = new ArraySet<JobStatus>();
+ mJobSet = new JobSet();
readJobMapFromDisk(mJobSet);
}
@@ -137,19 +138,6 @@
return replaced;
}
- /**
- * Whether this jobStatus object already exists in the JobStore.
- */
- public boolean containsJobIdForUid(int jobId, int uId) {
- for (int i=mJobSet.size()-1; i>=0; i--) {
- JobStatus ts = mJobSet.valueAt(i);
- if (ts.getUid() == uId && ts.getJobId() == jobId) {
- return true;
- }
- }
- return false;
- }
-
boolean containsJob(JobStatus jobStatus) {
return mJobSet.contains(jobStatus);
}
@@ -158,6 +146,10 @@
return mJobSet.size();
}
+ public int countJobsForUid(int uid) {
+ return mJobSet.countJobsForUid(uid);
+ }
+
/**
* Remove the provided job. Will also delete the job if it was persisted.
* @param writeBack If true, the job will be deleted (if it was persisted) immediately.
@@ -188,14 +180,7 @@
* @return A list of all the jobs scheduled by the provided user. Never null.
*/
public List<JobStatus> getJobsByUser(int userHandle) {
- List<JobStatus> matchingJobs = new ArrayList<JobStatus>();
- for (int i=mJobSet.size()-1; i>=0; i--) {
- JobStatus ts = mJobSet.valueAt(i);
- if (UserHandle.getUserId(ts.getUid()) == userHandle) {
- matchingJobs.add(ts);
- }
- }
- return matchingJobs;
+ return mJobSet.getJobsByUser(userHandle);
}
/**
@@ -203,14 +188,7 @@
* @return All JobStatus objects for a given uid from the master list. Never null.
*/
public List<JobStatus> getJobsByUid(int uid) {
- List<JobStatus> matchingJobs = new ArrayList<JobStatus>();
- for (int i=mJobSet.size()-1; i>=0; i--) {
- JobStatus ts = mJobSet.valueAt(i);
- if (ts.getUid() == uid) {
- matchingJobs.add(ts);
- }
- }
- return matchingJobs;
+ return mJobSet.getJobsByUid(uid);
}
/**
@@ -219,20 +197,21 @@
* @return the JobStatus that matches the provided uId and jobId, or null if none found.
*/
public JobStatus getJobByUidAndJobId(int uid, int jobId) {
- for (int i=mJobSet.size()-1; i>=0; i--) {
- JobStatus ts = mJobSet.valueAt(i);
- if (ts.getUid() == uid && ts.getJobId() == jobId) {
- return ts;
- }
- }
- return null;
+ return mJobSet.get(uid, jobId);
}
/**
- * @return The live array of JobStatus objects.
+ * Iterate over the set of all jobs, invoking the supplied functor on each. This is for
+ * customers who need to examine each job; we'd much rather not have to generate
+ * transient unified collections for them to iterate over and then discard, or creating
+ * iterators every time a client needs to perform a sweep.
*/
- public ArraySet<JobStatus> getJobs() {
- return mJobSet;
+ public void forEachJob(JobStatusFunctor functor) {
+ mJobSet.forEachJob(functor);
+ }
+
+ public interface JobStatusFunctor {
+ public void process(JobStatus jobStatus);
}
/** Version of the db schema. */
@@ -261,7 +240,7 @@
}
@VisibleForTesting
- public void readJobMapFromDisk(ArraySet<JobStatus> jobSet) {
+ public void readJobMapFromDisk(JobSet jobSet) {
new ReadJobMapFromDiskRunnable(jobSet).run();
}
@@ -273,21 +252,19 @@
@Override
public void run() {
final long startElapsed = SystemClock.elapsedRealtime();
- List<JobStatus> mStoreCopy = new ArrayList<JobStatus>();
+ final List<JobStatus> storeCopy = new ArrayList<JobStatus>();
synchronized (mLock) {
- // Copy over the jobs so we can release the lock before writing.
- for (int i=0; i<mJobSet.size(); i++) {
- JobStatus jobStatus = mJobSet.valueAt(i);
-
- if (!jobStatus.isPersisted()){
- continue;
+ // Clone the jobs so we can release the lock before writing.
+ mJobSet.forEachJob(new JobStatusFunctor() {
+ @Override
+ public void process(JobStatus job) {
+ if (job.isPersisted()) {
+ storeCopy.add(new JobStatus(job));
+ }
}
-
- JobStatus copy = new JobStatus(jobStatus);
- mStoreCopy.add(copy);
- }
+ });
}
- writeJobsMapImpl(mStoreCopy);
+ writeJobsMapImpl(storeCopy);
if (JobSchedulerService.DEBUG) {
Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
- startElapsed) + "ms");
@@ -440,13 +417,13 @@
* need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}.
*/
private class ReadJobMapFromDiskRunnable implements Runnable {
- private final ArraySet<JobStatus> jobSet;
+ private final JobSet jobSet;
/**
* @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
* so that after disk read we can populate it directly.
*/
- ReadJobMapFromDiskRunnable(ArraySet<JobStatus> jobSet) {
+ ReadJobMapFromDiskRunnable(JobSet jobSet) {
this.jobSet = jobSet;
}
@@ -759,4 +736,122 @@
return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
}
}
+
+ static class JobSet {
+ // Key is the getUid() originator of the jobs in each sheaf
+ private SparseArray<ArraySet<JobStatus>> mJobs;
+
+ public JobSet() {
+ mJobs = new SparseArray<ArraySet<JobStatus>>();
+ }
+
+ public List<JobStatus> getJobsByUid(int uid) {
+ ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
+ ArraySet<JobStatus> jobs = mJobs.get(uid);
+ if (jobs != null) {
+ matchingJobs.addAll(jobs);
+ }
+ return matchingJobs;
+ }
+
+ // By user, not by uid, so we need to traverse by key and check
+ public List<JobStatus> getJobsByUser(int userId) {
+ ArrayList<JobStatus> result = new ArrayList<JobStatus>();
+ for (int i = mJobs.size() - 1; i >= 0; i--) {
+ if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) {
+ ArraySet<JobStatus> jobs = mJobs.get(i);
+ if (jobs != null) {
+ result.addAll(jobs);
+ }
+ }
+ }
+ return result;
+ }
+
+ public boolean add(JobStatus job) {
+ final int uid = job.getUid();
+ ArraySet<JobStatus> jobs = mJobs.get(uid);
+ if (jobs == null) {
+ jobs = new ArraySet<JobStatus>();
+ mJobs.put(uid, jobs);
+ }
+ return jobs.add(job);
+ }
+
+ public boolean remove(JobStatus job) {
+ final int uid = job.getUid();
+ ArraySet<JobStatus> jobs = mJobs.get(uid);
+ boolean didRemove = (jobs != null) ? jobs.remove(job) : false;
+ if (didRemove && jobs.size() == 0) {
+ // no more jobs for this uid; let the now-empty set object be GC'd.
+ mJobs.remove(uid);
+ }
+ return didRemove;
+ }
+
+ public boolean contains(JobStatus job) {
+ final int uid = job.getUid();
+ ArraySet<JobStatus> jobs = mJobs.get(uid);
+ return jobs != null && jobs.contains(job);
+ }
+
+ public JobStatus get(int uid, int jobId) {
+ ArraySet<JobStatus> jobs = mJobs.get(uid);
+ if (jobs != null) {
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ JobStatus job = jobs.valueAt(i);
+ if (job.getJobId() == jobId) {
+ return job;
+ }
+ }
+ }
+ return null;
+ }
+
+ // Inefficient; use only for testing
+ public List<JobStatus> getAllJobs() {
+ ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
+ for (int i = mJobs.size(); i >= 0; i--) {
+ allJobs.addAll(mJobs.valueAt(i));
+ }
+ return allJobs;
+ }
+
+ public void clear() {
+ mJobs.clear();
+ }
+
+ public int size() {
+ int total = 0;
+ for (int i = mJobs.size() - 1; i >= 0; i--) {
+ total += mJobs.valueAt(i).size();
+ }
+ return total;
+ }
+
+ // We only want to count the jobs that this uid has scheduled on its own
+ // behalf, not those that the app has scheduled on someone else's behalf.
+ public int countJobsForUid(int uid) {
+ int total = 0;
+ ArraySet<JobStatus> jobs = mJobs.get(uid);
+ if (jobs != null) {
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ JobStatus job = jobs.valueAt(i);
+ if (job.getUid() == job.getSourceUid()) {
+ total++;
+ }
+ }
+ }
+ return total;
+ }
+
+ public void forEachJob(JobStatusFunctor functor) {
+ for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
+ ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ functor.process(jobs.valueAt(i));
+ }
+ }
+ }
+ }
}
diff --git a/services/core/java/com/android/server/net/IpConfigStore.java b/services/core/java/com/android/server/net/IpConfigStore.java
index 90e26d8..9f1435a 100644
--- a/services/core/java/com/android/server/net/IpConfigStore.java
+++ b/services/core/java/com/android/server/net/IpConfigStore.java
@@ -59,8 +59,12 @@
protected static final int IPCONFIG_FILE_VERSION = 2;
+ public IpConfigStore(DelayedDiskWrite writer) {
+ mWriter = writer;
+ }
+
public IpConfigStore() {
- mWriter = new DelayedDiskWrite();
+ this(new DelayedDiskWrite());
}
private boolean writeConfig(DataOutputStream out, int configKey,
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index abbad21..41077d0 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -6829,17 +6829,37 @@
// Extract pacakges only if profile-guided compilation is enabled because
// otherwise BackgroundDexOptService will not dexopt them later.
- if (mUseJitProfiles) {
- List<PackageParser.Package> pkgs;
- synchronized (mPackages) {
- pkgs = PackageManagerServiceUtils.getPackagesForDexopt(mPackages.values(), this);
+ if (!mUseJitProfiles || !isUpgrade()) {
+ return;
+ }
+
+ List<PackageParser.Package> pkgs;
+ synchronized (mPackages) {
+ pkgs = PackageManagerServiceUtils.getPackagesForDexopt(mPackages.values(), this);
+ }
+
+ int curr = 0;
+ int total = pkgs.size();
+ for (PackageParser.Package pkg : pkgs) {
+ curr++;
+
+ if (DEBUG_DEXOPT) {
+ Log.i(TAG, "Extracting app " + curr + " of " + total + ": " + pkg.packageName);
}
- for (PackageParser.Package pkg : pkgs) {
- if (PackageDexOptimizer.canOptimizePackage(pkg)) {
- performDexOpt(pkg.packageName, null /* instructionSet */,
- false /* useProfiles */, true /* extractOnly */, false /* force */);
+
+ if (!isFirstBoot()) {
+ try {
+ ActivityManagerNative.getDefault().showBootMessage(
+ mContext.getResources().getString(R.string.android_upgrading_apk,
+ curr, total), true);
+ } catch (RemoteException e) {
}
}
+
+ if (PackageDexOptimizer.canOptimizePackage(pkg)) {
+ performDexOpt(pkg.packageName, null /* instructionSet */,
+ false /* useProfiles */, true /* extractOnly */, false /* force */);
+ }
}
}
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 690cb98..dd13809 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -4434,7 +4434,7 @@
if ((perm.info.flags&PermissionInfo.FLAG_COSTS_MONEY) != 0) {
pw.print(", COSTS_MONEY");
}
- if ((perm.info.flags&PermissionInfo.FLAG_HIDDEN) != 0) {
+ if ((perm.info.flags&PermissionInfo.FLAG_REMOVED) != 0) {
pw.print(", HIDDEN");
}
if ((perm.info.flags&PermissionInfo.FLAG_INSTALLED) != 0) {
@@ -4611,7 +4611,7 @@
if (p.perm != null) {
pw.print(" perm="); pw.println(p.perm);
if ((p.perm.info.flags & PermissionInfo.FLAG_INSTALLED) == 0
- || (p.perm.info.flags & PermissionInfo.FLAG_HIDDEN) != 0) {
+ || (p.perm.info.flags & PermissionInfo.FLAG_REMOVED) != 0) {
pw.print(" flags=0x"); pw.println(Integer.toHexString(p.perm.info.flags));
}
}
diff --git a/services/core/java/com/android/server/policy/GlobalActions.java b/services/core/java/com/android/server/policy/GlobalActions.java
index a0f20aa..5615998 100644
--- a/services/core/java/com/android/server/policy/GlobalActions.java
+++ b/services/core/java/com/android/server/policy/GlobalActions.java
@@ -388,6 +388,8 @@
public void run() {
try {
// Take an "interactive" bugreport.
+ MetricsLogger.visible(this,
+ MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_INTERACTIVE);
ActivityManagerNative.getDefault().requestBugReport(
ActivityManager.BUGREPORT_OPTION_INTERACTIVE);
} catch (RemoteException e) {
@@ -405,6 +407,8 @@
}
try {
// Take a "full" bugreport.
+ MetricsLogger.visible(this,
+ MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_FULL);
ActivityManagerNative.getDefault().requestBugReport(
ActivityManager.BUGREPORT_OPTION_FULL);
} catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 716b96f..26a1941 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -25,6 +25,9 @@
import static android.content.pm.PackageManager.FEATURE_WATCH;
import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
+import static android.view.WindowManager.DOCKED_TOP;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_RIGHT;
import static android.view.WindowManager.LayoutParams.*;
import static android.view.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED;
import static android.view.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT;
@@ -6099,6 +6102,17 @@
}
}
+ @Override
+ public boolean isDockSideAllowed(int dockSide) {
+
+ // We do not allow all dock sides at which the navigation bar touches the docked stack.
+ if (!mNavigationBarCanMove) {
+ return dockSide == DOCKED_TOP || dockSide == DOCKED_LEFT || dockSide == DOCKED_RIGHT;
+ } else {
+ return dockSide == DOCKED_TOP || dockSide == DOCKED_LEFT;
+ }
+ }
+
void sendCloseSystemWindows() {
PhoneWindow.sendCloseSystemWindows(mContext, null);
}
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index dbaa598..f901f95 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -2703,12 +2703,9 @@
if (reason == null) {
reason = "";
}
- if (reason.equals(PowerManager.REBOOT_RECOVERY)) {
- // If we are rebooting to go into recovery, instead of
- // setting sys.powerctl directly we'll start the
- // pre-recovery service which will do some preparation for
- // recovery and then reboot for us.
- SystemProperties.set("ctl.start", "pre-recovery");
+ if (reason.equals(PowerManager.REBOOT_RECOVERY)
+ || reason.equals(PowerManager.REBOOT_RECOVERY_UPDATE)) {
+ SystemProperties.set("sys.powerctl", "reboot,recovery");
} else {
SystemProperties.set("sys.powerctl", "reboot," + reason);
}
@@ -3421,7 +3418,8 @@
@Override // Binder call
public void reboot(boolean confirm, String reason, boolean wait) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.REBOOT, null);
- if (PowerManager.REBOOT_RECOVERY.equals(reason)) {
+ if (PowerManager.REBOOT_RECOVERY.equals(reason)
+ || PowerManager.REBOOT_RECOVERY_UPDATE.equals(reason)) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
}
diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java
index ac6a28e..bcafddc 100644
--- a/services/core/java/com/android/server/power/ShutdownThread.java
+++ b/services/core/java/com/android/server/power/ShutdownThread.java
@@ -32,8 +32,10 @@
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
+import android.os.FileUtils;
import android.os.Handler;
import android.os.PowerManager;
+import android.os.RecoverySystem;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
@@ -81,13 +83,9 @@
private static Object sIsStartedGuard = new Object();
private static boolean sIsStarted = false;
- // uncrypt status files
- private static final String UNCRYPT_STATUS_FILE = "/cache/recovery/uncrypt_status";
- private static final String UNCRYPT_PACKAGE_FILE = "/cache/recovery/uncrypt_file";
-
private static boolean mReboot;
private static boolean mRebootSafeMode;
- private static boolean mRebootUpdate;
+ private static boolean mRebootHasProgressBar;
private static String mReason;
// Provides shutdown assurance in case the system_server is killed
@@ -96,6 +94,9 @@
// Indicates whether we are rebooting into safe mode
public static final String REBOOT_SAFEMODE_PROPERTY = "persist.sys.safemode";
+ // Indicates whether we should stay in safe mode until ro.build.date.utc is newer than this
+ public static final String AUDIT_SAFEMODE_PROPERTY = "persist.sys.audit_safemode";
+
// static instance of this thread
private static final ShutdownThread sInstance = new ShutdownThread();
@@ -213,7 +214,7 @@
public static void reboot(final Context context, String reason, boolean confirm) {
mReboot = true;
mRebootSafeMode = false;
- mRebootUpdate = false;
+ mRebootHasProgressBar = false;
mReason = reason;
shutdownInner(context, confirm);
}
@@ -233,7 +234,7 @@
mReboot = true;
mRebootSafeMode = true;
- mRebootUpdate = false;
+ mRebootHasProgressBar = false;
mReason = null;
shutdownInner(context, confirm);
}
@@ -250,10 +251,19 @@
// Throw up a system dialog to indicate the device is rebooting / shutting down.
ProgressDialog pd = new ProgressDialog(context);
- // Path 1: Reboot to recovery and install the update
- // Condition: mReason == REBOOT_RECOVERY and mRebootUpdate == True
- // (mRebootUpdate is set by checking if /cache/recovery/uncrypt_file exists.)
- // UI: progress bar
+ // Path 1: Reboot to recovery for update
+ // Condition: mReason == REBOOT_RECOVERY_UPDATE
+ //
+ // Path 1a: uncrypt needed
+ // Condition: if /cache/recovery/uncrypt_file exists but
+ // /cache/recovery/block.map doesn't.
+ // UI: determinate progress bar (mRebootHasProgressBar == True)
+ //
+ // * Path 1a is expected to be removed once the GmsCore shipped on
+ // device always calls uncrypt prior to reboot.
+ //
+ // Path 1b: uncrypt already done
+ // UI: spinning circle only (no progress bar)
//
// Path 2: Reboot to recovery for factory reset
// Condition: mReason == REBOOT_RECOVERY
@@ -262,24 +272,31 @@
// Path 3: Regular reboot / shutdown
// Condition: Otherwise
// UI: spinning circle only (no progress bar)
- if (PowerManager.REBOOT_RECOVERY.equals(mReason)) {
- mRebootUpdate = new File(UNCRYPT_PACKAGE_FILE).exists();
- if (mRebootUpdate) {
- pd.setTitle(context.getText(com.android.internal.R.string.reboot_to_update_title));
- pd.setMessage(context.getText(
- com.android.internal.R.string.reboot_to_update_prepare));
+ if (PowerManager.REBOOT_RECOVERY_UPDATE.equals(mReason)) {
+ // We need the progress bar if uncrypt will be invoked during the
+ // reboot, which might be time-consuming.
+ mRebootHasProgressBar = RecoverySystem.UNCRYPT_PACKAGE_FILE.exists()
+ && !(RecoverySystem.BLOCK_MAP_FILE.exists());
+ pd.setTitle(context.getText(com.android.internal.R.string.reboot_to_update_title));
+ if (mRebootHasProgressBar) {
pd.setMax(100);
- pd.setProgressNumberFormat(null);
- pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setProgress(0);
pd.setIndeterminate(false);
- } else {
- // Factory reset path. Set the dialog message accordingly.
- pd.setTitle(context.getText(com.android.internal.R.string.reboot_to_reset_title));
+ pd.setProgressNumberFormat(null);
+ pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMessage(context.getText(
- com.android.internal.R.string.reboot_to_reset_message));
+ com.android.internal.R.string.reboot_to_update_prepare));
+ } else {
pd.setIndeterminate(true);
+ pd.setMessage(context.getText(
+ com.android.internal.R.string.reboot_to_update_reboot));
}
+ } else if (PowerManager.REBOOT_RECOVERY.equals(mReason)) {
+ // Factory reset path. Set the dialog message accordingly.
+ pd.setTitle(context.getText(com.android.internal.R.string.reboot_to_reset_title));
+ pd.setMessage(context.getText(
+ com.android.internal.R.string.reboot_to_reset_message));
+ pd.setIndeterminate(true);
} else {
pd.setTitle(context.getText(com.android.internal.R.string.power_off));
pd.setMessage(context.getText(com.android.internal.R.string.shutdown_progress));
@@ -379,7 +396,7 @@
if (delay <= 0) {
Log.w(TAG, "Shutdown broadcast timed out");
break;
- } else if (mRebootUpdate) {
+ } else if (mRebootHasProgressBar) {
int status = (int)((MAX_BROADCAST_TIME - delay) * 1.0 *
BROADCAST_STOP_PERCENT / MAX_BROADCAST_TIME);
sInstance.setRebootProgress(status, null);
@@ -390,7 +407,7 @@
}
}
}
- if (mRebootUpdate) {
+ if (mRebootHasProgressBar) {
sInstance.setRebootProgress(BROADCAST_STOP_PERCENT, null);
}
@@ -404,7 +421,7 @@
} catch (RemoteException e) {
}
}
- if (mRebootUpdate) {
+ if (mRebootHasProgressBar) {
sInstance.setRebootProgress(ACTIVITY_MANAGER_STOP_PERCENT, null);
}
@@ -415,13 +432,13 @@
if (pm != null) {
pm.shutdown();
}
- if (mRebootUpdate) {
+ if (mRebootHasProgressBar) {
sInstance.setRebootProgress(PACKAGE_MANAGER_STOP_PERCENT, null);
}
// Shutdown radios.
shutdownRadios(MAX_RADIO_WAIT_TIME);
- if (mRebootUpdate) {
+ if (mRebootHasProgressBar) {
sInstance.setRebootProgress(RADIO_STOP_PERCENT, null);
}
@@ -455,7 +472,7 @@
if (delay <= 0) {
Log.w(TAG, "Shutdown wait timed out");
break;
- } else if (mRebootUpdate) {
+ } else if (mRebootHasProgressBar) {
int status = (int)((MAX_SHUTDOWN_WAIT_TIME - delay) * 1.0 *
(MOUNT_SERVICE_STOP_PERCENT - RADIO_STOP_PERCENT) /
MAX_SHUTDOWN_WAIT_TIME);
@@ -468,10 +485,11 @@
}
}
}
- if (mRebootUpdate) {
+ if (mRebootHasProgressBar) {
sInstance.setRebootProgress(MOUNT_SERVICE_STOP_PERCENT, null);
- // If it's to reboot to install update, invoke uncrypt via init service.
+ // If it's to reboot to install an update and uncrypt hasn't been
+ // done yet, trigger it now.
uncrypt();
}
@@ -549,7 +567,7 @@
long delay = endTime - SystemClock.elapsedRealtime();
while (delay > 0) {
- if (mRebootUpdate) {
+ if (mRebootHasProgressBar) {
int status = (int)((timeout - delay) * 1.0 *
(RADIO_STOP_PERCENT - PACKAGE_MANAGER_STOP_PERCENT) / timeout);
status += PACKAGE_MANAGER_STOP_PERCENT;
@@ -651,66 +669,40 @@
private void uncrypt() {
Log.i(TAG, "Calling uncrypt and monitoring the progress...");
+ final RecoverySystem.ProgressListener progressListener =
+ new RecoverySystem.ProgressListener() {
+ @Override
+ public void onProgress(int status) {
+ if (status >= 0 && status < 100) {
+ // Scale down to [MOUNT_SERVICE_STOP_PERCENT, 100).
+ status = (int)(status * (100.0 - MOUNT_SERVICE_STOP_PERCENT) / 100);
+ status += MOUNT_SERVICE_STOP_PERCENT;
+ CharSequence msg = mContext.getText(
+ com.android.internal.R.string.reboot_to_update_package);
+ sInstance.setRebootProgress(status, msg);
+ } else if (status == 100) {
+ CharSequence msg = mContext.getText(
+ com.android.internal.R.string.reboot_to_update_reboot);
+ sInstance.setRebootProgress(status, msg);
+ } else {
+ // Ignored
+ }
+ }
+ };
+
final boolean[] done = new boolean[1];
done[0] = false;
Thread t = new Thread() {
@Override
public void run() {
- // Create the status pipe file to communicate with /system/bin/uncrypt.
- new File(UNCRYPT_STATUS_FILE).delete();
+ RecoverySystem rs = (RecoverySystem) mContext.getSystemService(
+ Context.RECOVERY_SERVICE);
+ String filename = null;
try {
- Os.mkfifo(UNCRYPT_STATUS_FILE, 0600);
- } catch (ErrnoException e) {
- Log.w(TAG, "ErrnoException when creating named pipe \"" + UNCRYPT_STATUS_FILE +
- "\": " + e.getMessage());
- }
-
- SystemProperties.set("ctl.start", "uncrypt");
-
- // Read the status from the pipe.
- try (BufferedReader reader = new BufferedReader(
- new FileReader(UNCRYPT_STATUS_FILE))) {
-
- int lastStatus = Integer.MIN_VALUE;
- while (true) {
- String str = reader.readLine();
- try {
- int status = Integer.parseInt(str);
-
- // Avoid flooding the log with the same message.
- if (status == lastStatus && lastStatus != Integer.MIN_VALUE) {
- continue;
- }
- lastStatus = status;
-
- if (status >= 0 && status < 100) {
- // Update status
- Log.d(TAG, "uncrypt read status: " + status);
- // Scale down to [MOUNT_SERVICE_STOP_PERCENT, 100).
- status = (int)(status * (100.0 - MOUNT_SERVICE_STOP_PERCENT) / 100);
- status += MOUNT_SERVICE_STOP_PERCENT;
- CharSequence msg = mContext.getText(
- com.android.internal.R.string.reboot_to_update_package);
- sInstance.setRebootProgress(status, msg);
- } else if (status == 100) {
- Log.d(TAG, "uncrypt successfully finished.");
- CharSequence msg = mContext.getText(
- com.android.internal.R.string.reboot_to_update_reboot);
- sInstance.setRebootProgress(status, msg);
- break;
- } else {
- // Error in /system/bin/uncrypt. Or it's rebooting to recovery
- // to perform other operations (e.g. factory reset).
- Log.d(TAG, "uncrypt failed with status: " + status);
- break;
- }
- } catch (NumberFormatException unused) {
- Log.d(TAG, "uncrypt invalid status received: " + str);
- break;
- }
- }
- } catch (IOException unused) {
- Log.w(TAG, "IOException when reading \"" + UNCRYPT_STATUS_FILE + "\".");
+ filename = FileUtils.readTextFile(RecoverySystem.UNCRYPT_PACKAGE_FILE, 0, null);
+ rs.processPackage(mContext, new File(filename), progressListener);
+ } catch (IOException e) {
+ Log.e(TAG, "Error uncrypting file", e);
}
done[0] = true;
}
diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
index 4e96d71..bf281d6 100644
--- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java
+++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
@@ -24,6 +24,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.HdmiHotplugEvent;
@@ -45,6 +46,7 @@
import android.media.tv.TvInputHardwareInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvStreamConfig;
+import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
@@ -57,9 +59,13 @@
import android.view.KeyEvent;
import android.view.Surface;
+import com.android.internal.util.IndentingPrintWriter;
import com.android.server.SystemService;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
@@ -549,6 +555,70 @@
return (float) mCurrentIndex / (float) mCurrentMaxIndex;
}
+ public void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
+ final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
+ if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+ pw.println("Permission Denial: can't dump TvInputHardwareManager from pid="
+ + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
+ return;
+ }
+
+ synchronized (mLock) {
+ pw.println("TvInputHardwareManager Info:");
+ pw.increaseIndent();
+ pw.println("mConnections: deviceId -> Connection");
+ pw.increaseIndent();
+ for (int i = 0; i < mConnections.size(); i++) {
+ int deviceId = mConnections.keyAt(i);
+ Connection mConnection = mConnections.valueAt(i);
+ pw.println(deviceId + ": " + mConnection);
+
+ }
+ pw.decreaseIndent();
+
+ pw.println("mHardwareList:");
+ pw.increaseIndent();
+ for (TvInputHardwareInfo tvInputHardwareInfo : mHardwareList) {
+ pw.println(tvInputHardwareInfo);
+ }
+ pw.decreaseIndent();
+
+ pw.println("mHdmiDeviceList:");
+ pw.increaseIndent();
+ for (HdmiDeviceInfo hdmiDeviceInfo : mHdmiDeviceList) {
+ pw.println(hdmiDeviceInfo);
+ }
+ pw.decreaseIndent();
+
+ pw.println("mHardwareInputIdMap: deviceId -> inputId");
+ pw.increaseIndent();
+ for (int i = 0 ; i < mHardwareInputIdMap.size(); i++) {
+ int deviceId = mHardwareInputIdMap.keyAt(i);
+ String inputId = mHardwareInputIdMap.valueAt(i);
+ pw.println(deviceId + ": " + inputId);
+ }
+ pw.decreaseIndent();
+
+ pw.println("mHdmiInputIdMap: id -> inputId");
+ pw.increaseIndent();
+ for (int i = 0; i < mHdmiInputIdMap.size(); i++) {
+ int id = mHdmiInputIdMap.keyAt(i);
+ String inputId = mHdmiInputIdMap.valueAt(i);
+ pw.println(id + ": " + inputId);
+ }
+ pw.decreaseIndent();
+
+ pw.println("mInputMap: inputId -> inputInfo");
+ pw.increaseIndent();
+ for(Map.Entry<String, TvInputInfo> entry : mInputMap.entrySet()) {
+ pw.println(entry.getKey() + ": " + entry.getValue());
+ }
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+ }
+ }
+
private class Connection implements IBinder.DeathRecipient {
private final TvInputHardwareInfo mHardwareInfo;
private TvInputInfo mInfo;
@@ -641,6 +711,17 @@
resetLocked(null, null, null, null, null);
}
}
+
+ public String toString() {
+ return "Connection{"
+ + " mHardwareInfo: " + mHardwareInfo
+ + ", mInfo: " + mInfo
+ + ", mCallback: " + mCallback
+ + ", mConfigs: " + Arrays.toString(mConfigs)
+ + ", mCallingUid: " + mCallingUid
+ + ", mResolvedUserId: " + mResolvedUserId
+ + " }";
+ }
}
private class TvInputHardwareImpl extends ITvInputHardware.Stub {
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 628c627..8a2729e 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -1923,6 +1923,7 @@
pw.decreaseIndent();
}
}
+ mTvInputHardwareManager.dump(fd, writer, args);
}
}
diff --git a/services/core/java/com/android/server/wm/AppWindowAnimator.java b/services/core/java/com/android/server/wm/AppWindowAnimator.java
index fa5ee72..a81fba0 100644
--- a/services/core/java/com/android/server/wm/AppWindowAnimator.java
+++ b/services/core/java/com/android/server/wm/AppWindowAnimator.java
@@ -153,6 +153,13 @@
} else {
mClearProlongedAnimation = true;
}
+
+ // Since we are finally starting our animation, we don't need the logic anymore to prevent
+ // the app from showing again if we just moved between stacks. See
+ // {@link WindowState#notifyMovedInStack}.
+ for (int i = mAppToken.allAppWindows.size() - 1; i >= 0; i--) {
+ mAppToken.allAppWindows.get(i).resetJustMovedInStack();
+ }
}
public void setDummyAnimation() {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 325005b..00690c4 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -247,7 +247,15 @@
mStack.removeTask(this);
}
stack.positionTask(this, position, showForAllUsers());
- setBounds(bounds, config);
+ resizeLocked(bounds, config, false /* force */);
+
+ for (int activityNdx = mAppTokens.size() - 1; activityNdx >= 0; --activityNdx) {
+ final ArrayList<WindowState> windows = mAppTokens.get(activityNdx).allAppWindows;
+ for (int winNdx = windows.size() - 1; winNdx >= 0; --winNdx) {
+ final WindowState win = windows.get(winNdx);
+ win.notifyMovedInStack();
+ }
+ }
}
boolean removeAppToken(AppWindowToken wtoken) {
@@ -272,7 +280,7 @@
}
/** Set the task bounds. Passing in null sets the bounds to fullscreen. */
- int setBounds(Rect bounds, Configuration config) {
+ private int setBounds(Rect bounds, Configuration config) {
if (config == null) {
config = Configuration.EMPTY;
}
@@ -598,13 +606,21 @@
void resizeWindows() {
final ArrayList<WindowState> resizingWindows = mService.mResizingWindows;
for (int activityNdx = mAppTokens.size() - 1; activityNdx >= 0; --activityNdx) {
- final ArrayList<WindowState> windows = mAppTokens.get(activityNdx).allAppWindows;
+ final AppWindowToken atoken = mAppTokens.get(activityNdx);
+
+ // Some windows won't go through the resizing process, if they don't have a surface, so
+ // destroy all saved surfaces here.
+ atoken.destroySavedSurfaces();
+ final ArrayList<WindowState> windows = atoken.allAppWindows;
for (int winNdx = windows.size() - 1; winNdx >= 0; --winNdx) {
final WindowState win = windows.get(winNdx);
if (win.mHasSurface && !resizingWindows.contains(win)) {
if (DEBUG_RESIZE) Slog.d(TAG, "resizeWindows: Resizing " + win);
resizingWindows.add(win);
}
+ if (win.isGoneForLayoutLw()) {
+ win.mResizedWhileGone = true;
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java
index 06e5ac5..07a6514 100644
--- a/services/core/java/com/android/server/wm/TaskStack.java
+++ b/services/core/java/com/android/server/wm/TaskStack.java
@@ -352,6 +352,7 @@
mDisplayContent.rotateBounds(mRotation, newRotation, mTmpRect2);
if (mStackId == DOCKED_STACK_ID) {
+ repositionDockedStackAfterRotation(mTmpRect2);
snapDockedStackAfterRotation(mTmpRect2);
}
@@ -363,6 +364,43 @@
}
/**
+ * Some dock sides are not allowed by the policy. This method queries the policy and moves
+ * the docked stack around if needed.
+ *
+ * @param inOutBounds the bounds of the docked stack to adjust
+ */
+ private void repositionDockedStackAfterRotation(Rect inOutBounds) {
+ int dockSide = getDockSide(inOutBounds);
+ if (mService.mPolicy.isDockSideAllowed(dockSide)) {
+ return;
+ }
+ mDisplayContent.getLogicalDisplayRect(mTmpRect);
+ dockSide = DockedDividerUtils.invertDockSide(dockSide);
+ switch (dockSide) {
+ case DOCKED_LEFT:
+ int movement = inOutBounds.left;
+ inOutBounds.left -= movement;
+ inOutBounds.right -= movement;
+ break;
+ case DOCKED_RIGHT:
+ movement = mTmpRect.right - inOutBounds.right;
+ inOutBounds.left += movement;
+ inOutBounds.right += movement;
+ break;
+ case DOCKED_TOP:
+ movement = inOutBounds.top;
+ inOutBounds.top -= movement;
+ inOutBounds.bottom -= movement;
+ break;
+ case DOCKED_BOTTOM:
+ movement = mTmpRect.bottom - inOutBounds.bottom;
+ inOutBounds.top += movement;
+ inOutBounds.bottom += movement;
+ break;
+ }
+ }
+
+ /**
* Snaps the bounds after rotation to the closest snap target for the docked stack.
*/
private void snapDockedStackAfterRotation(Rect outBounds) {
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index 38d0711..85bddee 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -530,7 +530,7 @@
for (int i = windows.size() - 1; i >= 0; i--) {
final WindowState win = windows.get(i);
WindowStateAnimator winAnimator = win.mWinAnimator;
- if (winAnimator.mSurfaceController == null) {
+ if (winAnimator.mSurfaceController == null || !winAnimator.hasSurface()) {
continue;
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 142715e..f36585e 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -320,6 +320,8 @@
private static final float DRAG_SHADOW_ALPHA_TRANSPARENT = .7071f;
+ private static final String PROPERTY_BUILD_DATE_UTC = "ro.build.date.utc";
+
final private KeyguardDisableHandler mKeyguardDisableHandler;
final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -2758,6 +2760,9 @@
winAnimator.mReportSurfaceResized = false;
result |= WindowManagerGlobal.RELAYOUT_RES_SURFACE_RESIZED;
}
+ if (!win.isGoneForLayoutLw()) {
+ win.mResizedWhileGone = false;
+ }
outFrame.set(win.mCompatFrame);
outOverscanInsets.set(win.mOverscanInsets);
outContentInsets.set(win.mContentInsets);
@@ -7458,8 +7463,22 @@
|| volumeDownState > 0;
try {
if (SystemProperties.getInt(ShutdownThread.REBOOT_SAFEMODE_PROPERTY, 0) != 0) {
- mSafeMode = true;
- SystemProperties.set(ShutdownThread.REBOOT_SAFEMODE_PROPERTY, "");
+ int auditSafeMode = SystemProperties.getInt(ShutdownThread.AUDIT_SAFEMODE_PROPERTY, 0);
+
+ if (auditSafeMode == 0) {
+ mSafeMode = true;
+ SystemProperties.set(ShutdownThread.REBOOT_SAFEMODE_PROPERTY, "");
+ } else {
+ // stay in safe mode until we have updated to a newer build
+ int buildDate = SystemProperties.getInt(PROPERTY_BUILD_DATE_UTC, 0);
+
+ if (auditSafeMode >= buildDate) {
+ mSafeMode = true;
+ } else {
+ SystemProperties.set(ShutdownThread.REBOOT_SAFEMODE_PROPERTY, "");
+ SystemProperties.set(ShutdownThread.AUDIT_SAFEMODE_PROPERTY, "");
+ }
+ }
}
} catch (IllegalArgumentException e) {
}
@@ -10663,6 +10682,7 @@
@Override
public void waitForAllWindowsDrawn(Runnable callback, long timeout) {
+ boolean allWindowsDrawn = false;
synchronized (mWindowMap) {
mWaitingForDrawnCallback = callback;
final WindowList windows = getDefaultWindowListLocked();
@@ -10683,13 +10703,16 @@
}
}
mWindowPlacerLocked.requestTraversal();
+ mH.removeMessages(H.WAITING_FOR_DRAWN_TIMEOUT);
+ if (mWaitingForDrawn.isEmpty()) {
+ allWindowsDrawn = true;
+ } else {
+ mH.sendEmptyMessageDelayed(H.WAITING_FOR_DRAWN_TIMEOUT, timeout);
+ checkDrawnWindowsLocked();
+ }
}
- mH.removeMessages(H.WAITING_FOR_DRAWN_TIMEOUT);
- if (mWaitingForDrawn.isEmpty()) {
+ if (allWindowsDrawn) {
callback.run();
- } else {
- mH.sendEmptyMessageDelayed(H.WAITING_FOR_DRAWN_TIMEOUT, timeout);
- checkDrawnWindowsLocked();
}
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index f30c8d3..9dcb404 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -448,6 +448,16 @@
final private Rect mTmpRect = new Rect();
+ /**
+ * See {@link #notifyMovedInStack}.
+ */
+ private boolean mJustMovedInStack;
+
+ /**
+ * Whether the window was resized by us while it was gone for layout.
+ */
+ boolean mResizedWhileGone = false;
+
WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token,
WindowState attachedWindow, int appOp, int seq, WindowManager.LayoutParams a,
int viewVisibility, final DisplayContent displayContent) {
@@ -1235,7 +1245,7 @@
return mViewVisibility == View.GONE
|| !mRelayoutCalled
|| (atoken == null && mRootToken.hidden)
- || (atoken != null && (atoken.hiddenRequested || atoken.hidden))
+ || (atoken != null && atoken.hiddenRequested)
|| mAttachedHidden
|| (mAnimatingExit && !isAnimatingLw())
|| mDestroying;
@@ -1374,6 +1384,39 @@
}
}
+ /**
+ * Notifies this window that the corresponding task has just moved in the stack.
+ * <p>
+ * This is used to fix the following: If we moved in the stack, and if the last clip rect was
+ * empty, meaning that our task was completely offscreen, we need to keep it invisible because
+ * the actual app transition that updates the visibility is delayed by a few transactions.
+ * Instead of messing around with the ordering and timing how transitions and transactions are
+ * executed, we introduce this little hack which prevents this window of getting visible again
+ * with the wrong bounds until the app transitions has started.
+ * <p>
+ * This method notifies the window about that we just moved in the stack so we can apply this
+ * logic in {@link WindowStateAnimator#updateSurfaceWindowCrop}
+ */
+ void notifyMovedInStack() {
+ mJustMovedInStack = true;
+ }
+
+ /**
+ * See {@link #notifyMovedInStack}.
+ *
+ * @return Whether we just got moved in the corresponding stack.
+ */
+ boolean hasJustMovedInStack() {
+ return mJustMovedInStack;
+ }
+
+ /**
+ * Resets that we just moved in the corresponding stack. See {@link #notifyMovedInStack}.
+ */
+ void resetJustMovedInStack() {
+ mJustMovedInStack = false;
+ }
+
private final class DeadWindowEventReceiver extends InputEventReceiver {
DeadWindowEventReceiver(InputChannel inputChannel) {
super(inputChannel, mService.mH.getLooper());
@@ -1836,6 +1879,12 @@
return false;
}
+ if (mResizedWhileGone) {
+ // Somebody resized our window while we were gone for layout, which means that the
+ // client got an old size, so we have an outdated surface here.
+ return false;
+ }
+
if (DEBUG_DISABLE_SAVING_SURFACES) {
return false;
}
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 0828417..2d8a4c9 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -1191,6 +1191,11 @@
w.transformFromScreenToSurfaceSpace(clipRect);
+ // See {@link WindowState#notifyMovedInStack} for why this is necessary.
+ if (w.hasJustMovedInStack() && mLastClipRect.isEmpty() && !clipRect.isEmpty()) {
+ clipRect.setEmpty();
+ }
+
if (!clipRect.equals(mLastClipRect)) {
mLastClipRect.set(clipRect);
mSurfaceController.setCropInTransaction(clipRect, recoveringMemory);
diff --git a/services/core/java/com/android/server/wm/WindowSurfaceController.java b/services/core/java/com/android/server/wm/WindowSurfaceController.java
index 93164de..0db6f3a 100644
--- a/services/core/java/com/android/server/wm/WindowSurfaceController.java
+++ b/services/core/java/com/android/server/wm/WindowSurfaceController.java
@@ -62,7 +62,7 @@
// However, we need to somehow handle the situation where the cropping would completely hide
// the window. We achieve this by explicitly hiding the surface and not letting it be shown.
private boolean mHiddenForCrop = false;
-
+ private boolean mHiddenForOtherReasons = false;
private final String title;
public WindowSurfaceController(SurfaceSession s,
@@ -95,6 +95,11 @@
void hideInTransaction(String reason) {
if (SHOW_TRANSACTIONS) logSurface("HIDE ( " + reason + " )", null);
+ mHiddenForOtherReasons = true;
+ updateVisibility();
+ }
+
+ private void hideSurface() {
if (mSurfaceControl != null) {
mSurfaceShown = false;
try {
@@ -152,9 +157,10 @@
if (clipRect.width() > 0 && clipRect.height() > 0) {
mSurfaceControl.setWindowCrop(clipRect);
mHiddenForCrop = false;
+ updateVisibility();
} else {
- hideInTransaction("setCrop");
mHiddenForCrop = true;
+ updateVisibility();
}
} catch (RuntimeException e) {
Slog.w(TAG, "Error setting crop surface of " + this
@@ -317,11 +323,26 @@
"SHOW (performLayout)", null);
if (DEBUG_VISIBILITY) Slog.v(TAG, "Showing " + this
+ " during relayout");
+ mHiddenForOtherReasons = false;
+ return updateVisibility();
+ }
- if (mHiddenForCrop) {
+ private boolean updateVisibility() {
+ if (mHiddenForCrop || mHiddenForOtherReasons) {
+ if (mSurfaceShown) {
+ hideSurface();
+ }
return false;
+ } else {
+ if (!mSurfaceShown) {
+ return showSurface();
+ } else {
+ return true;
+ }
}
+ }
+ private boolean showSurface() {
try {
mSurfaceShown = true;
mSurfaceControl.show();
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 33225eb..725f6bf 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -6743,57 +6743,6 @@
}
}
- @Override
- public UserHandle createUser(ComponentName who, String name) {
- Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
- getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
-
- long id = mInjector.binderClearCallingIdentity();
- try {
- UserInfo userInfo = mUserManager.createUser(name, 0 /* flags */);
- if (userInfo != null) {
- return userInfo.getUserHandle();
- }
- return null;
- } finally {
- mInjector.binderRestoreCallingIdentity(id);
- }
- }
- }
-
- @Override
- public UserHandle createAndInitializeUser(ComponentName who, String name,
- String ownerName, ComponentName profileOwnerComponent, Bundle adminExtras) {
- UserHandle user = createUser(who, name);
- if (user == null) {
- return null;
- }
- long id = mInjector.binderClearCallingIdentity();
- try {
- String profileOwnerPkg = profileOwnerComponent.getPackageName();
-
- final int userHandle = user.getIdentifier();
- try {
- // Install the profile owner if not present.
- if (!mIPackageManager.isPackageAvailable(profileOwnerPkg, userHandle)) {
- mIPackageManager.installExistingPackageAsUser(profileOwnerPkg, userHandle);
- }
-
- // Start user in background.
- mInjector.getIActivityManager().startUserInBackground(userHandle);
- } catch (RemoteException e) {
- Slog.e(LOG_TAG, "Failed to make remote calls for configureUser", e);
- }
-
- setActiveAdmin(profileOwnerComponent, true, userHandle, adminExtras);
- setProfileOwner(profileOwnerComponent, ownerName, userHandle);
- return user;
- } finally {
- mInjector.binderRestoreCallingIdentity(id);
- }
- }
-
private void sendAdminEnabledBroadcastLocked(int userHandle) {
DevicePolicyData policyData = getUserData(userHandle);
if (policyData.mAdminBroadcastPending) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index c854573..c75f98f 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -341,8 +341,8 @@
// always make sure uncrypt gets executed properly when needed.
// If '/cache/recovery/block.map' hasn't been created, stop the
// reboot which will fail for sure, and get a chance to capture a
- // bugreport when that's still feasible. (Bug; 26444951)
- if (PowerManager.REBOOT_RECOVERY.equals(reason)) {
+ // bugreport when that's still feasible. (Bug: 26444951)
+ if (PowerManager.REBOOT_RECOVERY_UPDATE.equals(reason)) {
File packageFile = new File(UNCRYPT_PACKAGE_FILE);
if (packageFile.exists()) {
String filename = null;
@@ -833,6 +833,10 @@
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
+ if (!disableNonCoreServices) {
+ mSystemServiceManager.startService(RecoverySystemService.class);
+ }
+
/*
* MountService has a few dependencies: Notification Manager and
* AppWidget Provider. Make sure MountService is completely started
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index c786036..e64481e 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -12,6 +12,7 @@
import android.util.Log;
import android.util.ArraySet;
+import com.android.server.job.JobStore.JobSet;
import com.android.server.job.controllers.JobStatus;
import java.util.Iterator;
@@ -62,11 +63,11 @@
mTaskStoreUnderTest.add(ts);
Thread.sleep(IO_WAIT);
// Manually load tasks from xml file.
- final ArraySet<JobStatus> jobStatusSet = new ArraySet<JobStatus>();
+ final JobSet jobStatusSet = new JobSet();
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
assertEquals("Didn't get expected number of persisted tasks.", 1, jobStatusSet.size());
- final JobStatus loadedTaskStatus = jobStatusSet.iterator().next();
+ final JobStatus loadedTaskStatus = jobStatusSet.getAllJobs().get(0);
assertTasksEqual(task, loadedTaskStatus.getJob());
assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(ts));
assertEquals("Different uids.", SOME_UID, loadedTaskStatus.getUid());
@@ -97,10 +98,10 @@
mTaskStoreUnderTest.add(taskStatus2);
Thread.sleep(IO_WAIT);
- final ArraySet<JobStatus> jobStatusSet = new ArraySet<JobStatus>();
+ final JobSet jobStatusSet = new JobSet();
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
assertEquals("Incorrect # of persisted tasks.", 2, jobStatusSet.size());
- Iterator<JobStatus> it = jobStatusSet.iterator();
+ Iterator<JobStatus> it = jobStatusSet.getAllJobs().iterator();
JobStatus loaded1 = it.next();
JobStatus loaded2 = it.next();
@@ -145,10 +146,10 @@
mTaskStoreUnderTest.add(taskStatus);
Thread.sleep(IO_WAIT);
- final ArraySet<JobStatus> jobStatusSet = new ArraySet<JobStatus>();
+ final JobSet jobStatusSet = new JobSet();
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
- JobStatus loaded = jobStatusSet.iterator().next();
+ JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
assertTasksEqual(task, loaded.getJob());
}
public void testWritingTaskWithSourcePackage() throws Exception {
@@ -163,10 +164,10 @@
mTaskStoreUnderTest.add(taskStatus);
Thread.sleep(IO_WAIT);
- final ArraySet<JobStatus> jobStatusSet = new ArraySet<JobStatus>();
+ final JobSet jobStatusSet = new JobSet();
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
- JobStatus loaded = jobStatusSet.iterator().next();
+ JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
assertEquals("Source package not equal.", loaded.getSourcePackageName(),
taskStatus.getSourcePackageName());
assertEquals("Source user not equal.", loaded.getSourceUserId(),
@@ -184,10 +185,10 @@
mTaskStoreUnderTest.add(taskStatus);
Thread.sleep(IO_WAIT);
- final ArraySet<JobStatus> jobStatusSet = new ArraySet<JobStatus>();
+ final JobSet jobStatusSet = new JobSet();
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
- JobStatus loaded = jobStatusSet.iterator().next();
+ JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
assertEquals("Period not equal.", loaded.getJob().getIntervalMillis(),
taskStatus.getJob().getIntervalMillis());
assertEquals("Flex not equal.", loaded.getJob().getFlexMillis(),
@@ -211,10 +212,10 @@
mTaskStoreUnderTest.add(js);
Thread.sleep(IO_WAIT);
- final ArraySet<JobStatus> jobStatusSet = new ArraySet<JobStatus>();
+ final JobSet jobStatusSet = new JobSet();
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
- JobStatus loaded = jobStatusSet.iterator().next();
+ JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
// Assert early runtime was clamped to be under now + period. We can do <= here b/c we'll
// call SystemClock.elapsedRealtime after doing the disk i/o.
@@ -234,9 +235,9 @@
final JobStatus js = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1);
mTaskStoreUnderTest.add(js);
Thread.sleep(IO_WAIT);
- final ArraySet<JobStatus> jobStatusSet = new ArraySet<JobStatus>();
+ final JobSet jobStatusSet = new JobSet();
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
- JobStatus loaded = jobStatusSet.iterator().next();
+ JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
assertEquals("Priority not correctly persisted.", 42, loaded.getPriority());
}
@@ -255,10 +256,10 @@
JobStatus jsPersisted = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1);
mTaskStoreUnderTest.add(jsPersisted);
Thread.sleep(IO_WAIT);
- final ArraySet<JobStatus> jobStatusSet = new ArraySet<JobStatus>();
+ final JobSet jobStatusSet = new JobSet();
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
assertEquals("Job count is incorrect.", 1, jobStatusSet.size());
- JobStatus jobStatus = jobStatusSet.iterator().next();
+ JobStatus jobStatus = jobStatusSet.getAllJobs().iterator().next();
assertEquals("Wrong job persisted.", 43, jobStatus.getJobId());
}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
index b4c4bf8..7ee7423 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
@@ -199,6 +199,7 @@
}
modelData.setHandle(handle[0]);
modelData.setLoaded();
+ Slog.d(TAG, "Generic sound model loaded with handle:" + handle[0]);
}
modelData.setCallback(callback);
modelData.setRecognitionConfig(recognitionConfig);
@@ -227,7 +228,7 @@
synchronized (mLock) {
if (DBG) {
- Slog.d(TAG, "startRecognition for keyphraseId=" + keyphraseId
+ Slog.d(TAG, "startKeyphraseRecognition for keyphraseId=" + keyphraseId
+ " soundModel=" + soundModel + ", listener=" + listener.asBinder()
+ ", recognitionConfig=" + recognitionConfig);
Slog.d(TAG, "moduleProperties=" + mModuleProperties);
@@ -243,13 +244,13 @@
}
if (mModuleProperties == null) {
- Slog.w(TAG, "Attempting startRecognition without the capability");
+ Slog.w(TAG, "Attempting startKeyphraseRecognition without the capability");
return STATUS_ERROR;
}
if (mModule == null) {
mModule = SoundTrigger.attachModule(mModuleProperties.id, this, null);
if (mModule == null) {
- Slog.w(TAG, "startRecognition cannot attach to sound trigger module");
+ Slog.w(TAG, "startKeyphraseRecognition cannot attach to sound trigger module");
return STATUS_ERROR;
}
}
@@ -348,26 +349,29 @@
}
if (currentCallback == null || !modelData.isModelStarted()) {
- // startRecognition hasn't been called or it failed.
- Slog.w(TAG, "Attempting stopRecognition without a successful startRecognition");
+ // startGenericRecognition hasn't been called or it failed.
+ Slog.w(TAG, "Attempting stopGenericRecognition without a successful" +
+ " startGenericRecognition");
return STATUS_ERROR;
}
if (currentCallback.asBinder() != listener.asBinder()) {
// We don't allow a different listener to stop the recognition than the one
// that started it.
- Slog.w(TAG, "Attempting stopRecognition for another recognition");
+ Slog.w(TAG, "Attempting stopGenericRecognition for another recognition");
return STATUS_ERROR;
}
- int status = stopGenericRecognitionLocked(modelData, false /* don't notify for synchronous calls */);
+ int status = stopGenericRecognitionLocked(modelData,
+ false /* don't notify for synchronous calls */);
if (status != SoundTrigger.STATUS_OK) {
+ Slog.w(TAG, "stopGenericRecognition failed: " + status);
return status;
}
// We leave the sound model loaded but not started, this helps us when we start
// back.
// Also clear the internal state once the recognition has been stopped.
- modelData.clearState();
+ modelData.setLoaded();
modelData.clearCallback();
if (!computeRecognitionRunning()) {
internalClearGlobalStateLocked();
@@ -471,6 +475,66 @@
return mModuleProperties;
}
+ int unloadKeyphraseSoundModel(int keyphraseId) {
+ if (mModule == null || mCurrentKeyphraseModelHandle == INVALID_VALUE) {
+ return STATUS_ERROR;
+ }
+ if (mKeyphraseId != keyphraseId) {
+ Slog.w(TAG, "Given sound model is not the one loaded.");
+ return STATUS_ERROR;
+ }
+
+ synchronized (mLock) {
+ // Stop recognition if it's the current one.
+ mRequested = false;
+ int status = updateRecognitionLocked(false /* don't notify */);
+ if (status != SoundTrigger.STATUS_OK) {
+ Slog.w(TAG, "Stop recognition failed for keyphrase ID:" + status);
+ }
+
+ status = mModule.unloadSoundModel(mCurrentKeyphraseModelHandle);
+ if (status != SoundTrigger.STATUS_OK) {
+ Slog.w(TAG, "unloadKeyphraseSoundModel call failed with " + status);
+ }
+ internalClearKeyphraseSoundModelLocked();
+ return status;
+ }
+ }
+
+ int unloadGenericSoundModel(UUID modelId) {
+ if (modelId == null || mModule == null) {
+ return STATUS_ERROR;
+ }
+ ModelData modelData = mGenericModelDataMap.get(modelId);
+ if (modelData == null) {
+ Slog.w(TAG, "Unload error: Attempting unload invalid generic model with id:" + modelId);
+ return STATUS_ERROR;
+ }
+ synchronized (mLock) {
+ if (!modelData.isModelLoaded()) {
+ // Nothing to do here.
+ Slog.i(TAG, "Unload: Given generic model is not loaded:" + modelId);
+ return STATUS_OK;
+ }
+ if (modelData.isModelStarted()) {
+ int status = stopGenericRecognitionLocked(modelData,
+ false /* don't notify for synchronous calls */);
+ if (status != SoundTrigger.STATUS_OK) {
+ Slog.w(TAG, "stopGenericRecognition failed: " + status);
+ }
+ }
+
+ int status = mModule.unloadSoundModel(modelData.getHandle());
+ if (status != SoundTrigger.STATUS_OK) {
+ Slog.w(TAG, "unloadGenericSoundModel() call failed with " + status);
+ Slog.w(TAG, "unloadGenericSoundModel() force-marking model as unloaded.");
+ }
+ mGenericModelDataMap.remove(modelId);
+ if (DBG) dumpGenericModelState();
+ return status;
+ }
+ }
+
//---- SoundTrigger.StatusListener methods
@Override
public void onRecognition(RecognitionEvent event) {
@@ -913,6 +977,7 @@
}
}
} else {
+ Slog.i(TAG, "startRecognition successful.");
modelData.setStarted();
// Notify of resume if needed.
if (notify) {
@@ -923,6 +988,7 @@
}
}
}
+ if (DBG) dumpGenericModelState();
return status;
}
@@ -951,9 +1017,17 @@
}
}
}
+ if (DBG) dumpGenericModelState();
return status;
}
+ private void dumpGenericModelState() {
+ for (UUID modelId : mGenericModelDataMap.keySet()) {
+ ModelData modelData = mGenericModelDataMap.get(modelId);
+ Slog.i(TAG, "Model :" + modelData.toString());
+ }
+ }
+
// Computes whether we have any recognition running at all (voice or generic). Sets
// the mRecognitionRunning variable with the result.
private boolean computeRecognitionRunning() {
@@ -1069,5 +1143,18 @@
synchronized RecognitionConfig getRecognitionConfig() {
return mRecognitionConfig;
}
+
+ String stateToString() {
+ switch(mModelState) {
+ case MODEL_NOTLOADED: return "NOT_LOADED";
+ case MODEL_LOADED: return "LOADED";
+ case MODEL_STARTED: return "STARTED";
+ }
+ return "Unknown state";
+ }
+
+ public String toString() {
+ return "Handle: " + mModelHandle + "ModelState: " + stateToString();
+ }
}
}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java
index 7722876..113431f 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java
@@ -75,5 +75,7 @@
public abstract ModuleProperties getModuleProperties();
+ public abstract int unloadKeyphraseModel(int keyphaseId);
+
public abstract void dump(FileDescriptor fd, PrintWriter pw, String[] args);
}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 251f314..a4c1210 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -122,10 +122,10 @@
public int startRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback,
RecognitionConfig config) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
+ if (!isInitialized()) return STATUS_ERROR;
if (DEBUG) {
Slog.i(TAG, "startRecognition(): Uuid : " + parcelUuid);
}
- if (!isInitialized()) return STATUS_ERROR;
GenericSoundModel model = getSoundModel(parcelUuid);
if (model == null) {
@@ -173,6 +173,8 @@
if (DEBUG) {
Slog.i(TAG, "deleteSoundModel(): id = " + soundModelId);
}
+ // Unload the model if it is loaded.
+ mSoundTriggerHelper.unloadGenericSoundModel(soundModelId.getUuid());
mDbHelper.deleteGenericSoundModel(soundModelId.getUuid());
}
}
@@ -216,6 +218,12 @@
}
@Override
+ public int unloadKeyphraseModel(int keyphraseId) {
+ if (!isInitialized()) return STATUS_ERROR;
+ return mSoundTriggerHelper.unloadKeyphraseSoundModel(keyphraseId);
+ }
+
+ @Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!isInitialized()) return;
mSoundTriggerHelper.dump(fd, pw, args);
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 4a54643..6ab0b99 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -760,6 +760,10 @@
final long caller = Binder.clearCallingIdentity();
boolean deleted = false;
try {
+ int unloadStatus = mSoundTriggerInternal.unloadKeyphraseModel(keyphraseId);
+ if (unloadStatus != SoundTriggerInternal.STATUS_OK) {
+ Slog.w(TAG, "Unable to unload keyphrase sound model:" + unloadStatus);
+ }
deleted = mDbHelper.deleteKeyphraseSoundModel(keyphraseId, callingUid, bcp47Locale);
return deleted ? SoundTriggerInternal.STATUS_OK : SoundTriggerInternal.STATUS_ERROR;
} finally {
diff --git a/tests/SoundTriggerTestApp/AndroidManifest.xml b/tests/SoundTriggerTestApp/AndroidManifest.xml
index a72b3dd..dc4cdb5 100644
--- a/tests/SoundTriggerTestApp/AndroidManifest.xml
+++ b/tests/SoundTriggerTestApp/AndroidManifest.xml
@@ -2,10 +2,12 @@
package="com.android.test.soundtrigger">
<uses-permission android:name="android.permission.MANAGE_SOUND_TRIGGER" />
+ <uses-permission android:name="android.permission.WAKE_LOCk" />
<application>
<activity
android:name="TestSoundTriggerActivity"
android:label="SoundTrigger Test Application"
+ android:screenOrientation="portrait"
android:theme="@android:style/Theme.Material">
<!--
<intent-filter>
diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java
index 3149783..95e2dcf 100644
--- a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java
+++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java
@@ -28,6 +28,8 @@
import android.text.Editable;
import android.text.method.ScrollingMovementMethod;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
import android.os.UserManager;
import android.util.Log;
import android.view.View;
@@ -54,6 +56,8 @@
private TextView mDebugView = null;
private int mSelectedModelId = 1;
private ScrollView mScrollView = null;
+ private PowerManager.WakeLock mScreenWakelock;
+ private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -66,6 +70,7 @@
mDebugView.setMovementMethod(new ScrollingMovementMethod());
mSoundTriggerUtil = new SoundTriggerUtil(this);
mRandom = new Random();
+ mHandler = new Handler();
}
private void postMessage(String msg) {
@@ -85,24 +90,43 @@
});
}
- private UUID getSelectedUuid() {
+ private synchronized UUID getSelectedUuid() {
if (mSelectedModelId == 2) return mModelUuid2;
if (mSelectedModelId == 3) return mModelUuid3;
return mModelUuid1; // Default.
}
- private void setDetector(SoundTriggerDetector detector) {
- if (mSelectedModelId == 2) mDetector2 = detector;
- if (mSelectedModelId == 3) mDetector3 = detector;
+ private synchronized void setDetector(SoundTriggerDetector detector) {
+ if (mSelectedModelId == 2) {
+ mDetector2 = detector;
+ return;
+ }
+ if (mSelectedModelId == 3) {
+ mDetector3 = detector;
+ return;
+ }
mDetector1 = detector;
}
- private SoundTriggerDetector getDetector() {
+ private synchronized SoundTriggerDetector getDetector() {
if (mSelectedModelId == 2) return mDetector2;
if (mSelectedModelId == 3) return mDetector3;
return mDetector1;
}
+ private void screenWakeup() {
+ PowerManager pm = ((PowerManager)getSystemService(POWER_SERVICE));
+ if (mScreenWakelock == null) {
+ mScreenWakelock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "TAG");
+ }
+ mScreenWakelock.acquire();
+ }
+
+ private void screenRelease() {
+ PowerManager pm = ((PowerManager)getSystemService(POWER_SERVICE));
+ mScreenWakelock.release();
+ }
+
/**
* Called when the user clicks the enroll button.
* Performs a fresh enrollment.
@@ -139,7 +163,7 @@
Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show();
return;
}
- boolean status = mSoundTriggerUtil.deleteSoundModel(mModelUuid1);
+ boolean status = mSoundTriggerUtil.deleteSoundModel(modelUuid);
if (status) {
Toast.makeText(this, "Successfully deleted model UUID=" + soundModel.uuid,
Toast.LENGTH_SHORT)
@@ -204,22 +228,28 @@
}
}
- public void onRadioButtonClicked(View view) {
+ public synchronized void onRadioButtonClicked(View view) {
// Is the button now checked?
boolean checked = ((RadioButton) view).isChecked();
// Check which radio button was clicked
switch(view.getId()) {
case R.id.model_one:
- if (checked) mSelectedModelId = 1;
- postMessage("Selected model one.");
+ if (checked) {
+ mSelectedModelId = 1;
+ postMessage("Selected model one.");
+ }
break;
case R.id.model_two:
- if (checked) mSelectedModelId = 2;
- postMessage("Selected model two.");
+ if (checked) {
+ mSelectedModelId = 2;
+ postMessage("Selected model two.");
+ }
break;
case R.id.model_three:
- if (checked) mSelectedModelId = 3;
- postMessage("Selected model three.");
+ if (checked) {
+ mSelectedModelId = 3;
+ postMessage("Selected model three.");
+ }
break;
}
}
@@ -232,6 +262,13 @@
public void onDetected(SoundTriggerDetector.EventPayload event) {
postMessage("onDetected(): " + eventPayloadToString(event));
+ screenWakeup();
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ screenRelease();
+ }
+ }, 1000L);
}
public void onError() {
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java
index 01c3c50..5c74caf 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java
@@ -184,15 +184,6 @@
}
@Override
- public InputBindResult startInput(
- /* @InputMethodClient.StartInputReason */ int startInputReason,
- IInputMethodClient client, IInputContext inputContext, EditorInfo attribute,
- int controlFlags) throws RemoteException {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
public boolean switchToLastInputMethod(IBinder arg0) throws RemoteException {
// TODO Auto-generated method stub
return false;
@@ -228,7 +219,7 @@
}
@Override
- public InputBindResult windowGainedFocus(
+ public InputBindResult startInputOrWindowGainedFocus(
/* @InputMethodClient.StartInputReason */ int startInputReason,
IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode,
int windowFlags, EditorInfo attribute, IInputContext inputContext)
diff --git a/wifi/java/android/net/wifi/WifiScanner.java b/wifi/java/android/net/wifi/WifiScanner.java
index 2373754..69e179d 100644
--- a/wifi/java/android/net/wifi/WifiScanner.java
+++ b/wifi/java/android/net/wifi/WifiScanner.java
@@ -30,7 +30,9 @@
import android.util.Log;
import android.util.SparseArray;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.Preconditions;
import com.android.internal.util.Protocol;
import java.util.List;
@@ -78,6 +80,8 @@
public static final int REASON_INVALID_REQUEST = -3;
/** Invalid request */
public static final int REASON_NOT_AUTHORIZED = -4;
+ /** An outstanding request with the same listener hasn't finished yet. */
+ public static final int REASON_DUPLICATE_REQEUST = -5;
/** @hide */
public static final String GET_AVAILABLE_CHANNELS_EXTRA = "Channels";
@@ -460,8 +464,11 @@
* scans should also not share this object.
*/
public void startBackgroundScan(ScanSettings settings, ScanListener listener) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ int key = addListener(listener);
+ if (key == INVALID_KEY) return;
validateChannel();
- sAsyncChannel.sendMessage(CMD_START_BACKGROUND_SCAN, 0, putListener(listener), settings);
+ sAsyncChannel.sendMessage(CMD_START_BACKGROUND_SCAN, 0, key, settings);
}
/**
* stop an ongoing wifi scan
@@ -469,8 +476,11 @@
* #startBackgroundScan}
*/
public void stopBackgroundScan(ScanListener listener) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ int key = removeListener(listener);
+ if (key == INVALID_KEY) return;
validateChannel();
- sAsyncChannel.sendMessage(CMD_STOP_BACKGROUND_SCAN, 0, removeListener(listener));
+ sAsyncChannel.sendMessage(CMD_STOP_BACKGROUND_SCAN, 0, key);
}
/**
* reports currently available scan results on appropriate listeners
@@ -491,8 +501,11 @@
* scans should also not share this object.
*/
public void startScan(ScanSettings settings, ScanListener listener) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ int key = addListener(listener);
+ if (key == INVALID_KEY) return;
validateChannel();
- sAsyncChannel.sendMessage(CMD_START_SINGLE_SCAN, 0, putListener(listener), settings);
+ sAsyncChannel.sendMessage(CMD_START_SINGLE_SCAN, 0, key, settings);
}
/**
@@ -501,8 +514,11 @@
* @param listener
*/
public void stopScan(ScanListener listener) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ int key = removeListener(listener);
+ if (key == INVALID_KEY) return;
validateChannel();
- sAsyncChannel.sendMessage(CMD_STOP_SINGLE_SCAN, 0, removeListener(listener));
+ sAsyncChannel.sendMessage(CMD_STOP_SINGLE_SCAN, 0, key);
}
/** specifies information about an access point of interest */
@@ -634,8 +650,11 @@
* provided on {@link #stopTrackingWifiChange}
*/
public void startTrackingWifiChange(WifiChangeListener listener) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ int key = addListener(listener);
+ if (key == INVALID_KEY) return;
validateChannel();
- sAsyncChannel.sendMessage(CMD_START_TRACKING_CHANGE, 0, putListener(listener));
+ sAsyncChannel.sendMessage(CMD_START_TRACKING_CHANGE, 0, key);
}
/**
@@ -644,8 +663,10 @@
* #stopTrackingWifiChange}
*/
public void stopTrackingWifiChange(WifiChangeListener listener) {
+ int key = removeListener(listener);
+ if (key == INVALID_KEY) return;
validateChannel();
- sAsyncChannel.sendMessage(CMD_STOP_TRACKING_CHANGE, 0, removeListener(listener));
+ sAsyncChannel.sendMessage(CMD_STOP_TRACKING_CHANGE, 0, key);
}
/** @hide */
@@ -730,11 +751,14 @@
*/
public void startTrackingBssids(BssidInfo[] bssidInfos,
int apLostThreshold, BssidListener listener) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ int key = addListener(listener);
+ if (key == INVALID_KEY) return;
validateChannel();
HotlistSettings settings = new HotlistSettings();
settings.bssidInfos = bssidInfos;
settings.apLostThreshold = apLostThreshold;
- sAsyncChannel.sendMessage(CMD_SET_HOTLIST, 0, putListener(listener), settings);
+ sAsyncChannel.sendMessage(CMD_SET_HOTLIST, 0, key, settings);
}
/**
@@ -742,8 +766,11 @@
* @param listener same object provided in {@link #startTrackingBssids}
*/
public void stopTrackingBssids(BssidListener listener) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ int key = removeListener(listener);
+ if (key == INVALID_KEY) return;
validateChannel();
- sAsyncChannel.sendMessage(CMD_RESET_HOTLIST, 0, removeListener(listener));
+ sAsyncChannel.sendMessage(CMD_RESET_HOTLIST, 0, key);
}
@@ -812,7 +839,7 @@
private static final Object sThreadRefLock = new Object();
private static int sThreadRefCount;
- private static HandlerThread sHandlerThread;
+ private static Handler sInternalHandler;
/**
* Create a new WifiScanner instance.
@@ -824,12 +851,29 @@
* @hide
*/
public WifiScanner(Context context, IWifiScanner service) {
- mContext = context;
- mService = service;
- init();
+ this(context, service, null, true);
}
- private void init() {
+ /**
+ * Create a new WifiScanner instance.
+ *
+ * @param context The application context.
+ * @param service The IWifiScanner Binder interface
+ * @param looper Looper for running WifiScanner operations. If null, a handler thread will be
+ * created for running WifiScanner operations.
+ * @param waitForConnection If true, this will not return until a connection to Wifi Scanner
+ * service is established.
+ * @hide
+ */
+ @VisibleForTesting
+ public WifiScanner(Context context, IWifiScanner service, Looper looper,
+ boolean waitForConnection) {
+ mContext = context;
+ mService = service;
+ init(looper, waitForConnection);
+ }
+
+ private void init(Looper looper, boolean waitForConnection) {
synchronized (sThreadRefLock) {
if (++sThreadRefCount == 1) {
Messenger messenger = null;
@@ -846,17 +890,23 @@
return;
}
- sHandlerThread = new HandlerThread("WifiScanner");
sAsyncChannel = new AsyncChannel();
sConnected = new CountDownLatch(1);
- sHandlerThread.start();
- Handler handler = new ServiceHandler(sHandlerThread.getLooper());
- sAsyncChannel.connect(mContext, handler, messenger);
- try {
- sConnected.await();
- } catch (InterruptedException e) {
- Log.e(TAG, "interrupted wait at init");
+ if (looper == null) {
+ HandlerThread thread = new HandlerThread("WifiScanner");
+ thread.start();
+ sInternalHandler = new ServiceHandler(thread.getLooper());
+ } else {
+ sInternalHandler = new ServiceHandler(looper);
+ }
+ sAsyncChannel.connect(mContext, sInternalHandler, messenger);
+ if (waitForConnection) {
+ try {
+ sConnected.await();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "interrupted wait at init");
+ }
}
}
}
@@ -867,6 +917,30 @@
"No permission to access and change wifi or a bad initialization");
}
+ // Add a listener into listener map. If the listener already exists, return INVALID_KEY and
+ // send an error message to internal handler; Otherwise add the listener to the listener map and
+ // return the key of the listener.
+ private int addListener(ActionListener listener) {
+ synchronized (sListenerMap) {
+ boolean keyExists = (getListenerKey(listener) != INVALID_KEY);
+ // Note we need to put the listener into listener map even if it's a duplicate as the
+ // internal handler will need the key to find the listener. In case of duplicates,
+ // removing duplicate key logic will be handled in internal handler.
+ int key = putListener(listener);
+ if (keyExists) {
+ if (DBG) Log.d(TAG, "listener key already exists");
+ OperationResult operationResult = new OperationResult(REASON_DUPLICATE_REQEUST,
+ "Outstanding request with same key not stopped yet");
+ Message message = Message.obtain(sInternalHandler, CMD_OP_FAILED, 0, key,
+ operationResult);
+ message.sendToTarget();
+ return INVALID_KEY;
+ } else {
+ return key;
+ }
+ }
+ }
+
private static int putListener(Object listener) {
if (listener == null) return INVALID_KEY;
int key;
@@ -910,7 +984,10 @@
private static int removeListener(Object listener) {
int key = getListenerKey(listener);
- if (key == INVALID_KEY) return key;
+ if (key == INVALID_KEY) {
+ Log.e(TAG, "listener cannot be found");
+ return key;
+ }
synchronized (sListenerMapLock) {
sListenerMap.remove(key);
return key;