summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/test-current.txt2
-rw-r--r--core/java/android/app/AppCompatCallbacks.java11
-rw-r--r--core/java/android/app/admin/DevicePolicyManager.java60
-rw-r--r--core/java/android/content/res/AssetManager.java15
-rw-r--r--core/java/android/provider/Settings.java13
-rw-r--r--core/java/android/view/CompositionSamplingListener.java22
-rw-r--r--core/java/android/view/SurfaceView.java11
-rw-r--r--core/java/com/android/internal/compat/ChangeReporter.java102
-rw-r--r--core/java/com/android/internal/infra/ServiceConnector.java5
-rw-r--r--core/java/com/android/internal/os/RuntimeInit.java13
-rw-r--r--core/java/com/android/internal/os/ZygoteInit.java2
-rwxr-xr-xcore/jni/android/graphics/Bitmap.cpp14
-rw-r--r--core/jni/android_util_AssetManager.cpp19
-rw-r--r--core/jni/com_android_internal_os_Zygote.cpp6
-rw-r--r--libs/androidfw/AssetManager2.cpp58
-rw-r--r--libs/androidfw/include/androidfw/AssetManager2.h6
-rw-r--r--libs/androidfw/tests/AssetManager2_test.cpp8
-rw-r--r--media/java/android/media/ExifInterface.java159
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ProtoStore.java174
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTask.java378
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MalformedEncryptedFileException.java24
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MessageDigestMismatchException.java27
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/UnsupportedEncryptedFileException.java28
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ProtoStoreTest.java264
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTaskTest.java583
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java49
-rw-r--r--packages/SettingsLib/res/values/strings.xml4
-rw-r--r--packages/SettingsLib/search/Android.bp12
-rw-r--r--packages/SettingsLib/search/AndroidManifest.xml21
-rw-r--r--packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java4
-rw-r--r--packages/SettingsLib/search/src/com/android/settingslib/search/Indexable.java66
-rw-r--r--packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexableRaw.java64
-rw-r--r--packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexableResources.java2
-rw-r--r--packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java3
-rw-r--r--packages/Shell/src/com/android/shell/BugreportProgressService.java140
-rw-r--r--packages/SystemUI/res-keyguard/values/strings.xml2
-rw-r--r--packages/SystemUI/res/layout/status_bar_expanded_plugin_frame.xml6
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactory.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/SystemUIBinder.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/power/PowerUI.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/RecentsOnboarding.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVPluginManager.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java26
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java9
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/broadcast/UserBroadcastDispatcherTest.kt18
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java17
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java27
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java24
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java22
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java32
-rw-r--r--services/core/java/com/android/server/TelephonyRegistry.java2
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java2
-rw-r--r--services/core/java/com/android/server/compat/PlatformCompat.java9
-rw-r--r--services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java20
-rw-r--r--services/core/java/com/android/server/media/MediaSessionRecord.java1
-rw-r--r--services/core/java/com/android/server/tv/TvRemoteProviderProxy.java431
-rw-r--r--services/core/java/com/android/server/tv/TvRemoteProviderWatcher.java10
-rw-r--r--services/core/java/com/android/server/tv/TvRemoteService.java234
-rw-r--r--services/core/java/com/android/server/tv/TvRemoteServiceInput.java244
-rw-r--r--services/core/jni/com_android_server_VibratorService.cpp70
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java47
-rw-r--r--services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java39
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java3
-rw-r--r--tests/FlickerTests/AndroidManifest.xml2
-rw-r--r--wifi/java/android/net/wifi/WifiScanner.java69
-rw-r--r--wifi/tests/src/android/net/wifi/WifiScannerTest.java34
81 files changed, 3046 insertions, 894 deletions
diff --git a/api/test-current.txt b/api/test-current.txt
index 75d80bd9eeaf..9b00a42d27a9 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -786,7 +786,7 @@ package android.content.res {
public final class AssetManager implements java.lang.AutoCloseable {
method @NonNull public String[] getApkPaths();
- method @Nullable public java.util.Map<java.lang.String,java.lang.String> getOverlayableMap(String);
+ method @Nullable public String getOverlayablesToString(String);
}
public final class Configuration implements java.lang.Comparable<android.content.res.Configuration> android.os.Parcelable {
diff --git a/core/java/android/app/AppCompatCallbacks.java b/core/java/android/app/AppCompatCallbacks.java
index 08c97eb284e3..19d158dedd06 100644
--- a/core/java/android/app/AppCompatCallbacks.java
+++ b/core/java/android/app/AppCompatCallbacks.java
@@ -18,7 +18,6 @@ package android.app;
import android.compat.Compatibility;
import android.os.Process;
-import android.util.Log;
import android.util.StatsLog;
import com.android.internal.compat.ChangeReporter;
@@ -31,8 +30,6 @@ import java.util.Arrays;
* @hide
*/
public final class AppCompatCallbacks extends Compatibility.Callbacks {
- private static final String TAG = "Compatibility";
-
private final long[] mDisabledChanges;
private final ChangeReporter mChangeReporter;
@@ -48,7 +45,8 @@ public final class AppCompatCallbacks extends Compatibility.Callbacks {
private AppCompatCallbacks(long[] disabledChanges) {
mDisabledChanges = Arrays.copyOf(disabledChanges, disabledChanges.length);
Arrays.sort(mDisabledChanges);
- mChangeReporter = new ChangeReporter();
+ mChangeReporter = new ChangeReporter(
+ StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__SOURCE__APP_PROCESS);
}
protected void reportChange(long changeId) {
@@ -67,10 +65,7 @@ public final class AppCompatCallbacks extends Compatibility.Callbacks {
private void reportChange(long changeId, int state) {
int uid = Process.myUid();
- //TODO(b/138374585): Implement rate limiting for the logs.
- Log.d(TAG, ChangeReporter.createLogString(uid, changeId, state));
- mChangeReporter.reportChange(uid, changeId,
- state, /* source */StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__SOURCE__APP_PROCESS);
+ mChangeReporter.reportChange(uid, changeId, state);
}
}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index ae872168e425..c3c383ce5e55 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -2678,7 +2678,10 @@ public class DevicePolicyManager {
* only imposed if the administrator has also requested either {@link #PASSWORD_QUALITY_NUMERIC}
* , {@link #PASSWORD_QUALITY_NUMERIC_COMPLEX}, {@link #PASSWORD_QUALITY_ALPHABETIC},
* {@link #PASSWORD_QUALITY_ALPHANUMERIC}, or {@link #PASSWORD_QUALITY_COMPLEX} with
- * {@link #setPasswordQuality}.
+ * {@link #setPasswordQuality}. If an app targeting SDK level
+ * {@link android.os.Build.VERSION_CODES#R} and above enforces this constraint without settings
+ * password quality to one of these values first, this method will throw
+ * {@link IllegalStateException}.
* <p>
* On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
* password is always treated as empty.
@@ -2693,9 +2696,12 @@ public class DevicePolicyManager {
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param length The new desired minimum password length. A value of 0 means there is no
- * restriction.
+ * restriction.
* @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
- * does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ * does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ * @throws IllegalStateException if the calling app is targeting SDK level
+ * {@link android.os.Build.VERSION_CODES#R} and above and didn't set a sufficient password
+ * quality requirement prior to calling this method.
*/
public void setPasswordMinimumLength(@NonNull ComponentName admin, int length) {
if (mService != null) {
@@ -2747,7 +2753,10 @@ public class DevicePolicyManager {
* place immediately. To prompt the user for a new password, use
* {@link #ACTION_SET_NEW_PASSWORD} or {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after
* setting this value. This constraint is only imposed if the administrator has also requested
- * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The default value is 0.
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. If an app targeting
+ * SDK level {@link android.os.Build.VERSION_CODES#R} and above enforces this constraint without
+ * settings password quality to {@link #PASSWORD_QUALITY_COMPLEX} first, this method will throw
+ * {@link IllegalStateException}. The default value is 0.
* <p>
* On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
* password is always treated as empty.
@@ -2765,6 +2774,9 @@ public class DevicePolicyManager {
* A value of 0 means there is no restriction.
* @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
* does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ * @throws IllegalStateException if the calling app is targeting SDK level
+ * {@link android.os.Build.VERSION_CODES#R} and above and didn't set a sufficient password
+ * quality requirement prior to calling this method.
*/
public void setPasswordMinimumUpperCase(@NonNull ComponentName admin, int length) {
if (mService != null) {
@@ -2823,7 +2835,10 @@ public class DevicePolicyManager {
* place immediately. To prompt the user for a new password, use
* {@link #ACTION_SET_NEW_PASSWORD} or {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after
* setting this value. This constraint is only imposed if the administrator has also requested
- * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The default value is 0.
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. If an app targeting
+ * SDK level {@link android.os.Build.VERSION_CODES#R} and above enforces this constraint without
+ * settings password quality to {@link #PASSWORD_QUALITY_COMPLEX} first, this method will throw
+ * {@link IllegalStateException}. The default value is 0.
* <p>
* On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
* password is always treated as empty.
@@ -2841,6 +2856,9 @@ public class DevicePolicyManager {
* A value of 0 means there is no restriction.
* @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
* does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ * @throws IllegalStateException if the calling app is targeting SDK level
+ * {@link android.os.Build.VERSION_CODES#R} and above and didn't set a sufficient password
+ * quality requirement prior to calling this method.
*/
public void setPasswordMinimumLowerCase(@NonNull ComponentName admin, int length) {
if (mService != null) {
@@ -2899,7 +2917,10 @@ public class DevicePolicyManager {
* immediately. To prompt the user for a new password, use {@link #ACTION_SET_NEW_PASSWORD} or
* {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after setting this value. This constraint is
* only imposed if the administrator has also requested {@link #PASSWORD_QUALITY_COMPLEX} with
- * {@link #setPasswordQuality}. The default value is 1.
+ * {@link #setPasswordQuality}. If an app targeting SDK level
+ * {@link android.os.Build.VERSION_CODES#R} and above enforces this constraint without settings
+ * password quality to {@link #PASSWORD_QUALITY_COMPLEX} first, this method will throw
+ * {@link IllegalStateException}. The default value is 1.
* <p>
* On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
* password is always treated as empty.
@@ -2917,6 +2938,9 @@ public class DevicePolicyManager {
* 0 means there is no restriction.
* @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
* does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ * @throws IllegalStateException if the calling app is targeting SDK level
+ * {@link android.os.Build.VERSION_CODES#R} and above and didn't set a sufficient password
+ * quality requirement prior to calling this method.
*/
public void setPasswordMinimumLetters(@NonNull ComponentName admin, int length) {
if (mService != null) {
@@ -2974,7 +2998,10 @@ public class DevicePolicyManager {
* place immediately. To prompt the user for a new password, use
* {@link #ACTION_SET_NEW_PASSWORD} or {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after
* setting this value. This constraint is only imposed if the administrator has also requested
- * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The default value is 1.
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. If an app targeting
+ * SDK level {@link android.os.Build.VERSION_CODES#R} and above enforces this constraint without
+ * settings password quality to {@link #PASSWORD_QUALITY_COMPLEX} first, this method will throw
+ * {@link IllegalStateException}. The default value is 1.
* <p>
* On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
* password is always treated as empty.
@@ -2992,6 +3019,9 @@ public class DevicePolicyManager {
* value of 0 means there is no restriction.
* @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
* does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ * @throws IllegalStateException if the calling app is targeting SDK level
+ * {@link android.os.Build.VERSION_CODES#R} and above and didn't set a sufficient password
+ * quality requirement prior to calling this method.
*/
public void setPasswordMinimumNumeric(@NonNull ComponentName admin, int length) {
if (mService != null) {
@@ -3049,7 +3079,10 @@ public class DevicePolicyManager {
* immediately. To prompt the user for a new password, use {@link #ACTION_SET_NEW_PASSWORD} or
* {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after setting this value. This constraint is
* only imposed if the administrator has also requested {@link #PASSWORD_QUALITY_COMPLEX} with
- * {@link #setPasswordQuality}. The default value is 1.
+ * {@link #setPasswordQuality}. If an app targeting SDK level
+ * {@link android.os.Build.VERSION_CODES#R} and above enforces this constraint without settings
+ * password quality to {@link #PASSWORD_QUALITY_COMPLEX} first, this method will throw
+ * {@link IllegalStateException}. The default value is 1.
* <p>
* On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
* password is always treated as empty.
@@ -3067,6 +3100,9 @@ public class DevicePolicyManager {
* 0 means there is no restriction.
* @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
* does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ * @throws IllegalStateException if the calling app is targeting SDK level
+ * {@link android.os.Build.VERSION_CODES#R} and above and didn't set a sufficient password
+ * quality requirement prior to calling this method.
*/
public void setPasswordMinimumSymbols(@NonNull ComponentName admin, int length) {
if (mService != null) {
@@ -3123,7 +3159,10 @@ public class DevicePolicyManager {
* one, so the change does not take place immediately. To prompt the user for a new password,
* use {@link #ACTION_SET_NEW_PASSWORD} or {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after
* setting this value. This constraint is only imposed if the administrator has also requested
- * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The default value is 0.
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. If an app targeting
+ * SDK level {@link android.os.Build.VERSION_CODES#R} and above enforces this constraint without
+ * settings password quality to {@link #PASSWORD_QUALITY_COMPLEX} first, this method will throw
+ * {@link IllegalStateException}. The default value is 0.
* <p>
* On devices not supporting {@link PackageManager#FEATURE_SECURE_LOCK_SCREEN} feature, the
* password is always treated as empty.
@@ -3141,6 +3180,9 @@ public class DevicePolicyManager {
* 0 means there is no restriction.
* @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
* does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ * @throws IllegalStateException if the calling app is targeting SDK level
+ * {@link android.os.Build.VERSION_CODES#R} and above and didn't set a sufficient password
+ * quality requirement prior to calling this method.
*/
public void setPasswordMinimumNonLetter(@NonNull ComponentName admin, int length) {
if (mService != null) {
diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java
index 2420a6109155..567e26b4c2f6 100644
--- a/core/java/android/content/res/AssetManager.java
+++ b/core/java/android/content/res/AssetManager.java
@@ -1376,7 +1376,6 @@ public final class AssetManager implements AutoCloseable {
/**
* @hide
*/
- @TestApi
@GuardedBy("this")
public @Nullable Map<String, String> getOverlayableMap(String packageName) {
synchronized (this) {
@@ -1385,6 +1384,18 @@ public final class AssetManager implements AutoCloseable {
}
}
+ /**
+ * @hide
+ */
+ @TestApi
+ @GuardedBy("this")
+ public @Nullable String getOverlayablesToString(String packageName) {
+ synchronized (this) {
+ ensureValidLocked();
+ return nativeGetOverlayablesToString(mObject, packageName);
+ }
+ }
+
@GuardedBy("this")
private void incRefsLocked(long id) {
if (DEBUG_REFS) {
@@ -1504,6 +1515,8 @@ public final class AssetManager implements AutoCloseable {
private static native String[] nativeCreateIdmapsForStaticOverlaysTargetingAndroid();
private static native @Nullable Map nativeGetOverlayableMap(long ptr,
@NonNull String packageName);
+ private static native @Nullable String nativeGetOverlayablesToString(long ptr,
+ @NonNull String packageName);
// Global debug native methods.
/**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 61d8eb13bcf2..3c26df3c560b 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7631,6 +7631,19 @@ public final class Settings {
"face_unlock_always_require_confirmation";
/**
+ * Whether or not a user should re enroll their face.
+ *
+ * Face unlock re enroll.
+ * 0 = No re enrollment.
+ * 1 = Re enrollment is suggested.
+ * 2 = Re enrollment is required after a set time period.
+ * 3 = Re enrollment is required immediately.
+ *
+ * @hide
+ */
+ public static final String FACE_UNLOCK_RE_ENROLL = "face_unlock_re_enroll";
+
+ /**
* Whether or not debugging is enabled.
* @hide
*/
diff --git a/core/java/android/view/CompositionSamplingListener.java b/core/java/android/view/CompositionSamplingListener.java
index 368445cde72c..677a559cd3b0 100644
--- a/core/java/android/view/CompositionSamplingListener.java
+++ b/core/java/android/view/CompositionSamplingListener.java
@@ -28,7 +28,7 @@ import java.util.concurrent.Executor;
*/
public abstract class CompositionSamplingListener {
- private final long mNativeListener;
+ private long mNativeListener;
private final Executor mExecutor;
public CompositionSamplingListener(Executor executor) {
@@ -36,13 +36,19 @@ public abstract class CompositionSamplingListener {
mNativeListener = nativeCreate(this);
}
+ public void destroy() {
+ if (mNativeListener == 0) {
+ return;
+ }
+ unregister(this);
+ nativeDestroy(mNativeListener);
+ mNativeListener = 0;
+ }
+
@Override
protected void finalize() throws Throwable {
try {
- if (mNativeListener != 0) {
- unregister(this);
- nativeDestroy(mNativeListener);
- }
+ destroy();
} finally {
super.finalize();
}
@@ -58,6 +64,9 @@ public abstract class CompositionSamplingListener {
*/
public static void register(CompositionSamplingListener listener,
int displayId, SurfaceControl stopLayer, Rect samplingArea) {
+ if (listener.mNativeListener == 0) {
+ return;
+ }
Preconditions.checkArgument(displayId == Display.DEFAULT_DISPLAY,
"default display only for now");
long nativeStopLayerObject = stopLayer != null ? stopLayer.mNativeObject : 0;
@@ -69,6 +78,9 @@ public abstract class CompositionSamplingListener {
* Unregisters a sampling listener.
*/
public static void unregister(CompositionSamplingListener listener) {
+ if (listener.mNativeListener == 0) {
+ return;
+ }
nativeUnregister(listener.mNativeListener);
}
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index db3ef20d5859..06ff568202d5 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -1128,11 +1128,12 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
return;
}
- if (frameNumber > 0) {
- final ViewRootImpl viewRoot = getViewRootImpl();
-
- mRtTransaction.deferTransactionUntilSurface(mSurfaceControl, viewRoot.mSurface,
- frameNumber);
+ final ViewRootImpl viewRoot = getViewRootImpl();
+ if (frameNumber > 0 && viewRoot != null) {
+ if (viewRoot.mSurface.isValid()) {
+ mRtTransaction.deferTransactionUntilSurface(mSurfaceControl, viewRoot.mSurface,
+ frameNumber);
+ }
}
mRtTransaction.hide(mSurfaceControl);
diff --git a/core/java/com/android/internal/compat/ChangeReporter.java b/core/java/com/android/internal/compat/ChangeReporter.java
index 1ce071bd005a..5ea970d4c746 100644
--- a/core/java/com/android/internal/compat/ChangeReporter.java
+++ b/core/java/com/android/internal/compat/ChangeReporter.java
@@ -16,14 +16,89 @@
package com.android.internal.compat;
+import android.util.Log;
+import android.util.Slog;
import android.util.StatsLog;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
/**
* A helper class to report changes to stats log.
*
* @hide
*/
public final class ChangeReporter {
+ private static final String TAG = "CompatibilityChangeReporter";
+ private int mSource;
+
+ private final class ChangeReport {
+ int mUid;
+ long mChangeId;
+ int mState;
+
+ ChangeReport(int uid, long changeId, int state) {
+ mUid = uid;
+ mChangeId = changeId;
+ mState = state;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ChangeReport that = (ChangeReport) o;
+ return mUid == that.mUid
+ && mChangeId == that.mChangeId
+ && mState == that.mState;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mUid, mChangeId, mState);
+ }
+ }
+
+ @GuardedBy("mReportedChanges")
+ private Set<ChangeReport> mReportedChanges = new HashSet<>();
+
+ public ChangeReporter(int source) {
+ mSource = source;
+ }
+
+ /**
+ * Report the change to stats log.
+ *
+ * @param uid affected by the change
+ * @param changeId the reported change id
+ * @param state of the reported change - enabled/disabled/only logged
+ */
+ public void reportChange(int uid, long changeId, int state) {
+ debugLog(uid, changeId, state);
+ ChangeReport report = new ChangeReport(uid, changeId, state);
+ synchronized (mReportedChanges) {
+ if (!mReportedChanges.contains(report)) {
+ StatsLog.write(StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED, uid, changeId,
+ state, mSource);
+ mReportedChanges.add(report);
+ }
+ }
+ }
+
+ private void debugLog(int uid, long changeId, int state) {
+ //TODO(b/138374585): Implement rate limiting for the logs.
+ String message = String.format("Compat change id reported: %d; UID %d; state: %s", changeId,
+ uid, stateToString(state));
+ if (mSource == StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__SOURCE__SYSTEM_SERVER) {
+ Slog.d(TAG, message);
+ } else {
+ Log.d(TAG, message);
+ }
+
+ }
/**
* Transforms StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__STATE enum to a string.
@@ -43,31 +118,4 @@ public final class ChangeReporter {
return "UNKNOWN";
}
}
-
- /**
- * Constructs and returns a string to be logged to logcat when a change is reported.
- *
- * @param uid affected by the change
- * @param changeId the reported change id
- * @param state of the reported change - enabled/disabled/only logged
- * @return string to log
- */
- public static String createLogString(int uid, long changeId, int state) {
- return String.format("Compat change id reported: %d; UID %d; state: %s", changeId, uid,
- stateToString(state));
- }
-
- /**
- * Report the change to stats log.
- *
- * @param uid affected by the change
- * @param changeId the reported change id
- * @param state of the reported change - enabled/disabled/only logged
- * @param source of the logging - app process or system server
- */
- public void reportChange(int uid, long changeId, int state, int source) {
- //TODO(b/138374585): Implement rate limiting for stats log.
- StatsLog.write(StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED, uid, changeId,
- state, source);
- }
}
diff --git a/core/java/com/android/internal/infra/ServiceConnector.java b/core/java/com/android/internal/infra/ServiceConnector.java
index d6862f0188ce..98d679eb776b 100644
--- a/core/java/com/android/internal/infra/ServiceConnector.java
+++ b/core/java/com/android/internal/infra/ServiceConnector.java
@@ -32,6 +32,7 @@ import android.text.TextUtils;
import android.util.DebugUtils;
import android.util.Log;
+import com.android.internal.util.Preconditions;
import com.android.internal.util.function.pooled.PooledLambda;
import java.io.PrintWriter;
@@ -351,7 +352,7 @@ public interface ServiceConnector<I extends IInterface> {
@Override
public <R> CompletionAwareJob<I, R> postForResult(@NonNull Job<I, R> job) {
CompletionAwareJob<I, R> task = new CompletionAwareJob<>();
- task.mDelegate = job;
+ task.mDelegate = Preconditions.checkNotNull(job);
enqueue(task);
return task;
}
@@ -359,7 +360,7 @@ public interface ServiceConnector<I extends IInterface> {
@Override
public <R> AndroidFuture<R> postAsync(@NonNull Job<I, CompletableFuture<R>> job) {
CompletionAwareJob<I, R> task = new CompletionAwareJob<>();
- task.mDelegate = (Job) job;
+ task.mDelegate = Preconditions.checkNotNull((Job) job);
task.mAsync = true;
enqueue(task);
return task;
diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java
index 1de2e7272f4d..d6caa0930243 100644
--- a/core/java/com/android/internal/os/RuntimeInit.java
+++ b/core/java/com/android/internal/os/RuntimeInit.java
@@ -192,6 +192,15 @@ public class RuntimeInit {
}
}
+ /**
+ * Common initialization that (unlike {@link #commonInit()} should happen prior to
+ * the Zygote fork.
+ */
+ public static void preForkInit() {
+ if (DEBUG) Slog.d(TAG, "Entered preForkInit.");
+ RuntimeInit.enableDdms();
+ }
+
@UnsupportedAppUsage
protected static final void commonInit() {
if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");
@@ -324,7 +333,7 @@ public class RuntimeInit {
@UnsupportedAppUsage
public static final void main(String[] argv) {
- enableDdms();
+ preForkInit();
if (argv.length == 2 && argv[1].equals("application")) {
if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application");
redirectLogStreams();
@@ -418,7 +427,7 @@ public class RuntimeInit {
/**
* Enable DDMS.
*/
- static final void enableDdms() {
+ private static void enableDdms() {
// Register handlers for DDM messages.
android.ddm.DdmRegister.registerHandlers();
}
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index 3be1a1aefe57..158700b2a449 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -847,7 +847,7 @@ public class ZygoteInit {
TimingsTraceLog bootTimingsTraceLog = new TimingsTraceLog(bootTimeTag,
Trace.TRACE_TAG_DALVIK);
bootTimingsTraceLog.traceBegin("ZygoteInit");
- RuntimeInit.enableDdms();
+ RuntimeInit.preForkInit();
boolean startSystemServer = false;
String zygoteSocketName = "zygote";
diff --git a/core/jni/android/graphics/Bitmap.cpp b/core/jni/android/graphics/Bitmap.cpp
index 18a1b43d3f5f..89c12f88594d 100755
--- a/core/jni/android/graphics/Bitmap.cpp
+++ b/core/jni/android/graphics/Bitmap.cpp
@@ -265,6 +265,20 @@ void imageInfo(JNIEnv* env, jobject bitmap, AndroidBitmapInfo* info) {
info->format = ANDROID_BITMAP_FORMAT_NONE;
break;
}
+ switch (imageInfo.alphaType()) {
+ case kUnknown_SkAlphaType:
+ LOG_ALWAYS_FATAL("Bitmap has no alpha type");
+ break;
+ case kOpaque_SkAlphaType:
+ info->flags |= ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE;
+ break;
+ case kPremul_SkAlphaType:
+ info->flags |= ANDROID_BITMAP_FLAGS_ALPHA_PREMUL;
+ break;
+ case kUnpremul_SkAlphaType:
+ info->flags |= ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL;
+ break;
+ }
}
void* lockPixels(JNIEnv* env, jobject bitmap) {
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index bf4ffc7e42e0..daf33f61105c 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -352,7 +352,7 @@ static Guarded<AssetManager2>& AssetManagerFromLong(jlong ptr) {
}
static jobject NativeGetOverlayableMap(JNIEnv* env, jclass /*clazz*/, jlong ptr,
- jstring package_name) {
+ jstring package_name) {
ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
const ScopedUtfChars package_name_utf8(env, package_name);
CHECK(package_name_utf8.c_str() != nullptr);
@@ -397,6 +397,21 @@ static jobject NativeGetOverlayableMap(JNIEnv* env, jclass /*clazz*/, jlong ptr,
return array_map;
}
+static jstring NativeGetOverlayablesToString(JNIEnv* env, jclass /*clazz*/, jlong ptr,
+ jstring package_name) {
+ ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
+ const ScopedUtfChars package_name_utf8(env, package_name);
+ CHECK(package_name_utf8.c_str() != nullptr);
+ const std::string std_package_name(package_name_utf8.c_str());
+
+ std::string result;
+ if (!assetmanager->GetOverlayablesToString(std_package_name, &result)) {
+ return nullptr;
+ }
+
+ return env->NewStringUTF(result.c_str());
+}
+
#ifdef __ANDROID__ // Layoutlib does not support parcel
static jobject ReturnParcelFileDescriptor(JNIEnv* env, std::unique_ptr<Asset> asset,
jlongArray out_offsets) {
@@ -1608,6 +1623,8 @@ static const JNINativeMethod gAssetManagerMethods[] = {
(void*)NativeCreateIdmapsForStaticOverlaysTargetingAndroid},
{"nativeGetOverlayableMap", "(JLjava/lang/String;)Ljava/util/Map;",
(void*)NativeGetOverlayableMap},
+ {"nativeGetOverlayablesToString", "(JLjava/lang/String;)Ljava/lang/String;",
+ (void*)NativeGetOverlayablesToString},
// Global management/debug methods.
{"getGlobalAssetCount", "()I", (void*)NativeGetGlobalAssetCount},
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 93ef75148df7..3516dce5d5ed 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -74,7 +74,6 @@
#include <android-base/strings.h>
#include <android-base/unique_fd.h>
#include <bionic/malloc.h>
-#include <cutils/ashmem.h>
#include <cutils/fs.h>
#include <cutils/multiuser.h>
#include <cutils/sockets.h>
@@ -1657,11 +1656,6 @@ static void com_android_internal_os_Zygote_nativeInitNativeState(JNIEnv* env, jc
if (!SetTaskProfiles(0, {})) {
ZygoteFailure(env, "zygote", nullptr, "Zygote SetTaskProfiles failed");
}
-
- /*
- * ashmem initialization to avoid dlopen overhead
- */
- ashmem_init();
}
/**
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index 01caf011f644..eec49df79630 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -224,6 +224,62 @@ const std::unordered_map<std::string, std::string>*
return &loaded_package->GetOverlayableMap();
}
+bool AssetManager2::GetOverlayablesToString(const android::StringPiece& package_name,
+ std::string* out) const {
+ uint8_t package_id = 0U;
+ for (const auto& apk_assets : apk_assets_) {
+ const LoadedArsc* loaded_arsc = apk_assets->GetLoadedArsc();
+ if (loaded_arsc == nullptr) {
+ continue;
+ }
+
+ const auto& loaded_packages = loaded_arsc->GetPackages();
+ if (loaded_packages.empty()) {
+ continue;
+ }
+
+ const auto& loaded_package = loaded_packages[0];
+ if (loaded_package->GetPackageName() == package_name) {
+ package_id = GetAssignedPackageId(loaded_package.get());
+ break;
+ }
+ }
+
+ if (package_id == 0U) {
+ ANDROID_LOG(ERROR) << base::StringPrintf("No package with name '%s", package_name.data());
+ return false;
+ }
+
+ const size_t idx = package_ids_[package_id];
+ if (idx == 0xff) {
+ return false;
+ }
+
+ std::string output;
+ for (const ConfiguredPackage& package : package_groups_[idx].packages_) {
+ const LoadedPackage* loaded_package = package.loaded_package_;
+ for (auto it = loaded_package->begin(); it != loaded_package->end(); it++) {
+ const OverlayableInfo* info = loaded_package->GetOverlayableInfo(*it);
+ if (info != nullptr) {
+ ResourceName res_name;
+ if (!GetResourceName(*it, &res_name)) {
+ ANDROID_LOG(ERROR) << base::StringPrintf(
+ "Unable to retrieve name of overlayable resource 0x%08x", *it);
+ return false;
+ }
+
+ const std::string name = ToFormattedResourceString(&res_name);
+ output.append(base::StringPrintf(
+ "resource='%s' overlayable='%s' actor='%s' policy='0x%08x'\n",
+ name.c_str(), info->name.c_str(), info->actor.c_str(), info->policy_flags));
+ }
+ }
+ }
+
+ *out = std::move(output);
+ return true;
+}
+
void AssetManager2::SetConfiguration(const ResTable_config& configuration) {
const int diff = configuration_.diff(configuration);
configuration_ = configuration;
@@ -1073,7 +1129,7 @@ void AssetManager2::InvalidateCaches(uint32_t diff) {
}
}
-uint8_t AssetManager2::GetAssignedPackageId(const LoadedPackage* package) {
+uint8_t AssetManager2::GetAssignedPackageId(const LoadedPackage* package) const {
for (auto& package_group : package_groups_) {
for (auto& package2 : package_group.packages_) {
if (package2.loaded_package_ == package) {
diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h
index 1e2b36cb1703..de46081a6aa3 100644
--- a/libs/androidfw/include/androidfw/AssetManager2.h
+++ b/libs/androidfw/include/androidfw/AssetManager2.h
@@ -124,6 +124,10 @@ class AssetManager2 {
// This may be nullptr if the APK represented by `cookie` has no resource table.
const DynamicRefTable* GetDynamicRefTableForCookie(ApkAssetsCookie cookie) const;
+ // Returns a string representation of the overlayable API of a package.
+ bool GetOverlayablesToString(const android::StringPiece& package_name,
+ std::string* out) const;
+
const std::unordered_map<std::string, std::string>*
GetOverlayableMapForPackage(uint32_t package_id) const;
@@ -308,7 +312,7 @@ class AssetManager2 {
const ResolvedBag* GetBag(uint32_t resid, std::vector<uint32_t>& child_resids);
// Retrieve the assigned package id of the package if loaded into this AssetManager
- uint8_t GetAssignedPackageId(const LoadedPackage* package);
+ uint8_t GetAssignedPackageId(const LoadedPackage* package) const;
// The ordered list of ApkAssets to search. These are not owned by the AssetManager, and must
// have a longer lifetime.
diff --git a/libs/androidfw/tests/AssetManager2_test.cpp b/libs/androidfw/tests/AssetManager2_test.cpp
index 40c8e46e4d84..15910241518d 100644
--- a/libs/androidfw/tests/AssetManager2_test.cpp
+++ b/libs/androidfw/tests/AssetManager2_test.cpp
@@ -707,7 +707,7 @@ TEST_F(AssetManager2Test, GetLastPathAfterDisablingReturnsEmpty) {
EXPECT_EQ("", resultDisabled);
}
-TEST_F(AssetManager2Test, GetOverlayableMap) {
+TEST_F(AssetManager2Test, GetOverlayablesToString) {
ResTable_config desired_config;
memset(&desired_config, 0, sizeof(desired_config));
@@ -721,6 +721,12 @@ TEST_F(AssetManager2Test, GetOverlayableMap) {
ASSERT_EQ(2, map->size());
ASSERT_EQ(map->at("OverlayableResources1"), "overlay://theme");
ASSERT_EQ(map->at("OverlayableResources2"), "overlay://com.android.overlayable");
+
+ std::string api;
+ ASSERT_TRUE(assetmanager.GetOverlayablesToString("com.android.overlayable", &api));
+ ASSERT_EQ(api.find("not_overlayable"), std::string::npos);
+ ASSERT_NE(api.find("resource='com.android.overlayable:string/overlayable2' overlayable='OverlayableResources1' actor='overlay://theme' policy='0x0000000a'\n"),
+ std::string::npos);
}
} // namespace android
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 5b535651abd9..53babcb32e4d 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -494,6 +494,19 @@ public class ExifInterface {
// See http://www.exiv2.org/makernote.html#R11
private static final int PEF_MAKER_NOTE_SKIP_SIZE = 6;
+ // See PNG (Portable Network Graphics) Specification, Version 1.2,
+ // 3.1. PNG file signature
+ private static final byte[] PNG_SIGNATURE = new byte[] {(byte) 0x89, (byte) 0x50, (byte) 0x4e,
+ (byte) 0x47, (byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a};
+ // See PNG (Portable Network Graphics) Specification, Version 1.2,
+ // 3.7. eXIf Exchangeable Image File (Exif) Profile
+ private static final byte[] PNG_CHUNK_TYPE_EXIF = new byte[]{(byte) 0x65, (byte) 0x58,
+ (byte) 0x49, (byte) 0x66};
+ private static final byte[] PNG_CHUNK_TYPE_IEND = new byte[]{(byte) 0x49, (byte) 0x45,
+ (byte) 0x4e, (byte) 0x44};
+ private static final int PNG_CHUNK_LENGTH_BYTE_LENGTH = 4;
+ private static final int PNG_CHUNK_CRC_BYTE_LENGTH = 4;
+
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private static SimpleDateFormat sFormatter;
private static SimpleDateFormat sFormatterTz;
@@ -1311,6 +1324,7 @@ public class ExifInterface {
private static final int IMAGE_TYPE_RW2 = 10;
private static final int IMAGE_TYPE_SRW = 11;
private static final int IMAGE_TYPE_HEIF = 12;
+ private static final int IMAGE_TYPE_PNG = 13;
static {
sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
@@ -1811,6 +1825,10 @@ public class ExifInterface {
getRw2Attributes(inputStream);
break;
}
+ case IMAGE_TYPE_PNG: {
+ getPngAttributes(inputStream);
+ break;
+ }
case IMAGE_TYPE_ARW:
case IMAGE_TYPE_CR2:
case IMAGE_TYPE_DNG:
@@ -2024,6 +2042,7 @@ public class ExifInterface {
if (in.skip(mThumbnailOffset) != mThumbnailOffset) {
throw new IOException("Corrupted image");
}
+ // TODO: Need to handle potential OutOfMemoryError
byte[] buffer = new byte[mThumbnailLength];
if (in.read(buffer) != mThumbnailLength) {
throw new IOException("Corrupted image");
@@ -2363,6 +2382,8 @@ public class ExifInterface {
return IMAGE_TYPE_ORF;
} else if (isRw2Format(signatureCheckBytes)) {
return IMAGE_TYPE_RW2;
+ } else if (isPngFormat(signatureCheckBytes)) {
+ return IMAGE_TYPE_PNG;
}
// Certain file formats (PEF) are identified in readImageFileDirectory()
return IMAGE_TYPE_UNKNOWN;
@@ -2478,16 +2499,24 @@ public class ExifInterface {
* http://fileformats.archiveteam.org/wiki/Olympus_ORF
*/
private boolean isOrfFormat(byte[] signatureCheckBytes) throws IOException {
- ByteOrderedDataInputStream signatureInputStream =
- new ByteOrderedDataInputStream(signatureCheckBytes);
- // Read byte order
- mExifByteOrder = readByteOrder(signatureInputStream);
- // Set byte order
- signatureInputStream.setByteOrder(mExifByteOrder);
+ ByteOrderedDataInputStream signatureInputStream = null;
- short orfSignature = signatureInputStream.readShort();
- if (orfSignature == ORF_SIGNATURE_1 || orfSignature == ORF_SIGNATURE_2) {
- return true;
+ try {
+ signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes);
+
+ // Read byte order
+ mExifByteOrder = readByteOrder(signatureInputStream);
+ // Set byte order
+ signatureInputStream.setByteOrder(mExifByteOrder);
+
+ short orfSignature = signatureInputStream.readShort();
+ return orfSignature == ORF_SIGNATURE_1 || orfSignature == ORF_SIGNATURE_2;
+ } catch (Exception e) {
+ // Do nothing
+ } finally {
+ if (signatureInputStream != null) {
+ signatureInputStream.close();
+ }
}
return false;
}
@@ -2497,21 +2526,43 @@ public class ExifInterface {
* See http://lclevy.free.fr/raw/
*/
private boolean isRw2Format(byte[] signatureCheckBytes) throws IOException {
- ByteOrderedDataInputStream signatureInputStream =
- new ByteOrderedDataInputStream(signatureCheckBytes);
- // Read byte order
- mExifByteOrder = readByteOrder(signatureInputStream);
- // Set byte order
- signatureInputStream.setByteOrder(mExifByteOrder);
+ ByteOrderedDataInputStream signatureInputStream = null;
- short signatureByte = signatureInputStream.readShort();
- if (signatureByte == RW2_SIGNATURE) {
- return true;
+ try {
+ signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes);
+
+ // Read byte order
+ mExifByteOrder = readByteOrder(signatureInputStream);
+ // Set byte order
+ signatureInputStream.setByteOrder(mExifByteOrder);
+
+ short signatureByte = signatureInputStream.readShort();
+ signatureInputStream.close();
+ return signatureByte == RW2_SIGNATURE;
+ } catch (Exception e) {
+ // Do nothing
+ } finally {
+ if (signatureInputStream != null) {
+ signatureInputStream.close();
+ }
}
return false;
}
/**
+ * PNG's file signature is first 8 bytes.
+ * See PNG (Portable Network Graphics) Specification, Version 1.2, 3.1. PNG file signature
+ */
+ private boolean isPngFormat(byte[] signatureCheckBytes) throws IOException {
+ for (int i = 0; i < PNG_SIGNATURE.length; i++) {
+ if (signatureCheckBytes[i] != PNG_SIGNATURE[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
* Loads EXIF attributes from a JPEG input stream.
*
* @param in The input stream that starts with the JPEG data.
@@ -2585,7 +2636,7 @@ public class ExifInterface {
readExifSegment(value, imageType);
- // Save offset values for createJpegThumbnailBitmap() function
+ // Save offset values for handleThumbnailFromJfif() function
mExifOffset = (int) offset;
} else if (ArrayUtils.startsWith(bytes, IDENTIFIER_XMP_APP1)) {
// See XMP Specification Part 3: Storage in Files, 1.1.3 JPEG, Table 6
@@ -2886,6 +2937,7 @@ public class ExifInterface {
throw new IOException("Invalid identifier");
}
+ // TODO: Need to handle potential OutOfMemoryError
byte[] bytes = new byte[length];
if (in.read(bytes) != length) {
throw new IOException("Can't read exif");
@@ -3012,6 +3064,64 @@ public class ExifInterface {
}
}
+ // PNG contains the EXIF data as a Special-Purpose Chunk
+ private void getPngAttributes(ByteOrderedDataInputStream in) throws IOException {
+ if (DEBUG) {
+ Log.d(TAG, "getPngAttributes starting with: " + in);
+ }
+
+ // PNG uses Big Endian by default.
+ // See PNG (Portable Network Graphics) Specification, Version 1.2,
+ // 2.1. Integers and byte order
+ in.setByteOrder(ByteOrder.BIG_ENDIAN);
+
+ // Skip the signature bytes
+ in.seek(PNG_SIGNATURE.length);
+
+ try {
+ while (true) {
+ // Each chunk is made up of four parts:
+ // 1) Length: 4-byte unsigned integer indicating the number of bytes in the
+ // Chunk Data field. Excludes Chunk Type and CRC bytes.
+ // 2) Chunk Type: 4-byte chunk type code.
+ // 3) Chunk Data: The data bytes. Can be zero-length.
+ // 4) CRC: 4-byte data calculated on the preceding bytes in the chunk. Always
+ // present.
+ // --> 4 (length bytes) + 4 (type bytes) + X (data bytes) + 4 (CRC bytes)
+ // See PNG (Portable Network Graphics) Specification, Version 1.2,
+ // 3.2. Chunk layout
+ int length = in.readInt();
+
+ byte[] type = new byte[PNG_CHUNK_LENGTH_BYTE_LENGTH];
+ if (in.read(type) != type.length) {
+ throw new IOException("Encountered invalid length while parsing PNG chunk"
+ + "type");
+ }
+
+ if (Arrays.equals(type, PNG_CHUNK_TYPE_IEND)) {
+ // IEND marks the end of the image.
+ break;
+ } else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) {
+ // TODO: Need to handle potential OutOfMemoryError
+ byte[] data = new byte[length];
+ if (in.read(data) != length) {
+ throw new IOException("Failed to read given length for given PNG chunk "
+ + "type: " + byteArrayToHexString(type));
+ }
+ readExifSegment(data, IFD_TYPE_PRIMARY);
+ break;
+ } else {
+ // Skip to next chunk
+ in.skipBytes(length + PNG_CHUNK_CRC_BYTE_LENGTH);
+ }
+ }
+ } catch (EOFException e) {
+ // Should not reach here. Will only reach here if the file is corrupted or
+ // does not follow the PNG specifications
+ throw new IOException("Encountered corrupt PNG file.");
+ }
+ }
+
// Stores a new JPEG image with EXIF attributes into a given output stream.
private void saveJpegAttributes(InputStream inputStream, OutputStream outputStream)
throws IOException {
@@ -3517,6 +3627,7 @@ public class ExifInterface {
if (mFilename == null && mAssetInputStream == null
&& mSeekableFileDescriptor == null) {
+ // TODO: Need to handle potential OutOfMemoryError
// Save the thumbnail in memory if the input doesn't support reading again.
byte[] thumbnailBytes = new byte[thumbnailLength];
in.seek(thumbnailOffset);
@@ -3550,6 +3661,7 @@ public class ExifInterface {
return;
}
+ // TODO: Need to handle potential OutOfMemoryError
// Set thumbnail byte array data for non-consecutive strip bytes
byte[] totalStripBytes =
new byte[(int) Arrays.stream(stripByteCounts).sum()];
@@ -3568,6 +3680,7 @@ public class ExifInterface {
in.seek(skipBytes);
bytesRead += skipBytes;
+ // TODO: Need to handle potential OutOfMemoryError
// Read strip bytes
byte[] stripBytes = new byte[stripByteCount];
in.read(stripBytes);
@@ -4367,4 +4480,12 @@ public class ExifInterface {
}
return null;
}
+
+ private static String byteArrayToHexString(byte[] bytes) {
+ StringBuilder sb = new StringBuilder(bytes.length * 2);
+ for (int i = 0; i < bytes.length; i++) {
+ sb.append(String.format("%02x", bytes[i]));
+ }
+ return sb.toString();
+ }
}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ProtoStore.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ProtoStore.java
new file mode 100644
index 000000000000..3ba5f2b741b8
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ProtoStore.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.encryption.chunking;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AtomicFile;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
+
+import com.google.protobuf.nano.MessageNano;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Optional;
+
+/**
+ * Stores a nano proto for each package, persisting the proto to disk.
+ *
+ * <p>This is used to store {@link ChunksMetadataProto.ChunkListing}.
+ *
+ * @param <T> the type of nano proto to store.
+ */
+public class ProtoStore<T extends MessageNano> {
+ private static final String CHUNK_LISTING_FOLDER = "backup_chunk_listings";
+ private static final String KEY_VALUE_LISTING_FOLDER = "backup_kv_listings";
+
+ private static final String TAG = "BupEncProtoStore";
+
+ private final File mStoreFolder;
+ private final Class<T> mClazz;
+
+ /** Creates a new instance which stores chunk listings at the default location. */
+ public static ProtoStore<ChunksMetadataProto.ChunkListing> createChunkListingStore(
+ Context context) throws IOException {
+ return new ProtoStore<>(
+ ChunksMetadataProto.ChunkListing.class,
+ new File(context.getFilesDir().getAbsoluteFile(), CHUNK_LISTING_FOLDER));
+ }
+
+ /** Creates a new instance which stores key value listings in the default location. */
+ public static ProtoStore<KeyValueListingProto.KeyValueListing> createKeyValueListingStore(
+ Context context) throws IOException {
+ return new ProtoStore<>(
+ KeyValueListingProto.KeyValueListing.class,
+ new File(context.getFilesDir().getAbsoluteFile(), KEY_VALUE_LISTING_FOLDER));
+ }
+
+ /**
+ * Creates a new instance which stores protos in the given folder.
+ *
+ * @param storeFolder The location where the serialized form is stored.
+ */
+ @VisibleForTesting
+ ProtoStore(Class<T> clazz, File storeFolder) throws IOException {
+ mClazz = checkNotNull(clazz);
+ mStoreFolder = ensureDirectoryExistsOrThrow(storeFolder);
+ }
+
+ private static File ensureDirectoryExistsOrThrow(File directory) throws IOException {
+ if (directory.exists() && !directory.isDirectory()) {
+ throw new IOException("Store folder already exists, but isn't a directory.");
+ }
+
+ if (!directory.exists() && !directory.mkdir()) {
+ throw new IOException("Unable to create store folder.");
+ }
+
+ return directory;
+ }
+
+ /**
+ * Returns the chunk listing for the given package, or {@link Optional#empty()} if no listing
+ * exists.
+ */
+ public Optional<T> loadProto(String packageName)
+ throws IOException, IllegalAccessException, InstantiationException,
+ NoSuchMethodException, InvocationTargetException {
+ File file = getFileForPackage(packageName);
+
+ if (!file.exists()) {
+ Slog.d(
+ TAG,
+ "No chunk listing existed for " + packageName + ", returning empty listing.");
+ return Optional.empty();
+ }
+
+ AtomicFile protoStore = new AtomicFile(file);
+ byte[] data = protoStore.readFully();
+
+ Constructor<T> constructor = mClazz.getDeclaredConstructor();
+ T proto = constructor.newInstance();
+ MessageNano.mergeFrom(proto, data);
+ return Optional.of(proto);
+ }
+
+ /** Saves a proto to disk, associating it with the given package. */
+ public void saveProto(String packageName, T proto) throws IOException {
+ checkNotNull(proto);
+ File file = getFileForPackage(packageName);
+
+ try (FileOutputStream os = new FileOutputStream(file)) {
+ os.write(MessageNano.toByteArray(proto));
+ } catch (IOException e) {
+ Slog.e(
+ TAG,
+ "Exception occurred when saving the listing for "
+ + packageName
+ + ", deleting saved listing.",
+ e);
+
+ // If a problem occurred when writing the listing then it might be corrupt, so delete
+ // it.
+ file.delete();
+
+ throw e;
+ }
+ }
+
+ /** Deletes the proto for the given package, or does nothing if the package has no proto. */
+ public void deleteProto(String packageName) {
+ File file = getFileForPackage(packageName);
+ file.delete();
+ }
+
+ /** Deletes every proto of this type, for all package names. */
+ public void deleteAllProtos() {
+ File[] files = mStoreFolder.listFiles();
+
+ // We ensure that the storeFolder exists in the constructor, but check just in case it has
+ // mysteriously disappeared.
+ if (files == null) {
+ return;
+ }
+
+ for (File file : files) {
+ file.delete();
+ }
+ }
+
+ private File getFileForPackage(String packageName) {
+ checkPackageName(packageName);
+ return new File(mStoreFolder, packageName);
+ }
+
+ private static void checkPackageName(String packageName) {
+ if (TextUtils.isEmpty(packageName) || packageName.contains("/")) {
+ throw new IllegalArgumentException(
+ "Package name must not contain '/' or be empty: " + packageName);
+ }
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTask.java
new file mode 100644
index 000000000000..9bf148ddc901
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTask.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.encryption.tasks;
+
+import android.util.Slog;
+import android.util.SparseIntArray;
+
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkOrdering;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunksMetadata;
+
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Locale;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.GCMParameterSpec;
+
+/**
+ * A backup file consists of, in order:
+ *
+ * <ul>
+ * <li>A randomly ordered sequence of encrypted chunks
+ * <li>A plaintext {@link ChunksMetadata} proto, containing the bytes of an encrypted {@link
+ * ChunkOrdering} proto.
+ * <li>A 64-bit long denoting the offset of the file at which the ChunkOrdering proto starts.
+ * </ul>
+ *
+ * <p>This task decrypts such a blob and writes the plaintext to another file.
+ *
+ * <p>The backup file has two formats to indicate the boundaries of the chunks in the encrypted
+ * file. In {@link ChunksMetadataProto#EXPLICIT_STARTS} mode the chunk ordering contains the start
+ * positions of each chunk and the decryptor outputs the chunks in the order they appeared in the
+ * plaintext file. In {@link ChunksMetadataProto#INLINE_LENGTHS} mode the length of each encrypted
+ * chunk is prepended to the chunk in the file and the decryptor outputs the chunks in no specific
+ * order.
+ *
+ * <p>{@link ChunksMetadataProto#EXPLICIT_STARTS} is for use with full backup (Currently used for
+ * all backups as b/77188289 is not implemented yet), {@link ChunksMetadataProto#INLINE_LENGTHS}
+ * will be used for kv backup (once b/77188289 is implemented) to avoid re-uploading the chunk
+ * ordering (see b/70782620).
+ */
+public class BackupFileDecryptorTask {
+ private static final String TAG = "BackupFileDecryptorTask";
+
+ private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
+ private static final int GCM_NONCE_LENGTH_BYTES = 12;
+ private static final int GCM_TAG_LENGTH_BYTES = 16;
+ private static final int BITS_PER_BYTE = 8;
+ private static final String READ_MODE = "r";
+ private static final int BYTES_PER_LONG = 64 / BITS_PER_BYTE;
+
+ private final Cipher mCipher;
+ private final SecretKey mSecretKey;
+
+ /**
+ * A new instance.
+ *
+ * @param secretKey The tertiary key used to encrypt the backup blob.
+ */
+ public BackupFileDecryptorTask(SecretKey secretKey)
+ throws NoSuchPaddingException, NoSuchAlgorithmException {
+ this.mCipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ this.mSecretKey = secretKey;
+ }
+
+ /**
+ * Runs the task, reading the encrypted data from {@code input} and writing the plaintext data
+ * to {@code output}.
+ *
+ * @param inputFile The encrypted backup file.
+ * @param decryptedChunkOutput Unopened output to write the plaintext to, which this class will
+ * open and close during decryption.
+ * @throws IOException if an error occurred reading the encrypted file or writing the plaintext,
+ * or if one of the protos could not be deserialized.
+ */
+ public void decryptFile(File inputFile, DecryptedChunkOutput decryptedChunkOutput)
+ throws IOException, EncryptedRestoreException, IllegalBlockSizeException,
+ BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException,
+ ShortBufferException, NoSuchAlgorithmException {
+ RandomAccessFile input = new RandomAccessFile(inputFile, READ_MODE);
+
+ long metadataOffset = getChunksMetadataOffset(input);
+ ChunksMetadataProto.ChunksMetadata chunksMetadata =
+ getChunksMetadata(input, metadataOffset);
+ ChunkOrdering chunkOrdering = decryptChunkOrdering(chunksMetadata);
+
+ if (chunksMetadata.chunkOrderingType == ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED
+ || chunksMetadata.chunkOrderingType == ChunksMetadataProto.EXPLICIT_STARTS) {
+ Slog.d(TAG, "Using explicit starts");
+ decryptFileWithExplicitStarts(
+ input, decryptedChunkOutput, chunkOrdering, metadataOffset);
+
+ } else if (chunksMetadata.chunkOrderingType == ChunksMetadataProto.INLINE_LENGTHS) {
+ Slog.d(TAG, "Using inline lengths");
+ decryptFileWithInlineLengths(input, decryptedChunkOutput, metadataOffset);
+
+ } else {
+ throw new UnsupportedEncryptedFileException(
+ "Unknown chunk ordering type:" + chunksMetadata.chunkOrderingType);
+ }
+
+ if (!Arrays.equals(decryptedChunkOutput.getDigest(), chunkOrdering.checksum)) {
+ throw new MessageDigestMismatchException("Checksums did not match");
+ }
+ }
+
+ private void decryptFileWithExplicitStarts(
+ RandomAccessFile input,
+ DecryptedChunkOutput decryptedChunkOutput,
+ ChunkOrdering chunkOrdering,
+ long metadataOffset)
+ throws IOException, InvalidKeyException, IllegalBlockSizeException,
+ InvalidAlgorithmParameterException, ShortBufferException, BadPaddingException,
+ NoSuchAlgorithmException {
+ SparseIntArray chunkLengthsByPosition =
+ getChunkLengths(chunkOrdering.starts, (int) metadataOffset);
+ int largestChunkLength = getLargestChunkLength(chunkLengthsByPosition);
+ byte[] encryptedChunkBuffer = new byte[largestChunkLength];
+ // largestChunkLength is 0 if the backup file contains zero chunks e.g. 0 kv pairs.
+ int plaintextBufferLength =
+ Math.max(0, largestChunkLength - GCM_NONCE_LENGTH_BYTES - GCM_TAG_LENGTH_BYTES);
+ byte[] plaintextChunkBuffer = new byte[plaintextBufferLength];
+
+ try (DecryptedChunkOutput output = decryptedChunkOutput.open()) {
+ for (int start : chunkOrdering.starts) {
+ int length = chunkLengthsByPosition.get(start);
+
+ input.seek(start);
+ input.readFully(encryptedChunkBuffer, 0, length);
+ int plaintextLength =
+ decryptChunk(encryptedChunkBuffer, length, plaintextChunkBuffer);
+ outputChunk(output, plaintextChunkBuffer, plaintextLength);
+ }
+ }
+ }
+
+ private void decryptFileWithInlineLengths(
+ RandomAccessFile input, DecryptedChunkOutput decryptedChunkOutput, long metadataOffset)
+ throws MalformedEncryptedFileException, IOException, IllegalBlockSizeException,
+ BadPaddingException, InvalidAlgorithmParameterException, ShortBufferException,
+ InvalidKeyException, NoSuchAlgorithmException {
+ input.seek(0);
+ try (DecryptedChunkOutput output = decryptedChunkOutput.open()) {
+ while (input.getFilePointer() < metadataOffset) {
+ long start = input.getFilePointer();
+ int encryptedChunkLength = input.readInt();
+
+ if (encryptedChunkLength <= 0) {
+ // If the length of the encrypted chunk is not positive we will not make
+ // progress reading the file and so will loop forever.
+ throw new MalformedEncryptedFileException(
+ "Encrypted chunk length not positive:" + encryptedChunkLength);
+ }
+
+ if (start + encryptedChunkLength > metadataOffset) {
+ throw new MalformedEncryptedFileException(
+ String.format(
+ Locale.US,
+ "Encrypted chunk longer (%d) than file (%d)",
+ encryptedChunkLength,
+ metadataOffset));
+ }
+
+ byte[] plaintextChunk = new byte[encryptedChunkLength];
+ byte[] plaintext =
+ new byte
+ [encryptedChunkLength
+ - GCM_NONCE_LENGTH_BYTES
+ - GCM_TAG_LENGTH_BYTES];
+
+ input.readFully(plaintextChunk);
+
+ int plaintextChunkLength =
+ decryptChunk(plaintextChunk, encryptedChunkLength, plaintext);
+ outputChunk(output, plaintext, plaintextChunkLength);
+ }
+ }
+ }
+
+ private void outputChunk(
+ DecryptedChunkOutput output, byte[] plaintextChunkBuffer, int plaintextLength)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException {
+ output.processChunk(plaintextChunkBuffer, plaintextLength);
+ }
+
+ /**
+ * Decrypts chunk and returns the length of the plaintext.
+ *
+ * @param encryptedChunkBuffer The encrypted data, prefixed by the nonce.
+ * @param encryptedChunkBufferLength The length of the encrypted chunk (including nonce).
+ * @param plaintextChunkBuffer The buffer into which to write the plaintext chunk.
+ * @return The length of the plaintext chunk.
+ */
+ private int decryptChunk(
+ byte[] encryptedChunkBuffer,
+ int encryptedChunkBufferLength,
+ byte[] plaintextChunkBuffer)
+ throws InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException,
+ ShortBufferException, IllegalBlockSizeException {
+
+ mCipher.init(
+ Cipher.DECRYPT_MODE,
+ mSecretKey,
+ new GCMParameterSpec(
+ GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE,
+ encryptedChunkBuffer,
+ 0,
+ GCM_NONCE_LENGTH_BYTES));
+
+ return mCipher.doFinal(
+ encryptedChunkBuffer,
+ GCM_NONCE_LENGTH_BYTES,
+ encryptedChunkBufferLength - GCM_NONCE_LENGTH_BYTES,
+ plaintextChunkBuffer);
+ }
+
+ /** Given all the lengths, returns the largest length. */
+ private int getLargestChunkLength(SparseIntArray lengths) {
+ int maxSeen = 0;
+ for (int i = 0; i < lengths.size(); i++) {
+ maxSeen = Math.max(maxSeen, lengths.valueAt(i));
+ }
+ return maxSeen;
+ }
+
+ /**
+ * From a list of the starting position of each chunk in the correct order of the backup data,
+ * calculates a mapping from start position to length of that chunk.
+ *
+ * @param starts The start positions of chunks, in order.
+ * @param chunkOrderingPosition Where the {@link ChunkOrdering} proto starts, used to calculate
+ * the length of the last chunk.
+ * @return The mapping.
+ */
+ private SparseIntArray getChunkLengths(int[] starts, int chunkOrderingPosition) {
+ int[] boundaries = Arrays.copyOf(starts, starts.length + 1);
+ boundaries[boundaries.length - 1] = chunkOrderingPosition;
+ Arrays.sort(boundaries);
+
+ SparseIntArray lengths = new SparseIntArray();
+ for (int i = 0; i < boundaries.length - 1; i++) {
+ lengths.put(boundaries[i], boundaries[i + 1] - boundaries[i]);
+ }
+ return lengths;
+ }
+
+ /**
+ * Reads and decrypts the {@link ChunkOrdering} from the {@link ChunksMetadata}.
+ *
+ * @param metadata The metadata.
+ * @return The ordering.
+ * @throws InvalidProtocolBufferNanoException if there is an issue deserializing the proto.
+ */
+ private ChunkOrdering decryptChunkOrdering(ChunksMetadata metadata)
+ throws InvalidProtocolBufferNanoException, InvalidAlgorithmParameterException,
+ InvalidKeyException, BadPaddingException, IllegalBlockSizeException,
+ UnsupportedEncryptedFileException {
+ assertCryptoSupported(metadata);
+
+ mCipher.init(
+ Cipher.DECRYPT_MODE,
+ mSecretKey,
+ new GCMParameterSpec(
+ GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE,
+ metadata.chunkOrdering,
+ 0,
+ GCM_NONCE_LENGTH_BYTES));
+
+ byte[] decrypted =
+ mCipher.doFinal(
+ metadata.chunkOrdering,
+ GCM_NONCE_LENGTH_BYTES,
+ metadata.chunkOrdering.length - GCM_NONCE_LENGTH_BYTES);
+
+ return ChunkOrdering.parseFrom(decrypted);
+ }
+
+ /**
+ * Asserts that the Cipher and MessageDigest algorithms in the backup metadata are supported.
+ * For now we only support SHA-256 for checksum and 256-bit AES/GCM/NoPadding for the Cipher.
+ *
+ * @param chunksMetadata The file metadata.
+ * @throws UnsupportedEncryptedFileException if any algorithm is unsupported.
+ */
+ private void assertCryptoSupported(ChunksMetadata chunksMetadata)
+ throws UnsupportedEncryptedFileException {
+ if (chunksMetadata.checksumType != ChunksMetadataProto.SHA_256) {
+ // For now we only support SHA-256.
+ throw new UnsupportedEncryptedFileException(
+ "Unrecognized checksum type for backup (this version of backup only supports"
+ + " SHA-256): "
+ + chunksMetadata.checksumType);
+ }
+
+ if (chunksMetadata.cipherType != ChunksMetadataProto.AES_256_GCM) {
+ throw new UnsupportedEncryptedFileException(
+ "Unrecognized cipher type for backup (this version of backup only supports"
+ + " AES-256-GCM: "
+ + chunksMetadata.cipherType);
+ }
+ }
+
+ /**
+ * Reads the offset of the {@link ChunksMetadata} proto from the end of the file.
+ *
+ * @return The offset.
+ * @throws IOException if there is an error reading.
+ */
+ private long getChunksMetadataOffset(RandomAccessFile input) throws IOException {
+ input.seek(input.length() - BYTES_PER_LONG);
+ return input.readLong();
+ }
+
+ /**
+ * Reads the {@link ChunksMetadata} proto from the given position in the file.
+ *
+ * @param input The encrypted file.
+ * @param position The position where the proto starts.
+ * @return The proto.
+ * @throws IOException if there is an issue reading the file or deserializing the proto.
+ */
+ private ChunksMetadata getChunksMetadata(RandomAccessFile input, long position)
+ throws IOException, MalformedEncryptedFileException {
+ long length = input.length();
+ if (position >= length || position < 0) {
+ throw new MalformedEncryptedFileException(
+ String.format(
+ Locale.US,
+ "%d is not valid position for chunks metadata in file of %d bytes",
+ position,
+ length));
+ }
+
+ // Read chunk ordering bytes
+ input.seek(position);
+ long chunksMetadataLength = input.length() - BYTES_PER_LONG - position;
+ byte[] chunksMetadataBytes = new byte[(int) chunksMetadataLength];
+ input.readFully(chunksMetadataBytes);
+
+ try {
+ return ChunksMetadata.parseFrom(chunksMetadataBytes);
+ } catch (InvalidProtocolBufferNanoException e) {
+ throw new MalformedEncryptedFileException(
+ String.format(
+ Locale.US,
+ "Could not read chunks metadata at position %d of file of %d bytes",
+ position,
+ length));
+ }
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MalformedEncryptedFileException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MalformedEncryptedFileException.java
new file mode 100644
index 000000000000..78c370b0d548
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MalformedEncryptedFileException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.encryption.tasks;
+
+/** Exception thrown when we cannot parse the encrypted backup file. */
+public class MalformedEncryptedFileException extends EncryptedRestoreException {
+ public MalformedEncryptedFileException(String message) {
+ super(message);
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MessageDigestMismatchException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MessageDigestMismatchException.java
new file mode 100644
index 000000000000..1e4f43b43e26
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MessageDigestMismatchException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.encryption.tasks;
+
+/**
+ * Error thrown if the message digest of the plaintext backup does not match that in the {@link
+ * com.android.server.backup.encryption.protos.ChunksMetadataProto.ChunkOrdering}.
+ */
+public class MessageDigestMismatchException extends EncryptedRestoreException {
+ public MessageDigestMismatchException(String message) {
+ super(message);
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/UnsupportedEncryptedFileException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/UnsupportedEncryptedFileException.java
new file mode 100644
index 000000000000..9a97e3870d83
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/UnsupportedEncryptedFileException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.encryption.tasks;
+
+/**
+ * Thrown when the backup file provided by the server uses encryption algorithms this version of
+ * backup does not support. This could happen if the backup was created with a newer version of the
+ * code.
+ */
+public class UnsupportedEncryptedFileException extends EncryptedRestoreException {
+ public UnsupportedEncryptedFileException(String message) {
+ super(message);
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ProtoStoreTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ProtoStoreTest.java
new file mode 100644
index 000000000000..d73c8e47f609
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ProtoStoreTest.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.encryption.chunking;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Optional;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class ProtoStoreTest {
+ private static final String TEST_KEY_1 = "test_key_1";
+ private static final ChunkHash TEST_HASH_1 =
+ new ChunkHash(Arrays.copyOf(new byte[] {1}, EncryptedChunk.KEY_LENGTH_BYTES));
+ private static final ChunkHash TEST_HASH_2 =
+ new ChunkHash(Arrays.copyOf(new byte[] {2}, EncryptedChunk.KEY_LENGTH_BYTES));
+ private static final int TEST_LENGTH_1 = 10;
+ private static final int TEST_LENGTH_2 = 18;
+
+ private static final String TEST_PACKAGE_1 = "com.example.test1";
+ private static final String TEST_PACKAGE_2 = "com.example.test2";
+
+ @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ private File mStoreFolder;
+ private ProtoStore<ChunksMetadataProto.ChunkListing> mProtoStore;
+
+ @Before
+ public void setUp() throws Exception {
+ mStoreFolder = mTemporaryFolder.newFolder();
+ mProtoStore = new ProtoStore<>(ChunksMetadataProto.ChunkListing.class, mStoreFolder);
+ }
+
+ @Test
+ public void differentStoreTypes_operateSimultaneouslyWithoutInterfering() throws Exception {
+ ChunksMetadataProto.ChunkListing chunkListing =
+ createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
+ KeyValueListingProto.KeyValueListing keyValueListing =
+ new KeyValueListingProto.KeyValueListing();
+ keyValueListing.entries = new KeyValueListingProto.KeyValueEntry[1];
+ keyValueListing.entries[0] = new KeyValueListingProto.KeyValueEntry();
+ keyValueListing.entries[0].key = TEST_KEY_1;
+ keyValueListing.entries[0].hash = TEST_HASH_1.getHash();
+
+ Context application = ApplicationProvider.getApplicationContext();
+ ProtoStore<ChunksMetadataProto.ChunkListing> chunkListingStore =
+ ProtoStore.createChunkListingStore(application);
+ ProtoStore<KeyValueListingProto.KeyValueListing> keyValueListingStore =
+ ProtoStore.createKeyValueListingStore(application);
+
+ chunkListingStore.saveProto(TEST_PACKAGE_1, chunkListing);
+ keyValueListingStore.saveProto(TEST_PACKAGE_1, keyValueListing);
+
+ ChunksMetadataProto.ChunkListing actualChunkListing =
+ chunkListingStore.loadProto(TEST_PACKAGE_1).get();
+ KeyValueListingProto.KeyValueListing actualKeyValueListing =
+ keyValueListingStore.loadProto(TEST_PACKAGE_1).get();
+ assertListingsEqual(actualChunkListing, chunkListing);
+ assertThat(actualKeyValueListing.entries.length).isEqualTo(1);
+ assertThat(actualKeyValueListing.entries[0].key).isEqualTo(TEST_KEY_1);
+ assertThat(actualKeyValueListing.entries[0].hash).isEqualTo(TEST_HASH_1.getHash());
+ }
+
+ @Test
+ public void construct_storeLocationIsFile_throws() throws Exception {
+ assertThrows(
+ IOException.class,
+ () ->
+ new ProtoStore<>(
+ ChunksMetadataProto.ChunkListing.class,
+ mTemporaryFolder.newFile()));
+ }
+
+ @Test
+ public void loadChunkListing_noListingExists_returnsEmptyListing() throws Exception {
+ Optional<ChunksMetadataProto.ChunkListing> chunkListing =
+ mProtoStore.loadProto(TEST_PACKAGE_1);
+ assertThat(chunkListing.isPresent()).isFalse();
+ }
+
+ @Test
+ public void loadChunkListing_listingExists_returnsExistingListing() throws Exception {
+ ChunksMetadataProto.ChunkListing expected =
+ createChunkListing(
+ ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1, TEST_HASH_2, TEST_LENGTH_2));
+ mProtoStore.saveProto(TEST_PACKAGE_1, expected);
+
+ ChunksMetadataProto.ChunkListing result = mProtoStore.loadProto(TEST_PACKAGE_1).get();
+
+ assertListingsEqual(result, expected);
+ }
+
+ @Test
+ public void loadProto_emptyPackageName_throwsException() throws Exception {
+ assertThrows(IllegalArgumentException.class, () -> mProtoStore.loadProto(""));
+ }
+
+ @Test
+ public void loadProto_nullPackageName_throwsException() throws Exception {
+ assertThrows(IllegalArgumentException.class, () -> mProtoStore.loadProto(null));
+ }
+
+ @Test
+ public void loadProto_packageNameContainsSlash_throwsException() throws Exception {
+ assertThrows(
+ IllegalArgumentException.class, () -> mProtoStore.loadProto(TEST_PACKAGE_1 + "/"));
+ }
+
+ @Test
+ public void saveProto_persistsToNewInstance() throws Exception {
+ ChunksMetadataProto.ChunkListing expected =
+ createChunkListing(
+ ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1, TEST_HASH_2, TEST_LENGTH_2));
+ mProtoStore.saveProto(TEST_PACKAGE_1, expected);
+ mProtoStore = new ProtoStore<>(ChunksMetadataProto.ChunkListing.class, mStoreFolder);
+
+ ChunksMetadataProto.ChunkListing result = mProtoStore.loadProto(TEST_PACKAGE_1).get();
+
+ assertListingsEqual(result, expected);
+ }
+
+ @Test
+ public void saveProto_emptyPackageName_throwsException() throws Exception {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mProtoStore.saveProto("", new ChunksMetadataProto.ChunkListing()));
+ }
+
+ @Test
+ public void saveProto_nullPackageName_throwsException() throws Exception {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mProtoStore.saveProto(null, new ChunksMetadataProto.ChunkListing()));
+ }
+
+ @Test
+ public void saveProto_packageNameContainsSlash_throwsException() throws Exception {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ mProtoStore.saveProto(
+ TEST_PACKAGE_1 + "/", new ChunksMetadataProto.ChunkListing()));
+ }
+
+ @Test
+ public void saveProto_nullListing_throwsException() throws Exception {
+ assertThrows(NullPointerException.class, () -> mProtoStore.saveProto(TEST_PACKAGE_1, null));
+ }
+
+ @Test
+ public void deleteProto_noListingExists_doesNothing() throws Exception {
+ ChunksMetadataProto.ChunkListing listing =
+ createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
+ mProtoStore.saveProto(TEST_PACKAGE_1, listing);
+
+ mProtoStore.deleteProto(TEST_PACKAGE_2);
+
+ assertThat(mProtoStore.loadProto(TEST_PACKAGE_1).get().chunks.length).isEqualTo(1);
+ }
+
+ @Test
+ public void deleteProto_listingExists_deletesListing() throws Exception {
+ ChunksMetadataProto.ChunkListing listing =
+ createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
+ mProtoStore.saveProto(TEST_PACKAGE_1, listing);
+
+ mProtoStore.deleteProto(TEST_PACKAGE_1);
+
+ assertThat(mProtoStore.loadProto(TEST_PACKAGE_1).isPresent()).isFalse();
+ }
+
+ @Test
+ public void deleteAllProtos_deletesAllProtos() throws Exception {
+ ChunksMetadataProto.ChunkListing listing1 =
+ createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
+ ChunksMetadataProto.ChunkListing listing2 =
+ createChunkListing(ImmutableMap.of(TEST_HASH_2, TEST_LENGTH_2));
+ mProtoStore.saveProto(TEST_PACKAGE_1, listing1);
+ mProtoStore.saveProto(TEST_PACKAGE_2, listing2);
+
+ mProtoStore.deleteAllProtos();
+
+ assertThat(mProtoStore.loadProto(TEST_PACKAGE_1).isPresent()).isFalse();
+ assertThat(mProtoStore.loadProto(TEST_PACKAGE_2).isPresent()).isFalse();
+ }
+
+ @Test
+ public void deleteAllProtos_folderDeleted_doesNotCrash() throws Exception {
+ mStoreFolder.delete();
+
+ mProtoStore.deleteAllProtos();
+ }
+
+ private static ChunksMetadataProto.ChunkListing createChunkListing(
+ ImmutableMap<ChunkHash, Integer> chunks) {
+ ChunksMetadataProto.ChunkListing listing = new ChunksMetadataProto.ChunkListing();
+ listing.cipherType = ChunksMetadataProto.AES_256_GCM;
+ listing.chunkOrderingType = ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
+
+ List<ChunksMetadataProto.Chunk> chunkProtos = new ArrayList<>();
+ for (Entry<ChunkHash, Integer> entry : chunks.entrySet()) {
+ ChunksMetadataProto.Chunk chunk = new ChunksMetadataProto.Chunk();
+ chunk.hash = entry.getKey().getHash();
+ chunk.length = entry.getValue();
+ chunkProtos.add(chunk);
+ }
+ listing.chunks = chunkProtos.toArray(new ChunksMetadataProto.Chunk[0]);
+ return listing;
+ }
+
+ private void assertListingsEqual(
+ ChunksMetadataProto.ChunkListing result, ChunksMetadataProto.ChunkListing expected) {
+ assertThat(result.chunks.length).isEqualTo(expected.chunks.length);
+ for (int i = 0; i < result.chunks.length; i++) {
+ assertWithMessage("Chunk " + i)
+ .that(result.chunks[i].length)
+ .isEqualTo(expected.chunks[i].length);
+ assertWithMessage("Chunk " + i)
+ .that(result.chunks[i].hash)
+ .isEqualTo(expected.chunks[i].hash);
+ }
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTaskTest.java
new file mode 100644
index 000000000000..07a6fd2d5b60
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTaskTest.java
@@ -0,0 +1,583 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.encryption.tasks;
+
+import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
+import static com.android.server.backup.testing.CryptoTestUtils.newChunkOrdering;
+import static com.android.server.backup.testing.CryptoTestUtils.newChunksMetadata;
+import static com.android.server.backup.testing.CryptoTestUtils.newPair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.annotation.Nullable;
+import android.app.backup.BackupDataInput;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.encryption.chunking.DecryptedChunkFileOutput;
+import com.android.server.backup.encryption.chunking.EncryptedChunk;
+import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
+import com.android.server.backup.encryption.kv.DecryptedChunkKvOutput;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkOrdering;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunksMetadata;
+import com.android.server.backup.encryption.protos.nano.KeyValuePairProto.KeyValuePair;
+import com.android.server.backup.encryption.tasks.BackupEncrypter.Result;
+import com.android.server.backup.testing.CryptoTestUtils;
+import com.android.server.testing.shadows.ShadowBackupDataInput;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.nano.MessageNano;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.RandomAccessFile;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+@Config(shadows = {ShadowBackupDataInput.class})
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class BackupFileDecryptorTaskTest {
+ private static final String READ_WRITE_MODE = "rw";
+ private static final int BYTES_PER_KILOBYTE = 1024;
+ private static final int MIN_CHUNK_SIZE_BYTES = 2 * BYTES_PER_KILOBYTE;
+ private static final int AVERAGE_CHUNK_SIZE_BYTES = 4 * BYTES_PER_KILOBYTE;
+ private static final int MAX_CHUNK_SIZE_BYTES = 64 * BYTES_PER_KILOBYTE;
+ private static final int BACKUP_DATA_SIZE_BYTES = 60 * BYTES_PER_KILOBYTE;
+ private static final int GCM_NONCE_LENGTH_BYTES = 12;
+ private static final int GCM_TAG_LENGTH_BYTES = 16;
+ private static final int BITS_PER_BYTE = 8;
+ private static final int CHECKSUM_LENGTH_BYTES = 256 / BITS_PER_BYTE;
+ @Nullable private static final FileDescriptor NULL_FILE_DESCRIPTOR = null;
+
+ private static final Set<KeyValuePair> TEST_KV_DATA = new HashSet<>();
+
+ static {
+ TEST_KV_DATA.add(newPair("key1", "value1"));
+ TEST_KV_DATA.add(newPair("key2", "value2"));
+ }
+
+ @Rule public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ private SecretKey mTertiaryKey;
+ private SecretKey mChunkEncryptionKey;
+ private File mInputFile;
+ private File mOutputFile;
+ private DecryptedChunkOutput mFileOutput;
+ private DecryptedChunkKvOutput mKvOutput;
+ private Random mRandom;
+ private BackupFileDecryptorTask mTask;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mRandom = new Random();
+ mTertiaryKey = generateAesKey();
+ // In good situations it's always the same. We allow changing it for testing when somehow it
+ // has become mismatched that we throw an error.
+ mChunkEncryptionKey = mTertiaryKey;
+ mInputFile = mTemporaryFolder.newFile();
+ mOutputFile = mTemporaryFolder.newFile();
+ mFileOutput = new DecryptedChunkFileOutput(mOutputFile);
+ mKvOutput = new DecryptedChunkKvOutput(new ChunkHasher(mTertiaryKey));
+ mTask = new BackupFileDecryptorTask(mTertiaryKey);
+ }
+
+ @Test
+ public void decryptFile_throwsForNonExistentInput() throws Exception {
+ assertThrows(
+ FileNotFoundException.class,
+ () ->
+ mTask.decryptFile(
+ new File(mTemporaryFolder.newFolder(), "nonexistent"),
+ mFileOutput));
+ }
+
+ @Test
+ public void decryptFile_throwsForDirectoryInputFile() throws Exception {
+ assertThrows(
+ FileNotFoundException.class,
+ () -> mTask.decryptFile(mTemporaryFolder.newFolder(), mFileOutput));
+ }
+
+ @Test
+ public void decryptFile_withExplicitStarts_decryptsEncryptedData() throws Exception {
+ byte[] backupData = randomData(BACKUP_DATA_SIZE_BYTES);
+ createEncryptedFileUsingExplicitStarts(backupData);
+
+ mTask.decryptFile(mInputFile, mFileOutput);
+
+ assertThat(Files.readAllBytes(Paths.get(mOutputFile.toURI()))).isEqualTo(backupData);
+ }
+
+ @Test
+ public void decryptFile_withInlineLengths_decryptsEncryptedData() throws Exception {
+ createEncryptedFileUsingInlineLengths(
+ TEST_KV_DATA, chunkOrdering -> chunkOrdering, chunksMetadata -> chunksMetadata);
+ mTask.decryptFile(mInputFile, mKvOutput);
+ assertThat(asMap(mKvOutput.getPairs())).containsExactlyEntriesIn(asMap(TEST_KV_DATA));
+ }
+
+ @Test
+ public void decryptFile_withNoChunkOrderingType_decryptsUsingExplicitStarts() throws Exception {
+ byte[] backupData = randomData(BACKUP_DATA_SIZE_BYTES);
+ createEncryptedFileUsingExplicitStarts(
+ backupData,
+ chunkOrdering -> chunkOrdering,
+ chunksMetadata -> {
+ ChunksMetadata metadata = CryptoTestUtils.clone(chunksMetadata);
+ metadata.chunkOrderingType =
+ ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
+ return metadata;
+ });
+
+ mTask.decryptFile(mInputFile, mFileOutput);
+
+ assertThat(Files.readAllBytes(Paths.get(mOutputFile.toURI()))).isEqualTo(backupData);
+ }
+
+ @Test
+ public void decryptFile_withInlineLengths_throwsForZeroLengths() throws Exception {
+ createEncryptedFileUsingInlineLengths(
+ TEST_KV_DATA, chunkOrdering -> chunkOrdering, chunksMetadata -> chunksMetadata);
+
+ // Set the length of the first chunk to zero.
+ RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
+ raf.seek(0);
+ raf.writeInt(0);
+
+ assertThrows(
+ MalformedEncryptedFileException.class,
+ () -> mTask.decryptFile(mInputFile, mKvOutput));
+ }
+
+ @Test
+ public void decryptFile_withInlineLengths_throwsForLongLengths() throws Exception {
+ createEncryptedFileUsingInlineLengths(
+ TEST_KV_DATA, chunkOrdering -> chunkOrdering, chunksMetadata -> chunksMetadata);
+
+ // Set the length of the first chunk to zero.
+ RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
+ raf.seek(0);
+ raf.writeInt((int) mInputFile.length());
+
+ assertThrows(
+ MalformedEncryptedFileException.class,
+ () -> mTask.decryptFile(mInputFile, mKvOutput));
+ }
+
+ @Test
+ public void decryptFile_throwsForBadKey() throws Exception {
+ createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
+
+ assertThrows(
+ AEADBadTagException.class,
+ () ->
+ new BackupFileDecryptorTask(generateAesKey())
+ .decryptFile(mInputFile, mFileOutput));
+ }
+
+ @Test
+ public void decryptFile_withExplicitStarts_throwsForMangledOrdering() throws Exception {
+ createEncryptedFileUsingExplicitStarts(
+ randomData(BACKUP_DATA_SIZE_BYTES),
+ chunkOrdering -> {
+ ChunkOrdering ordering = CryptoTestUtils.clone(chunkOrdering);
+ Arrays.sort(ordering.starts);
+ return ordering;
+ });
+
+ assertThrows(
+ MessageDigestMismatchException.class,
+ () -> mTask.decryptFile(mInputFile, mFileOutput));
+ }
+
+ @Test
+ public void decryptFile_withExplicitStarts_noChunks_returnsNoData() throws Exception {
+ byte[] backupData = randomData(/*length=*/ 0);
+ createEncryptedFileUsingExplicitStarts(
+ backupData,
+ chunkOrdering -> {
+ ChunkOrdering ordering = CryptoTestUtils.clone(chunkOrdering);
+ ordering.starts = new int[0];
+ return ordering;
+ });
+
+ mTask.decryptFile(mInputFile, mFileOutput);
+
+ assertThat(Files.readAllBytes(Paths.get(mOutputFile.toURI()))).isEqualTo(backupData);
+ }
+
+ @Test
+ public void decryptFile_throwsForMismatchedChecksum() throws Exception {
+ createEncryptedFileUsingExplicitStarts(
+ randomData(BACKUP_DATA_SIZE_BYTES),
+ chunkOrdering -> {
+ ChunkOrdering ordering = CryptoTestUtils.clone(chunkOrdering);
+ ordering.checksum =
+ Arrays.copyOf(randomData(CHECKSUM_LENGTH_BYTES), CHECKSUM_LENGTH_BYTES);
+ return ordering;
+ });
+
+ assertThrows(
+ MessageDigestMismatchException.class,
+ () -> mTask.decryptFile(mInputFile, mFileOutput));
+ }
+
+ @Test
+ public void decryptFile_throwsForBadChunksMetadataOffset() throws Exception {
+ createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
+
+ // Replace the metadata with all 1s.
+ RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
+ raf.seek(raf.length() - Long.BYTES);
+ int metadataOffset = (int) raf.readLong();
+ int metadataLength = (int) raf.length() - metadataOffset - Long.BYTES;
+
+ byte[] allOnes = new byte[metadataLength];
+ Arrays.fill(allOnes, (byte) 1);
+
+ raf.seek(metadataOffset);
+ raf.write(allOnes, /*off=*/ 0, metadataLength);
+
+ MalformedEncryptedFileException thrown =
+ expectThrows(
+ MalformedEncryptedFileException.class,
+ () -> mTask.decryptFile(mInputFile, mFileOutput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ "Could not read chunks metadata at position "
+ + metadataOffset
+ + " of file of "
+ + raf.length()
+ + " bytes");
+ }
+
+ @Test
+ public void decryptFile_throwsForChunksMetadataOffsetBeyondEndOfFile() throws Exception {
+ createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
+
+ RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
+ raf.seek(raf.length() - Long.BYTES);
+ raf.writeLong(raf.length());
+
+ MalformedEncryptedFileException thrown =
+ expectThrows(
+ MalformedEncryptedFileException.class,
+ () -> mTask.decryptFile(mInputFile, mFileOutput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ raf.length()
+ + " is not valid position for chunks metadata in file of "
+ + raf.length()
+ + " bytes");
+ }
+
+ @Test
+ public void decryptFile_throwsForChunksMetadataOffsetBeforeBeginningOfFile() throws Exception {
+ createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
+
+ RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
+ raf.seek(raf.length() - Long.BYTES);
+ raf.writeLong(-1);
+
+ MalformedEncryptedFileException thrown =
+ expectThrows(
+ MalformedEncryptedFileException.class,
+ () -> mTask.decryptFile(mInputFile, mFileOutput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ "-1 is not valid position for chunks metadata in file of "
+ + raf.length()
+ + " bytes");
+ }
+
+ @Test
+ public void decryptFile_throwsForMangledChunks() throws Exception {
+ createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
+
+ // Mess up some bits in a random byte
+ RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
+ raf.seek(50);
+ byte fiftiethByte = raf.readByte();
+ raf.seek(50);
+ raf.write(~fiftiethByte);
+
+ assertThrows(AEADBadTagException.class, () -> mTask.decryptFile(mInputFile, mFileOutput));
+ }
+
+ @Test
+ public void decryptFile_throwsForBadChunkEncryptionKey() throws Exception {
+ mChunkEncryptionKey = generateAesKey();
+
+ createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
+
+ assertThrows(AEADBadTagException.class, () -> mTask.decryptFile(mInputFile, mFileOutput));
+ }
+
+ @Test
+ public void decryptFile_throwsForUnsupportedCipherType() throws Exception {
+ createEncryptedFileUsingExplicitStarts(
+ randomData(BACKUP_DATA_SIZE_BYTES),
+ chunkOrdering -> chunkOrdering,
+ chunksMetadata -> {
+ ChunksMetadata metadata = CryptoTestUtils.clone(chunksMetadata);
+ metadata.cipherType = ChunksMetadataProto.UNKNOWN_CIPHER_TYPE;
+ return metadata;
+ });
+
+ assertThrows(
+ UnsupportedEncryptedFileException.class,
+ () -> mTask.decryptFile(mInputFile, mFileOutput));
+ }
+
+ @Test
+ public void decryptFile_throwsForUnsupportedMessageDigestType() throws Exception {
+ createEncryptedFileUsingExplicitStarts(
+ randomData(BACKUP_DATA_SIZE_BYTES),
+ chunkOrdering -> chunkOrdering,
+ chunksMetadata -> {
+ ChunksMetadata metadata = CryptoTestUtils.clone(chunksMetadata);
+ metadata.checksumType = ChunksMetadataProto.UNKNOWN_CHECKSUM_TYPE;
+ return metadata;
+ });
+
+ assertThrows(
+ UnsupportedEncryptedFileException.class,
+ () -> mTask.decryptFile(mInputFile, mFileOutput));
+ }
+
+ /**
+ * Creates an encrypted backup file from the given data.
+ *
+ * @param data The plaintext content.
+ */
+ private void createEncryptedFileUsingExplicitStarts(byte[] data) throws Exception {
+ createEncryptedFileUsingExplicitStarts(data, chunkOrdering -> chunkOrdering);
+ }
+
+ /**
+ * Creates an encrypted backup file from the given data.
+ *
+ * @param data The plaintext content.
+ * @param chunkOrderingTransformer Transforms the ordering before it's encrypted.
+ */
+ private void createEncryptedFileUsingExplicitStarts(
+ byte[] data, Transformer<ChunkOrdering> chunkOrderingTransformer) throws Exception {
+ createEncryptedFileUsingExplicitStarts(
+ data, chunkOrderingTransformer, chunksMetadata -> chunksMetadata);
+ }
+
+ /**
+ * Creates an encrypted backup file from the given data in mode {@link
+ * ChunksMetadataProto#EXPLICIT_STARTS}.
+ *
+ * @param data The plaintext content.
+ * @param chunkOrderingTransformer Transforms the ordering before it's encrypted.
+ * @param chunksMetadataTransformer Transforms the metadata before it's written.
+ */
+ private void createEncryptedFileUsingExplicitStarts(
+ byte[] data,
+ Transformer<ChunkOrdering> chunkOrderingTransformer,
+ Transformer<ChunksMetadata> chunksMetadataTransformer)
+ throws Exception {
+ Result result = backupFullData(data);
+
+ ArrayList<EncryptedChunk> chunks = new ArrayList<>(result.getNewChunks());
+ Collections.shuffle(chunks);
+ HashMap<ChunkHash, Integer> startPositions = new HashMap<>();
+
+ try (FileOutputStream fos = new FileOutputStream(mInputFile);
+ DataOutputStream dos = new DataOutputStream(fos)) {
+ int position = 0;
+
+ for (EncryptedChunk chunk : chunks) {
+ startPositions.put(chunk.key(), position);
+ dos.write(chunk.nonce());
+ dos.write(chunk.encryptedBytes());
+ position += chunk.nonce().length + chunk.encryptedBytes().length;
+ }
+
+ int[] starts = new int[chunks.size()];
+ List<ChunkHash> chunkListing = result.getAllChunks();
+
+ for (int i = 0; i < chunks.size(); i++) {
+ starts[i] = startPositions.get(chunkListing.get(i));
+ }
+
+ ChunkOrdering chunkOrdering = newChunkOrdering(starts, result.getDigest());
+ chunkOrdering = chunkOrderingTransformer.accept(chunkOrdering);
+
+ ChunksMetadata metadata =
+ newChunksMetadata(
+ ChunksMetadataProto.AES_256_GCM,
+ ChunksMetadataProto.SHA_256,
+ ChunksMetadataProto.EXPLICIT_STARTS,
+ encrypt(chunkOrdering));
+ metadata = chunksMetadataTransformer.accept(metadata);
+
+ dos.write(MessageNano.toByteArray(metadata));
+ dos.writeLong(position);
+ }
+ }
+
+ /**
+ * Creates an encrypted backup file from the given data in mode {@link
+ * ChunksMetadataProto#INLINE_LENGTHS}.
+ *
+ * @param data The plaintext key value pairs to back up.
+ * @param chunkOrderingTransformer Transforms the ordering before it's encrypted.
+ * @param chunksMetadataTransformer Transforms the metadata before it's written.
+ */
+ private void createEncryptedFileUsingInlineLengths(
+ Set<KeyValuePair> data,
+ Transformer<ChunkOrdering> chunkOrderingTransformer,
+ Transformer<ChunksMetadata> chunksMetadataTransformer)
+ throws Exception {
+ Result result = backupKvData(data);
+
+ List<EncryptedChunk> chunks = new ArrayList<>(result.getNewChunks());
+ System.out.println("we have chunk count " + chunks.size());
+ Collections.shuffle(chunks);
+
+ try (FileOutputStream fos = new FileOutputStream(mInputFile);
+ DataOutputStream dos = new DataOutputStream(fos)) {
+ for (EncryptedChunk chunk : chunks) {
+ dos.writeInt(chunk.nonce().length + chunk.encryptedBytes().length);
+ dos.write(chunk.nonce());
+ dos.write(chunk.encryptedBytes());
+ }
+
+ ChunkOrdering chunkOrdering = newChunkOrdering(null, result.getDigest());
+ chunkOrdering = chunkOrderingTransformer.accept(chunkOrdering);
+
+ ChunksMetadata metadata =
+ newChunksMetadata(
+ ChunksMetadataProto.AES_256_GCM,
+ ChunksMetadataProto.SHA_256,
+ ChunksMetadataProto.INLINE_LENGTHS,
+ encrypt(chunkOrdering));
+ metadata = chunksMetadataTransformer.accept(metadata);
+
+ int metadataStart = dos.size();
+ dos.write(MessageNano.toByteArray(metadata));
+ dos.writeLong(metadataStart);
+ }
+ }
+
+ /** Performs a full backup of the given data, and returns the chunks. */
+ private BackupEncrypter.Result backupFullData(byte[] data) throws Exception {
+ BackupStreamEncrypter encrypter =
+ new BackupStreamEncrypter(
+ new ByteArrayInputStream(data),
+ MIN_CHUNK_SIZE_BYTES,
+ MAX_CHUNK_SIZE_BYTES,
+ AVERAGE_CHUNK_SIZE_BYTES);
+ return encrypter.backup(
+ mChunkEncryptionKey,
+ randomData(FingerprintMixer.SALT_LENGTH_BYTES),
+ new HashSet<>());
+ }
+
+ private Result backupKvData(Set<KeyValuePair> data) throws Exception {
+ ShadowBackupDataInput.reset();
+ for (KeyValuePair pair : data) {
+ ShadowBackupDataInput.addEntity(pair.key, pair.value);
+ }
+ KvBackupEncrypter encrypter =
+ new KvBackupEncrypter(new BackupDataInput(NULL_FILE_DESCRIPTOR));
+ return encrypter.backup(
+ mChunkEncryptionKey,
+ randomData(FingerprintMixer.SALT_LENGTH_BYTES),
+ Collections.EMPTY_SET);
+ }
+
+ /** Encrypts {@code chunkOrdering} using {@link #mTertiaryKey}. */
+ private byte[] encrypt(ChunkOrdering chunkOrdering) throws Exception {
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ byte[] nonce = randomData(GCM_NONCE_LENGTH_BYTES);
+ cipher.init(
+ Cipher.ENCRYPT_MODE,
+ mTertiaryKey,
+ new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE, nonce));
+ byte[] nanoBytes = MessageNano.toByteArray(chunkOrdering);
+ byte[] encryptedBytes = cipher.doFinal(nanoBytes);
+
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ out.write(nonce);
+ out.write(encryptedBytes);
+ return out.toByteArray();
+ }
+ }
+
+ /** Returns {@code length} random bytes. */
+ private byte[] randomData(int length) {
+ byte[] data = new byte[length];
+ mRandom.nextBytes(data);
+ return data;
+ }
+
+ private static ImmutableMap<String, String> asMap(Collection<KeyValuePair> pairs) {
+ ImmutableMap.Builder<String, String> map = ImmutableMap.builder();
+ for (KeyValuePair pair : pairs) {
+ map.put(pair.key, new String(pair.value, Charset.forName("UTF-8")));
+ }
+ return map.build();
+ }
+
+ private interface Transformer<T> {
+ T accept(T t);
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
index b9055cecd502..5cff53f817d4 100644
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
@@ -18,7 +18,9 @@ package com.android.server.backup.testing;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
+import java.nio.charset.Charset;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Random;
@@ -86,11 +88,33 @@ public class CryptoTestUtils {
public static ChunksMetadataProto.ChunkOrdering newChunkOrdering(
int[] starts, byte[] checksum) {
ChunksMetadataProto.ChunkOrdering chunkOrdering = new ChunksMetadataProto.ChunkOrdering();
- chunkOrdering.starts = Arrays.copyOf(starts, starts.length);
- chunkOrdering.checksum = Arrays.copyOf(checksum, checksum.length);
+ chunkOrdering.starts = starts == null ? null : Arrays.copyOf(starts, starts.length);
+ chunkOrdering.checksum =
+ checksum == null ? checksum : Arrays.copyOf(checksum, checksum.length);
return chunkOrdering;
}
+ public static ChunksMetadataProto.ChunksMetadata newChunksMetadata(
+ int cipherType, int checksumType, int chunkOrderingType, byte[] chunkOrdering) {
+ ChunksMetadataProto.ChunksMetadata metadata = new ChunksMetadataProto.ChunksMetadata();
+ metadata.cipherType = cipherType;
+ metadata.checksumType = checksumType;
+ metadata.chunkOrdering = Arrays.copyOf(chunkOrdering, chunkOrdering.length);
+ metadata.chunkOrderingType = chunkOrderingType;
+ return metadata;
+ }
+
+ public static KeyValuePairProto.KeyValuePair newPair(String key, String value) {
+ return newPair(key, value.getBytes(Charset.forName("UTF-8")));
+ }
+
+ public static KeyValuePairProto.KeyValuePair newPair(String key, byte[] value) {
+ KeyValuePairProto.KeyValuePair newPair = new KeyValuePairProto.KeyValuePair();
+ newPair.key = key;
+ newPair.value = value;
+ return newPair;
+ }
+
public static ChunksMetadataProto.ChunkListing clone(
ChunksMetadataProto.ChunkListing original) {
ChunksMetadataProto.Chunk[] clonedChunks;
@@ -114,4 +138,25 @@ public class CryptoTestUtils {
public static ChunksMetadataProto.Chunk clone(ChunksMetadataProto.Chunk original) {
return newChunk(original.hash, original.length);
}
+
+ public static ChunksMetadataProto.ChunksMetadata clone(
+ ChunksMetadataProto.ChunksMetadata original) {
+ ChunksMetadataProto.ChunksMetadata cloneMetadata = new ChunksMetadataProto.ChunksMetadata();
+ cloneMetadata.chunkOrderingType = original.chunkOrderingType;
+ cloneMetadata.chunkOrdering =
+ original.chunkOrdering == null
+ ? null
+ : Arrays.copyOf(original.chunkOrdering, original.chunkOrdering.length);
+ cloneMetadata.checksumType = original.checksumType;
+ cloneMetadata.cipherType = original.cipherType;
+ return cloneMetadata;
+ }
+
+ public static ChunksMetadataProto.ChunkOrdering clone(
+ ChunksMetadataProto.ChunkOrdering original) {
+ ChunksMetadataProto.ChunkOrdering clone = new ChunksMetadataProto.ChunkOrdering();
+ clone.starts = Arrays.copyOf(original.starts, original.starts.length);
+ clone.checksum = Arrays.copyOf(original.checksum, original.checksum.length);
+ return clone;
+ }
}
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 8f474704097e..1f923aff0cea 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1004,9 +1004,9 @@
<!-- [CHAR_LIMIT=40] Label for battery level chart when charging -->
<string name="power_charging"><xliff:g id="level">%1$s</xliff:g> - <xliff:g id="state">%2$s</xliff:g></string>
<!-- [CHAR_LIMIT=40] Label for estimated remaining duration of battery charging -->
- <string name="power_remaining_charging_duration_only"><xliff:g id="time">%1$s</xliff:g> left until fully charged</string>
+ <string name="power_remaining_charging_duration_only"><xliff:g id="time">%1$s</xliff:g> left until charged</string>
<!-- [CHAR_LIMIT=40] Label for battery level chart when charging with duration -->
- <string name="power_charging_duration"><xliff:g id="level">%1$s</xliff:g> - <xliff:g id="time">%2$s</xliff:g> until fully charged</string>
+ <string name="power_charging_duration"><xliff:g id="level">%1$s</xliff:g> - <xliff:g id="time">%2$s</xliff:g> until charged</string>
<!-- Battery Info screen. Value for a status item. Used for diagnostic info screens, precise translation isn't needed -->
<string name="battery_info_status_unknown">Unknown</string>
diff --git a/packages/SettingsLib/search/Android.bp b/packages/SettingsLib/search/Android.bp
index 15c8367cf727..d398aa5c44ac 100644
--- a/packages/SettingsLib/search/Android.bp
+++ b/packages/SettingsLib/search/Android.bp
@@ -1,7 +1,9 @@
-java_library {
+android_library {
name: "SettingsLib-search",
- host_supported: true,
srcs: ["src/**/*.java"],
+
+ sdk_version: "system_current",
+ min_sdk_version: "21",
}
java_plugin {
@@ -9,9 +11,11 @@ java_plugin {
processor_class: "com.android.settingslib.search.IndexableProcessor",
static_libs: [
"javapoet-prebuilt-jar",
- "SettingsLib-search",
],
- srcs: ["processor-src/**/*.java"],
+ srcs: [
+ "processor-src/**/*.java",
+ "src/com/android/settingslib/search/SearchIndexable.java"
+ ],
java_resource_dirs: ["resources"],
}
diff --git a/packages/SettingsLib/search/AndroidManifest.xml b/packages/SettingsLib/search/AndroidManifest.xml
new file mode 100644
index 000000000000..970728365f5c
--- /dev/null
+++ b/packages/SettingsLib/search/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.search">
+
+</manifest> \ No newline at end of file
diff --git a/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java b/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java
index 10fc685015b7..5dc9061a81a0 100644
--- a/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java
+++ b/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java
@@ -143,7 +143,7 @@ public class IndexableProcessor extends AbstractProcessor {
final TypeSpec baseClass = TypeSpec.classBuilder(CLASS_BASE)
.addModifiers(Modifier.PUBLIC)
- .addSuperinterface(ClassName.get(SearchIndexableResources.class))
+ .addSuperinterface(ClassName.get(PACKAGE, "SearchIndexableResources"))
.addField(providers)
.addMethod(baseConstructorBuilder.build())
.addMethod(addIndex)
@@ -210,4 +210,4 @@ public class IndexableProcessor extends AbstractProcessor {
mFiler = processingEnvironment.getFiler();
mMessager = processingEnvironment.getMessager();
}
-}
+} \ No newline at end of file
diff --git a/packages/SettingsLib/search/src/com/android/settingslib/search/Indexable.java b/packages/SettingsLib/search/src/com/android/settingslib/search/Indexable.java
new file mode 100644
index 000000000000..e68b0d1d6798
--- /dev/null
+++ b/packages/SettingsLib/search/src/com/android/settingslib/search/Indexable.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.search;
+
+import android.content.Context;
+import android.provider.SearchIndexableResource;
+
+import java.util.List;
+
+/**
+ * Interface for classes whose instances can provide data for indexing.
+ *
+ * See {@link android.provider.SearchIndexableResource} and {@link SearchIndexableRaw}.
+ */
+public interface Indexable {
+
+ /**
+ * Interface for classes whose instances can provide data for indexing.
+ */
+ interface SearchIndexProvider {
+ /**
+ * Return a list of references for indexing.
+ *
+ * See {@link android.provider.SearchIndexableResource}
+ *
+ * @param context the context.
+ * @param enabled hint telling if the data needs to be considered into the search results
+ * or not.
+ * @return a list of {@link android.provider.SearchIndexableResource} references.
+ * Can be null.
+ */
+ List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled);
+
+ /**
+ * Return a list of raw data for indexing. See {@link SearchIndexableRaw}
+ *
+ * @param context the context.
+ * @param enabled hint telling if the data needs to be considered into the search results
+ * or not.
+ * @return a list of {@link SearchIndexableRaw} references. Can be null.
+ */
+ List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled);
+
+ /**
+ * Return a list of data keys that cannot be indexed. See {@link SearchIndexableRaw}
+ *
+ * @param context the context.
+ * @return a list of {@link SearchIndexableRaw} references. Can be null.
+ */
+ List<String> getNonIndexableKeys(Context context);
+ }
+}
diff --git a/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexableRaw.java b/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexableRaw.java
new file mode 100644
index 000000000000..021ca3362aab
--- /dev/null
+++ b/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexableRaw.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.search;
+
+import android.content.Context;
+import android.provider.SearchIndexableData;
+
+/**
+ * Indexable raw data for Search.
+ *
+ * This is the raw data used by the Indexer and should match its data model.
+ *
+ * See {@link Indexable} and {@link android.provider.SearchIndexableResource}.
+ */
+public class SearchIndexableRaw extends SearchIndexableData {
+
+ /**
+ * Title's raw data.
+ */
+ public String title;
+
+ /**
+ * Summary's raw data when the data is "ON".
+ */
+ public String summaryOn;
+
+ /**
+ * Summary's raw data when the data is "OFF".
+ */
+ public String summaryOff;
+
+ /**
+ * Entries associated with the raw data (when the data can have several values).
+ */
+ public String entries;
+
+ /**
+ * Keywords' raw data.
+ */
+ public String keywords;
+
+ /**
+ * Fragment's or Activity's title associated with the raw data.
+ */
+ public String screenTitle;
+
+ public SearchIndexableRaw(Context context) {
+ super(context);
+ }
+}
diff --git a/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexableResources.java b/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexableResources.java
index 300d360e0057..976647b3e88f 100644
--- a/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexableResources.java
+++ b/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexableResources.java
@@ -32,4 +32,4 @@ public interface SearchIndexableResources {
* as a device binary.
*/
void addIndex(Class indexClass);
-}
+} \ No newline at end of file
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 3a7de188f962..86625faab4d6 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -719,7 +719,8 @@ public class SettingsBackupTest {
Settings.Secure.BIOMETRIC_DEBUG_ENABLED,
Settings.Secure.FACE_UNLOCK_ATTENTION_REQUIRED,
Settings.Secure.FACE_UNLOCK_DIVERSITY_REQUIRED,
- Settings.Secure.MANAGED_PROVISIONING_DPC_DOWNLOADED);
+ Settings.Secure.MANAGED_PROVISIONING_DPC_DOWNLOADED,
+ Settings.Secure.FACE_UNLOCK_RE_ENROLL);
@Test
public void systemSettingsBackedUpOrBlacklisted() {
diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java
index 602fe3ec12fd..2a41aa6bb8f6 100644
--- a/packages/Shell/src/com/android/shell/BugreportProgressService.java
+++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java
@@ -37,6 +37,7 @@ import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
+import android.app.admin.DevicePolicyManager;
import android.content.ClipData;
import android.content.Context;
import android.content.DialogInterface;
@@ -107,6 +108,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -157,6 +160,8 @@ public class BugreportProgressService extends Service {
private static final String TAG = "BugreportProgressService";
private static final boolean DEBUG = false;
+ private Intent startSelfIntent;
+
private static final String AUTHORITY = "com.android.shell";
// External intents sent by dumpstate.
@@ -235,6 +240,24 @@ public class BugreportProgressService extends Service {
private static final String NOTIFICATION_CHANNEL_ID = "bugreports";
+ /**
+ * Always keep the newest 8 bugreport files.
+ */
+ private static final int MIN_KEEP_COUNT = 8;
+
+ /**
+ * Always keep bugreports taken in the last week.
+ */
+ private static final long MIN_KEEP_AGE = DateUtils.WEEK_IN_MILLIS;
+
+ private static final String BUGREPORT_MIMETYPE = "application/vnd.android.bugreport";
+
+ /** Always keep just the last 3 remote bugreport's files around. */
+ private static final int REMOTE_BUGREPORT_FILES_AMOUNT = 3;
+
+ /** Always keep remote bugreport files created in the last day. */
+ private static final long REMOTE_MIN_KEEP_AGE = DateUtils.DAY_IN_MILLIS;
+
private final Object mLock = new Object();
/** Managed bugreport info (keyed by id) */
@@ -281,6 +304,7 @@ public class BugreportProgressService extends Service {
mMainThreadHandler = new Handler(Looper.getMainLooper());
mServiceHandler = new ServiceHandler("BugreportProgressServiceMainThread");
mScreenshotHandler = new ScreenshotHandler("BugreportProgressServiceScreenshotThread");
+ startSelfIntent = new Intent(this, this.getClass());
mScreenshotsDir = new File(getFilesDir(), SCREENSHOT_DIR);
if (!mScreenshotsDir.exists()) {
@@ -307,6 +331,9 @@ public class BugreportProgressService extends Service {
public int onStartCommand(Intent intent, int flags, int startId) {
Log.v(TAG, "onStartCommand(): " + dumpIntent(intent));
if (intent != null) {
+ if (!intent.hasExtra(EXTRA_ORIGINAL_INTENT) && !intent.hasExtra(EXTRA_ID)) {
+ return START_NOT_STICKY;
+ }
// Handle it in a separate thread.
final Message msg = mServiceHandler.obtainMessage();
msg.what = MSG_SERVICE_COMMAND;
@@ -352,10 +379,11 @@ public class BugreportProgressService extends Service {
private final BugreportInfo mInfo;
- BugreportCallbackImpl(String name, @Nullable String title, @Nullable String description) {
+ BugreportCallbackImpl(String name, @Nullable String title, @Nullable String description,
+ @BugreportParams.BugreportMode int type) {
// pid not used in this workflow, so setting default = 0
mInfo = new BugreportInfo(mContext, 0 /* pid */, name,
- 100 /* max progress*/, title, description);
+ 100 /* max progress*/, title, description, type);
}
@Override
@@ -380,10 +408,9 @@ public class BugreportProgressService extends Service {
@Override
public void onFinished() {
+ // TODO: Make all callback functions lock protected.
trackInfoWithId();
- // Stop running on foreground, otherwise share notification cannot be dismissed.
- onBugreportFinished(mInfo.id);
- stopSelfWhenDone();
+ sendBugreportFinishedBroadcast();
}
/**
@@ -400,6 +427,90 @@ public class BugreportProgressService extends Service {
}
return;
}
+
+ private void sendBugreportFinishedBroadcast() {
+ final String bugreportFileName = mInfo.name + ".zip";
+ final File bugreportFile = new File(BUGREPORT_DIR, bugreportFileName);
+ final String bugreportFilePath = bugreportFile.getAbsolutePath();
+ if (bugreportFile.length() == 0) {
+ Log.e(TAG, "Bugreport file empty. File path = " + bugreportFilePath);
+ return;
+ }
+ if (mInfo.type == BugreportParams.BUGREPORT_MODE_REMOTE) {
+ sendRemoteBugreportFinishedBroadcast(bugreportFilePath, bugreportFile);
+ } else {
+ cleanupOldFiles(MIN_KEEP_COUNT, MIN_KEEP_AGE);
+ final Intent intent = new Intent(INTENT_BUGREPORT_FINISHED);
+ intent.putExtra(EXTRA_BUGREPORT, bugreportFilePath);
+ addScreenshotToIntent(intent);
+ mContext.sendBroadcast(intent, android.Manifest.permission.DUMP);
+ onBugreportFinished(mInfo.id);
+ }
+ }
+
+ private void sendRemoteBugreportFinishedBroadcast(String bugreportFileName,
+ File bugreportFile) {
+ cleanupOldFiles(REMOTE_BUGREPORT_FILES_AMOUNT, REMOTE_MIN_KEEP_AGE);
+ final Intent intent = new Intent(DevicePolicyManager.ACTION_REMOTE_BUGREPORT_DISPATCH);
+ final Uri bugreportUri = getUri(mContext, bugreportFile);
+ final String bugreportHash = generateFileHash(bugreportFileName);
+ if (bugreportHash == null) {
+ Log.e(TAG, "Error generating file hash for remote bugreport");
+ return;
+ }
+ intent.setDataAndType(bugreportUri, BUGREPORT_MIMETYPE);
+ intent.putExtra(DevicePolicyManager.EXTRA_REMOTE_BUGREPORT_HASH, bugreportHash);
+ intent.putExtra(EXTRA_BUGREPORT, bugreportFileName);
+ mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM,
+ android.Manifest.permission.DUMP);
+ }
+
+ private void addScreenshotToIntent(Intent intent) {
+ final String screenshotFileName = mInfo.name + ".png";
+ final File screenshotFile = new File(BUGREPORT_DIR, screenshotFileName);
+ final String screenshotFilePath = screenshotFile.getAbsolutePath();
+ if (screenshotFile.length() > 0) {
+ intent.putExtra(EXTRA_SCREENSHOT, screenshotFilePath);
+ }
+ return;
+ }
+
+ private String generateFileHash(String fileName) {
+ String fileHash = null;
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ FileInputStream input = new FileInputStream(new File(fileName));
+ byte[] buffer = new byte[65536];
+ int size;
+ while ((size = input.read(buffer)) > 0) {
+ md.update(buffer, 0, size);
+ }
+ input.close();
+ byte[] hashBytes = md.digest();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < hashBytes.length; i++) {
+ sb.append(String.format("%02x", hashBytes[i]));
+ }
+ fileHash = sb.toString();
+ } catch (IOException | NoSuchAlgorithmException e) {
+ Log.e(TAG, "generating file hash for bugreport file failed " + fileName, e);
+ }
+ return fileHash;
+ }
+ }
+
+ static void cleanupOldFiles(final int minCount, final long minAge) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ FileUtils.deleteOlderFiles(new File(BUGREPORT_DIR), minCount, minAge);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "RuntimeException deleting old files", e);
+ }
+ return null;
+ }
+ }.execute();
}
/**
@@ -598,7 +709,7 @@ public class BugreportProgressService extends Service {
+ " screenshot file fd: " + screenshotFd);
BugreportCallbackImpl bugreportCallback = new BugreportCallbackImpl(bugreportName,
- shareTitle, shareDescription);
+ shareTitle, shareDescription, bugreportType);
try {
mBugreportManager.startBugreport(bugreportFd, screenshotFd,
new BugreportParams(bugreportType), executor, bugreportCallback);
@@ -711,6 +822,9 @@ public class BugreportProgressService extends Service {
} else {
mForegroundId = id;
Log.d(TAG, "Start running as foreground service on id " + mForegroundId);
+ // Explicitly starting the service so that stopForeground() does not crash
+ // Workaround for b/140997620
+ startForegroundService(startSelfIntent);
startForeground(mForegroundId, notification);
}
}
@@ -1925,10 +2039,19 @@ public class BugreportProgressService extends Service {
String shareDescription;
/**
+ * Type of the bugreport
+ */
+ int type;
+
+ /**
* Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED.
*/
BugreportInfo(Context context, int id, int pid, String name, int max) {
- this(context, pid, name, max, null, null);
+ // bugreports triggered by STARTED broadcast do not use callback functions,
+ // onFinished() callback method is the only function where type is used.
+ // Set type to -1 as it is unused in this workflow.
+ // This constructor will soon be removed.
+ this(context, pid, name, max, null, null, -1);
this.id = id;
}
@@ -1936,13 +2059,14 @@ public class BugreportProgressService extends Service {
* Constructor for tracked bugreports - typically called upon receiving BUGREPORT_REQUESTED.
*/
BugreportInfo(Context context, int pid, String name, int max, @Nullable String shareTitle,
- @Nullable String shareDescription) {
+ @Nullable String shareDescription, int type) {
this.context = context;
this.pid = pid;
this.name = name;
this.max = this.realMax = max;
this.shareTitle = shareTitle == null ? "" : shareTitle;
this.shareDescription = shareDescription == null ? "" : shareDescription;
+ this.type = type;
}
/**
diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml
index 0fe7084bb145..485240a8895b 100644
--- a/packages/SystemUI/res-keyguard/values/strings.xml
+++ b/packages/SystemUI/res-keyguard/values/strings.xml
@@ -62,7 +62,7 @@
<!-- When the lock screen is showing, the phone is plugged in and the battery is fully
charged, say that it is charged. -->
- <string name="keyguard_charged">Fully charged</string>
+ <string name="keyguard_charged">Charged</string>
<!-- When the lock screen is showing and the phone plugged in, and the battery is not fully charged, say that it's wirelessly charging. [CHAR LIMIT=50] -->
<string name="keyguard_plugged_in_wireless"><xliff:g id="percentage" example="20%">%s</xliff:g> • Charging wirelessly</string>
diff --git a/packages/SystemUI/res/layout/status_bar_expanded_plugin_frame.xml b/packages/SystemUI/res/layout/status_bar_expanded_plugin_frame.xml
index 4849dfb777ed..7d6ff3b16db6 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded_plugin_frame.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded_plugin_frame.xml
@@ -20,10 +20,10 @@
android:id="@+id/plugin_frame"
android:theme="@style/qs_theme"
android:layout_width="@dimen/qs_panel_width"
- android:layout_height="96dp"
+ android:layout_height="105dp"
android:layout_gravity="center_horizontal"
- android:layout_marginTop="@*android:dimen/quick_qs_total_height"
+ android:layout_marginTop="@dimen/notification_side_paddings"
android:layout_marginLeft="@dimen/notification_side_paddings"
android:layout_marginRight="@dimen/notification_side_paddings"
android:visibility="gone"
- android:background="@drawable/qs_background_primary"/> \ No newline at end of file
+ android:background="@drawable/qs_background_primary"/>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
index eaaa3ed78654..e3ac0f684e44 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -86,13 +86,12 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView
mSecurityMessageDisplay.setMessage("");
}
final boolean wasDisabled = mPasswordEntry.isEnabled();
- // Don't set enabled password entry & showSoftInput when PasswordEntry is invisible or in
- // pausing stage.
+ setPasswordEntryEnabled(true);
+ setPasswordEntryInputEnabled(true);
+ // Don't call showSoftInput when PasswordEntry is invisible or in pausing stage.
if (!mResumed || !mPasswordEntry.isVisibleToUser()) {
return;
}
- setPasswordEntryEnabled(true);
- setPasswordEntryInputEnabled(true);
if (wasDisabled) {
mImm.showSoftInput(mPasswordEntry, InputMethodManager.SHOW_IMPLICIT);
}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactory.java
index 2c8324cafca0..aa13fa834f56 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactory.java
@@ -92,6 +92,12 @@ public class SystemUIAppComponentFactory extends AppComponentFactory {
public Activity instantiateActivityCompat(@NonNull ClassLoader cl, @NonNull String className,
@Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ if (mComponentHelper == null) {
+ // This shouldn't happen, but is seen on occasion.
+ // Bug filed against framework to take a look: http://b/141008541
+ SystemUIFactory.getInstance().getRootComponent().inject(
+ SystemUIAppComponentFactory.this);
+ }
Activity activity = mComponentHelper.resolveActivity(className);
if (activity != null) {
return activity;
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/SystemUIBinder.java
index 4531c892a022..0fa80aca97fb 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIBinder.java
@@ -17,6 +17,7 @@
package com.android.systemui;
import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.power.PowerUI;
import dagger.Binds;
import dagger.Module;
@@ -33,4 +34,10 @@ public abstract class SystemUIBinder {
@IntoMap
@ClassKey(KeyguardViewMediator.class)
public abstract SystemUI bindKeyguardViewMediator(KeyguardViewMediator sysui);
+
+ /** Inject into PowerUI. */
+ @Binds
+ @IntoMap
+ @ClassKey(PowerUI.class)
+ public abstract SystemUI bindPowerUI(PowerUI sysui);
}
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt
index f0e8c16e650a..5e977b4684dc 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt
@@ -75,7 +75,7 @@ open class BroadcastDispatcher @Inject constructor (
* @param filter A filter to determine what broadcasts should be dispatched to this receiver.
* It will only take into account actions and categories for filtering.
* @param handler A handler to dispatch [BroadcastReceiver.onReceive]. By default, it is the
- * main handler.
+ * main handler. Pass `null` to use the default.
* @param user A user handle to determine which broadcast should be dispatched to this receiver.
* By default, it is the current user.
*/
@@ -83,10 +83,12 @@ open class BroadcastDispatcher @Inject constructor (
fun registerReceiver(
receiver: BroadcastReceiver,
filter: IntentFilter,
- handler: Handler = mainHandler,
+ handler: Handler? = mainHandler,
user: UserHandle = context.user
) {
- this.handler.obtainMessage(MSG_ADD_RECEIVER, ReceiverData(receiver, filter, handler, user))
+ this.handler
+ .obtainMessage(MSG_ADD_RECEIVER,
+ ReceiverData(receiver, filter, handler ?: mainHandler, user))
.sendToTarget()
}
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt
index d44b63e813e6..54f9950239c2 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt
@@ -34,7 +34,7 @@ import java.util.concurrent.atomic.AtomicBoolean
private const val MSG_REGISTER_RECEIVER = 0
private const val MSG_UNREGISTER_RECEIVER = 1
-private const val TAG = "UniversalReceiver"
+private const val TAG = "UserBroadcastDispatcher"
private const val DEBUG = false
/**
@@ -97,7 +97,7 @@ class UserBroadcastDispatcher(
private val receiverToReceiverData = ArrayMap<BroadcastReceiver, MutableSet<ReceiverData>>()
override fun onReceive(context: Context, intent: Intent) {
- bgHandler.post(HandleBroadcastRunnable(actionsToReceivers, context, intent))
+ bgHandler.post(HandleBroadcastRunnable(actionsToReceivers, context, intent, pendingResult))
}
/**
@@ -160,7 +160,8 @@ class UserBroadcastDispatcher(
private class HandleBroadcastRunnable(
val actionsToReceivers: Map<String, Set<ReceiverData>>,
val context: Context,
- val intent: Intent
+ val intent: Intent,
+ val pendingResult: PendingResult
) : Runnable {
override fun run() {
if (DEBUG) Log.w(TAG, "Dispatching $intent")
@@ -171,6 +172,7 @@ class UserBroadcastDispatcher(
?.forEach {
it.handler.post {
if (DEBUG) Log.w(TAG, "Dispatching to ${it.receiver}")
+ it.receiver.pendingResult = pendingResult
it.receiver.onReceive(context, intent)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index 57a5ae63511c..22846bc02a38 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -90,6 +90,7 @@ import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.MultiListLayout;
import com.android.systemui.MultiListLayout.MultiListAdapter;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.GlobalActions.GlobalActionsManager;
@@ -187,7 +188,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener,
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED);
- context.registerReceiver(mBroadcastReceiver, filter);
+ Dependency.get(BroadcastDispatcher.class).registerReceiver(mBroadcastReceiver, filter);
ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java
index 4f2a6d82a08e..5723afd4ae95 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java
@@ -247,6 +247,9 @@ public class PipMenuActivity extends Activity {
protected void onStop() {
super.onStop();
+ // In cases such as device lock, hide and finish it so that it can be recreated on the top
+ // next time it starts, see also {@link #onUserLeaveHint}
+ hideMenu();
cancelDelayedFinish();
}
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
index 75dc39722bcf..a258f356bf53 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
@@ -45,6 +45,7 @@ import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.SystemUI;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.statusbar.phone.StatusBar;
import java.io.FileDescriptor;
@@ -53,6 +54,8 @@ import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.Future;
+import javax.inject.Inject;
+
public class PowerUI extends SystemUI {
static final String TAG = "PowerUI";
@@ -97,6 +100,12 @@ public class PowerUI extends SystemUI {
private IThermalEventListener mSkinThermalEventListener;
private IThermalEventListener mUsbThermalEventListener;
+ private final BroadcastDispatcher mBroadcastDispatcher;
+
+ @Inject
+ public PowerUI(BroadcastDispatcher broadcastDispatcher) {
+ mBroadcastDispatcher = broadcastDispatcher;
+ }
public void start() {
mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
@@ -211,7 +220,7 @@ public class PowerUI extends SystemUI {
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_USER_SWITCHED);
- mContext.registerReceiver(this, filter, null, mHandler);
+ mBroadcastDispatcher.registerReceiver(this, filter, mHandler);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index d20b22815805..4013586d4197 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -257,10 +257,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mNextAlarmTextView.setSelected(true);
mPermissionsHubEnabled = PrivacyItemControllerKt.isPermissionsHubEnabled();
- // Change the ignored slots when DeviceConfig flag changes
- DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY,
- mContext.getMainExecutor(), mPropertiesListener);
-
}
private List<String> getIgnoredIconSlots() {
@@ -489,6 +485,9 @@ public class QuickStatusBarHeader extends RelativeLayout implements
super.onAttachedToWindow();
mStatusBarIconController.addIconGroup(mIconManager);
requestApplyInsets();
+ // Change the ignored slots when DeviceConfig flag changes
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY,
+ mContext.getMainExecutor(), mPropertiesListener);
}
@Override
@@ -527,6 +526,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
public void onDetachedFromWindow() {
setListening(false);
mStatusBarIconController.removeIconGroup(mIconManager);
+ DeviceConfig.removeOnPropertiesChangedListener(mPropertiesListener);
super.onDetachedFromWindow();
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsOnboarding.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsOnboarding.java
index d0c47345a83a..c1ce16337f8d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsOnboarding.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsOnboarding.java
@@ -61,8 +61,10 @@ import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.TextView;
+import com.android.systemui.Dependency;
import com.android.systemui.Prefs;
import com.android.systemui.R;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.shared.recents.IOverviewProxy;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
@@ -245,10 +247,15 @@ public class RecentsOnboarding {
private final View.OnAttachStateChangeListener mOnAttachStateChangeListener
= new View.OnAttachStateChangeListener() {
+
+ private final BroadcastDispatcher mBroadcastDispatcher = Dependency.get(
+ BroadcastDispatcher.class);
+
@Override
public void onViewAttachedToWindow(View view) {
if (view == mLayout) {
- mContext.registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
+ mBroadcastDispatcher.registerReceiver(mReceiver,
+ new IntentFilter(Intent.ACTION_SCREEN_OFF));
mLayoutAttachedToWindow = true;
if (view.getTag().equals(R.string.recents_swipe_up_onboarding)) {
mHasDismissedSwipeUpTip = false;
@@ -273,7 +280,7 @@ public class RecentsOnboarding {
}
mOverviewOpenedCountSinceQuickScrubTipDismiss = 0;
}
- mContext.unregisterReceiver(mReceiver);
+ mBroadcastDispatcher.unregisterReceiver(mReceiver);
}
}
};
@@ -335,10 +342,11 @@ public class RecentsOnboarding {
private void notifyOnTip(int action, int target) {
try {
IOverviewProxy overviewProxy = mOverviewProxyService.getProxy();
- if(overviewProxy != null) {
+ if (overviewProxy != null) {
overviewProxy.onTip(action, target);
}
- } catch (RemoteException e) {}
+ } catch (RemoteException e) {
+ }
}
public void onNavigationModeChanged(int mode) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
index c3c0d63f66c4..0f277ca8b2c6 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
@@ -47,6 +47,7 @@ import android.widget.TextView;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.WindowManagerWrapper;
import com.android.systemui.statusbar.phone.NavigationBarView;
@@ -159,6 +160,8 @@ public class ScreenPinningRequest implements View.OnClickListener,
private ValueAnimator mColorAnim;
private ViewGroup mLayout;
private boolean mShowCancel;
+ private final BroadcastDispatcher mBroadcastDispatcher =
+ Dependency.get(BroadcastDispatcher.class);
public RequestWindowView(Context context, boolean showCancel) {
super(context);
@@ -212,7 +215,7 @@ public class ScreenPinningRequest implements View.OnClickListener,
IntentFilter filter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
filter.addAction(Intent.ACTION_USER_SWITCHED);
filter.addAction(Intent.ACTION_SCREEN_OFF);
- mContext.registerReceiver(mReceiver, filter);
+ mBroadcastDispatcher.registerReceiver(mReceiver, filter);
}
private void inflateView(int rotation) {
@@ -313,7 +316,7 @@ public class ScreenPinningRequest implements View.OnClickListener,
@Override
public void onDetachedFromWindow() {
- mContext.unregisterReceiver(mReceiver);
+ mBroadcastDispatcher.unregisterReceiver(mReceiver);
}
protected void onConfigurationChanged() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVPluginManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVPluginManager.kt
index 7dcc2fcfe2b2..53601babfd56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVPluginManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVPluginManager.kt
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone
import android.content.Context
import android.view.View
+import android.view.ViewGroup.MarginLayoutParams
import android.widget.FrameLayout
import com.android.systemui.plugins.NPVPlugin
import com.android.systemui.plugins.PluginListener
@@ -36,6 +37,7 @@ class NPVPluginManager(
private var plugin: NPVPlugin? = null
private var animator = createAnimator()
+ private var yOffset = 0f
private fun createAnimator() = TouchAnimator.Builder()
.addFloat(parent, "alpha", 1f, 0f)
@@ -76,7 +78,7 @@ class NPVPluginManager(
}
fun setExpansion(expansion: Float, headerTranslation: Float, heightDiff: Float) {
- parent.setTranslationY(expansion * heightDiff + headerTranslation)
+ parent.setTranslationY(expansion * heightDiff + headerTranslation + yOffset)
if (!expansion.isNaN()) animator.setPosition(expansion)
}
@@ -88,5 +90,13 @@ class NPVPluginManager(
animator = createAnimator()
}
- fun getHeight() = if (plugin != null) parent.height else 0
+ fun getHeight() =
+ if (plugin != null) {
+ parent.height + (parent.getLayoutParams() as MarginLayoutParams).topMargin
+ } else 0
+
+ fun setYOffset(y: Float) {
+ yOffset = y
+ parent.setTranslationY(yOffset)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
index b87140dddec3..6e61d7ceaf6c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
@@ -89,6 +89,7 @@ import com.android.systemui.R;
import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.assist.AssistHandleViewController;
import com.android.systemui.assist.AssistManager;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
import com.android.systemui.model.SysUiState;
@@ -139,6 +140,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
private final MetricsLogger mMetricsLogger;
private final DeviceProvisionedController mDeviceProvisionedController;
private final StatusBarStateController mStatusBarStateController;
+ private final NavigationModeController mNavigationModeController;
protected NavigationBarView mNavigationBarView = null;
@@ -170,6 +172,8 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
private OverviewProxyService mOverviewProxyService;
+ private final BroadcastDispatcher mBroadcastDispatcher;
+
@VisibleForTesting
public int mDisplayId;
private boolean mIsOnDefaultDisplay;
@@ -251,7 +255,8 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
AssistManager assistManager, OverviewProxyService overviewProxyService,
NavigationModeController navigationModeController,
StatusBarStateController statusBarStateController,
- SysUiState sysUiFlagsContainer) {
+ SysUiState sysUiFlagsContainer,
+ BroadcastDispatcher broadcastDispatcher) {
mAccessibilityManagerWrapper = accessibilityManagerWrapper;
mDeviceProvisionedController = deviceProvisionedController;
mStatusBarStateController = statusBarStateController;
@@ -260,7 +265,9 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
mSysUiFlagsContainer = sysUiFlagsContainer;
mAssistantAvailable = mAssistManager.getAssistInfoForUser(UserHandle.USER_CURRENT) != null;
mOverviewProxyService = overviewProxyService;
+ mNavigationModeController = navigationModeController;
mNavBarMode = navigationModeController.addListener(this);
+ mBroadcastDispatcher = broadcastDispatcher;
}
// ----- Fragment Lifecycle Callbacks -----
@@ -299,6 +306,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
@Override
public void onDestroy() {
super.onDestroy();
+ mNavigationModeController.removeListener(this);
mAccessibilityManagerWrapper.removeCallback(mAccessibilityListener);
mContentResolver.unregisterContentObserver(mMagnificationObserver);
mContentResolver.unregisterContentObserver(mAssistContentObserver);
@@ -337,7 +345,8 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_USER_SWITCHED);
- getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null);
+ mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter, Handler.getMain(),
+ UserHandle.ALL);
notifyNavigationBarScreenOn();
mOverviewProxyService.addCallback(mOverviewProxyListener);
@@ -380,7 +389,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
mNavigationBarView.getLightTransitionsController().destroy(getContext());
}
mOverviewProxyService.removeCallback(mOverviewProxyListener);
- getContext().unregisterReceiver(mBroadcastReceiver);
+ mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index fa4812dc4876..a1a47e1305f5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -829,7 +829,11 @@ public class NavigationBarView extends FrameLayout implements
mRecentsOnboarding.onNavigationModeChanged(mNavBarMode);
getRotateSuggestionButton().onNavigationModeChanged(mNavBarMode);
- mRegionSamplingHelper.start(mSamplingBounds);
+ if (isGesturalMode(mNavBarMode)) {
+ mRegionSamplingHelper.start(mSamplingBounds);
+ } else {
+ mRegionSamplingHelper.stop();
+ }
}
public void setAccessibilityButtonState(final boolean visible, final boolean longClickable) {
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 86da10a4b970..353a5381aa14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -652,8 +652,7 @@ public class NotificationPanelView extends PanelView implements
mNotificationStackScroller.setLayoutParams(lp);
}
int sideMargin = res.getDimensionPixelOffset(R.dimen.notification_side_paddings);
- int topMargin =
- res.getDimensionPixelOffset(com.android.internal.R.dimen.quick_qs_total_height);
+ int topMargin = sideMargin;
lp = (FrameLayout.LayoutParams) mPluginFrame.getLayoutParams();
if (lp.width != qsWidth || lp.gravity != panelGravity || lp.leftMargin != sideMargin
|| lp.rightMargin != sideMargin || lp.topMargin != topMargin) {
@@ -796,6 +795,7 @@ public class NotificationPanelView extends PanelView implements
int oldMaxHeight = mQsMaxExpansionHeight;
if (mQs != null) {
mQsMinExpansionHeight = mKeyguardShowing ? 0 : mQs.getQsMinExpansionHeight();
+ mNPVPluginManager.setYOffset(mQsMinExpansionHeight);
mQsMinExpansionHeight += mNPVPluginManager.getHeight();
mQsMaxExpansionHeight = mQs.getDesiredHeight();
mNotificationStackScroller.setMaxTopPadding(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java
index c1ff572bb210..1a6b415f87db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java
@@ -127,6 +127,11 @@ public class RegionSamplingHelper implements View.OnAttachStateChangeListener,
updateSamplingListener();
}
+ void stopAndDestroy() {
+ stop();
+ mSamplingListener.destroy();
+ }
+
@Override
public void onViewAttachedToWindow(View view) {
updateSamplingListener();
@@ -134,9 +139,7 @@ public class RegionSamplingHelper implements View.OnAttachStateChangeListener,
@Override
public void onViewDetachedFromWindow(View view) {
- // isAttachedToWindow is only changed after this call to the listeners, so let's post it
- // instead
- postUpdateSamplingListener();
+ stopAndDestroy();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 377d13860185..7bab7f159b7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -147,6 +147,7 @@ import com.android.systemui.SystemUIFactory;
import com.android.systemui.UiOffloadThread;
import com.android.systemui.appops.AppOpsController;
import com.android.systemui.assist.AssistManager;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.charging.WirelessChargingAnimation;
import com.android.systemui.classifier.FalsingLog;
@@ -223,7 +224,6 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceP
import com.android.systemui.statusbar.policy.ExtensionController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.KeyguardStateControllerImpl;
import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
import com.android.systemui.statusbar.policy.NetworkController;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
@@ -394,6 +394,9 @@ public class StatusBar extends SystemUI implements DemoMode,
@Inject
protected NotifPipelineInitializer mNotifPipelineInitializer;
+ @VisibleForTesting
+ BroadcastDispatcher mBroadcastDispatcher;
+
// expanded notifications
protected NotificationPanelView mNotificationPanel; // the sliding/resizing panel within the notification window
@@ -668,6 +671,7 @@ public class StatusBar extends SystemUI implements DemoMode,
mBubbleController = Dependency.get(BubbleController.class);
mBubbleController.setExpandListener(mBubbleExpandListener);
mActivityIntentHelper = new ActivityIntentHelper(mContext);
+ mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
KeyguardSliceProvider sliceProvider = KeyguardSliceProvider.getAttachedInstance();
if (sliceProvider != null) {
sliceProvider.initDependencies(mMediaManager, mStatusBarStateController,
@@ -1049,11 +1053,7 @@ public class StatusBar extends SystemUI implements DemoMode,
}
// receive broadcasts
- IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
- filter.addAction(Intent.ACTION_SCREEN_OFF);
- filter.addAction(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG);
- context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null);
+ registerBroadcastReceiver();
IntentFilter demoFilter = new IntentFilter();
if (DEBUG_MEDIA_FAKE_ARTWORK) {
@@ -1074,6 +1074,15 @@ public class StatusBar extends SystemUI implements DemoMode,
ThreadedRenderer.overrideProperty("ambientRatio", String.valueOf(1.5f));
}
+ @VisibleForTesting
+ protected void registerBroadcastReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ filter.addAction(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG);
+ mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter, null, UserHandle.ALL);
+ }
+
protected QS createDefaultQSFragment() {
return FragmentHostManager.get(mStatusBarWindow).create(QSFragment.class);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 5bda34d64b73..ce929b7c621b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -28,6 +28,7 @@ import android.view.WindowManager.LayoutParams;
import com.android.systemui.Dependency;
import com.android.systemui.R;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -138,20 +139,21 @@ public class SystemUIDialog extends AlertDialog {
private final Dialog mDialog;
private boolean mRegistered;
+ private final BroadcastDispatcher mBroadcastDispatcher;
DismissReceiver(Dialog dialog) {
mDialog = dialog;
+ mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
}
void register() {
- mDialog.getContext()
- .registerReceiverAsUser(this, UserHandle.CURRENT, INTENT_FILTER, null, null);
+ mBroadcastDispatcher.registerReceiver(this, INTENT_FILTER, null, UserHandle.CURRENT);
mRegistered = true;
}
void unregister() {
if (mRegistered) {
- mDialog.getContext().unregisterReceiver(this);
+ mBroadcastDispatcher.unregisterReceiver(this);
mRegistered = false;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
index c2c3f81527e8..b331fc3bf0ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.policy;
+import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
+
import android.app.StatusBarManager;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -44,6 +46,7 @@ import com.android.systemui.Dependency;
import com.android.systemui.FontSizeUtils;
import com.android.systemui.R;
import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
import com.android.systemui.settings.CurrentUserTracker;
@@ -60,6 +63,9 @@ import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
+import javax.inject.Inject;
+import javax.inject.Named;
+
/**
* Digital clock for the status bar.
*/
@@ -107,15 +113,20 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C
*/
private int mNonAdaptedColor;
- public Clock(Context context) {
- this(context, null);
- }
+ private final BroadcastDispatcher mBroadcastDispatcher;
public Clock(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
+ this(context, attrs, null);
+ }
+
+ @Inject
+ public Clock(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs,
+ BroadcastDispatcher broadcastDispatcher) {
+ this(context, attrs, 0, broadcastDispatcher);
}
- public Clock(Context context, AttributeSet attrs, int defStyle) {
+ public Clock(Context context, AttributeSet attrs, int defStyle,
+ BroadcastDispatcher broadcastDispatcher) {
super(context, attrs, defStyle);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
@@ -134,6 +145,7 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C
mCurrentUserId = newUserId;
}
};
+ mBroadcastDispatcher = broadcastDispatcher;
}
@Override
@@ -358,11 +370,11 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C
}
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
- mContext.registerReceiver(mScreenReceiver, filter);
+ mBroadcastDispatcher.registerReceiver(mScreenReceiver, filter);
}
} else {
if (mSecondsHandler != null) {
- mContext.unregisterReceiver(mScreenReceiver);
+ mBroadcastDispatcher.unregisterReceiver(mScreenReceiver);
mSecondsHandler.removeCallbacks(mSecondTick);
mSecondsHandler = null;
updateClock();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
index f8c7532ec281..cc91bc082871 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -101,7 +101,9 @@ public class KeyguardStateControllerImpl extends KeyguardUpdateMonitorCallback
@Override
public void addCallback(@NonNull Callback callback) {
Preconditions.checkNotNull(callback, "Callback must not be null. b/128895449");
- mCallbacks.add(callback);
+ if (!mCallbacks.contains(callback)) {
+ mCallbacks.add(callback);
+ }
if (mCallbacks.size() != 0 && !mListening) {
mListening = true;
mKeyguardUpdateMonitor.registerCallback(this);
diff --git a/packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java b/packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java
index e44e58a84dc8..7e801da9cd1b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java
+++ b/packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java
@@ -37,6 +37,7 @@ import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.phone.LockIcon;
import com.android.systemui.statusbar.phone.NotificationPanelView;
+import com.android.systemui.statusbar.policy.Clock;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -178,6 +179,11 @@ public class InjectionInflationController {
* Creates the QSCustomizer.
*/
QSCustomizer createQSCustomizer();
+
+ /**
+ * Creates a Clock.
+ */
+ Clock createClock();
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index a6b5b38fd728..edea92f5952a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -59,6 +59,7 @@ import com.android.settingslib.volume.MediaSessions;
import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.plugins.VolumeDialogController;
import com.android.systemui.qs.tiles.DndTile;
@@ -137,9 +138,10 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
private UserActivityListener mUserActivityListener;
protected final VC mVolumeController = new VC();
+ protected final BroadcastDispatcher mBroadcastDispatcher;
@Inject
- public VolumeDialogControllerImpl(Context context) {
+ public VolumeDialogControllerImpl(Context context, BroadcastDispatcher broadcastDispatcher) {
mContext = context.getApplicationContext();
mNotificationManager = (NotificationManager) mContext.getSystemService(
Context.NOTIFICATION_SERVICE);
@@ -152,6 +154,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
mAudio = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
mNoMan = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
mObserver = new SettingObserver(mWorker);
+ mBroadcastDispatcher = broadcastDispatcher;
mObserver.init();
mReceiver.init();
mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
@@ -1004,11 +1007,11 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
- mContext.registerReceiver(this, filter, null, mWorker);
+ mBroadcastDispatcher.registerReceiver(this, filter, mWorker);
}
public void destroy() {
- mContext.unregisterReceiver(this);
+ mBroadcastDispatcher.unregisterReceiver(this);
}
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/broadcast/UserBroadcastDispatcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/broadcast/UserBroadcastDispatcherTest.kt
index 011c2cd57588..e838d9e94a31 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/broadcast/UserBroadcastDispatcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/broadcast/UserBroadcastDispatcherTest.kt
@@ -70,6 +70,8 @@ class UserBroadcastDispatcherTest : SysuiTestCase() {
private lateinit var mockContext: Context
@Mock
private lateinit var mockHandler: Handler
+ @Mock
+ private lateinit var mPendingResult: BroadcastReceiver.PendingResult
@Captor
private lateinit var argumentCaptor: ArgumentCaptor<IntentFilter>
@@ -88,6 +90,7 @@ class UserBroadcastDispatcherTest : SysuiTestCase() {
universalBroadcastReceiver = UserBroadcastDispatcher(
mockContext, USER_ID, handler, testableLooper.looper)
+ universalBroadcastReceiver.pendingResult = mPendingResult
}
@Test
@@ -227,4 +230,19 @@ class UserBroadcastDispatcherTest : SysuiTestCase() {
verify(broadcastReceiver).onReceive(mockContext, intent)
verify(broadcastReceiverOther).onReceive(mockContext, intent)
}
+
+ @Test
+ fun testPendingResult() {
+ intentFilter = IntentFilter(ACTION_1)
+ universalBroadcastReceiver.registerReceiver(
+ ReceiverData(broadcastReceiver, intentFilter, handler, USER_HANDLE))
+
+ val intent = Intent(ACTION_1)
+ universalBroadcastReceiver.onReceive(mockContext, intent)
+
+ testableLooper.processAllMessages()
+
+ verify(broadcastReceiver).onReceive(mockContext, intent)
+ verify(broadcastReceiver).pendingResult = mPendingResult
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
index 4d95f3f474b5..4958c649d532 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
@@ -19,6 +19,7 @@ import static android.provider.Settings.Global.SHOW_USB_TEMPERATURE_ALARM;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.anyObject;
import static org.mockito.Mockito.mock;
@@ -27,8 +28,11 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.IntentFilter;
import android.os.BatteryManager;
+import android.os.Handler;
import android.os.IThermalEventListener;
import android.os.IThermalService;
import android.os.PowerManager;
@@ -43,6 +47,7 @@ import android.testing.TestableResources;
import com.android.settingslib.fuelgauge.Estimate;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.power.PowerUI.WarningsUI;
import com.android.systemui.statusbar.phone.StatusBar;
@@ -80,6 +85,7 @@ public class PowerUITest extends SysuiTestCase {
@Mock private IThermalService mThermalServiceMock;
private IThermalEventListener mUsbThermalEventListener;
private IThermalEventListener mSkinThermalEventListener;
+ @Mock private BroadcastDispatcher mBroadcastDispatcher;
@Before
public void setup() {
@@ -96,6 +102,15 @@ public class PowerUITest extends SysuiTestCase {
}
@Test
+ public void testReceiverIsRegisteredToDispatcherOnStart() {
+ mPowerUI.start();
+ verify(mBroadcastDispatcher).registerReceiver(
+ any(BroadcastReceiver.class),
+ any(IntentFilter.class),
+ any(Handler.class)); //PowerUI does not call with User
+ }
+
+ @Test
public void testSkinWarning_throttlingCritical() throws Exception {
mPowerUI.start();
@@ -667,7 +682,7 @@ public class PowerUITest extends SysuiTestCase {
}
private void createPowerUi() {
- mPowerUI = new PowerUI();
+ mPowerUI = new PowerUI(mBroadcastDispatcher);
mPowerUI.mContext = mContext;
mPowerUI.mComponents = mContext.getComponents();
mPowerUI.mThermalService = mThermalServiceMock;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java
index 33d3ac848f0f..0bff5aa9e991 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java
@@ -24,9 +24,11 @@ import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.LayoutRes;
@@ -34,11 +36,14 @@ import android.annotation.Nullable;
import android.app.Fragment;
import android.app.FragmentController;
import android.app.FragmentHostCallback;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.IntentFilter;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import android.os.UserHandle;
import android.testing.AndroidTestingRunner;
import android.testing.LeakCheck.Tracker;
import android.testing.TestableLooper;
@@ -58,6 +63,7 @@ import com.android.systemui.Dependency;
import com.android.systemui.SysuiBaseFragmentTest;
import com.android.systemui.SysuiTestableContext;
import com.android.systemui.assist.AssistManager;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.model.SysUiState;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.OverviewProxyService;
@@ -70,6 +76,8 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
@RunWithLooper()
@@ -85,6 +93,8 @@ public class NavigationBarFragmentTest extends SysuiBaseFragmentTest {
private OverviewProxyService mOverviewProxyService;
private CommandQueue mCommandQueue;
private SysUiState mMockSysUiState;
+ @Mock
+ private BroadcastDispatcher mBroadcastDispatcher;
private AccessibilityManagerWrapper mAccessibilityWrapper =
new AccessibilityManagerWrapper(mContext) {
@@ -112,6 +122,8 @@ public class NavigationBarFragmentTest extends SysuiBaseFragmentTest {
@Before
public void setupFragment() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
setupSysuiDependency();
createRootView();
mOverviewProxyService =
@@ -177,6 +189,18 @@ public class NavigationBarFragmentTest extends SysuiBaseFragmentTest {
}
@Test
+ public void testRegisteredWithDispatcher() {
+ mFragments.dispatchResume();
+ processAllMessages();
+
+ verify(mBroadcastDispatcher).registerReceiver(
+ any(BroadcastReceiver.class),
+ any(IntentFilter.class),
+ any(Handler.class),
+ any(UserHandle.class));
+ }
+
+ @Test
public void testSetImeWindowStatusWhenImeSwitchOnDisplay() {
// Create default & external NavBar fragment.
NavigationBarFragment defaultNavBar = (NavigationBarFragment) mFragment;
@@ -227,7 +251,8 @@ public class NavigationBarFragmentTest extends SysuiBaseFragmentTest {
mOverviewProxyService,
mock(NavigationModeController.class),
mock(StatusBarStateController.class),
- mMockSysUiState);
+ mMockSysUiState,
+ mBroadcastDispatcher);
}
private class HostCallbacksForExternalDisplay extends
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index 3be71c07009d..b75cb8cf487c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -41,7 +41,9 @@ import static org.mockito.Mockito.when;
import android.app.Notification;
import android.app.StatusBarManager;
import android.app.trust.TrustManager;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.IntentFilter;
import android.hardware.display.AmbientDisplayConfiguration;
import android.hardware.fingerprint.FingerprintManager;
import android.metrics.LogMaker;
@@ -51,6 +53,7 @@ import android.os.IPowerManager;
import android.os.Looper;
import android.os.PowerManager;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.service.dreams.IDreamManager;
import android.support.test.metricshelper.MetricsAsserts;
import android.testing.AndroidTestingRunner;
@@ -75,6 +78,7 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.UiOffloadThread;
import com.android.systemui.appops.AppOpsController;
import com.android.systemui.assist.AssistManager;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.doze.DozeLog;
@@ -171,6 +175,8 @@ public class StatusBarTest extends SysuiTestCase {
private AmbientDisplayConfiguration mAmbientDisplayConfiguration;
@Mock
private StatusBarWindowView mStatusBarWindowView;
+ @Mock
+ private BroadcastDispatcher mBroadcastDispatcher;
private TestableStatusBar mStatusBar;
private FakeMetricsLogger mMetricsLogger;
@@ -199,6 +205,7 @@ public class StatusBarTest extends SysuiTestCase {
mDependency.injectTestDependency(NotificationFilter.class, mNotificationFilter);
mDependency.injectTestDependency(NotificationAlertingManager.class,
mNotificationAlertingManager);
+ mDependency.injectTestDependency(BroadcastDispatcher.class, mBroadcastDispatcher);
IPowerManager powerManagerService = mock(IPowerManager.class);
mPowerManager = new PowerManager(mContext, powerManagerService,
@@ -263,7 +270,8 @@ public class StatusBarTest extends SysuiTestCase {
mDozeScrimController, mock(NotificationShelf.class),
mLockscreenUserManager, mCommandQueue, mNotificationPresenter,
mock(BubbleController.class), mock(NavigationBarController.class),
- mock(AutoHideController.class), mKeyguardUpdateMonitor, mStatusBarWindowView);
+ mock(AutoHideController.class), mKeyguardUpdateMonitor, mStatusBarWindowView,
+ mBroadcastDispatcher);
mStatusBar.mContext = mContext;
mStatusBar.mComponents = mContext.getComponents();
SystemUIFactory.getInstance().getRootComponent()
@@ -774,6 +782,16 @@ public class StatusBarTest extends SysuiTestCase {
verify(mNotificationPanelView, never()).expand(anyBoolean());
}
+ @Test
+ public void testRegisterBroadcastsonDispatcher() {
+ mStatusBar.registerBroadcastReceiver();
+ verify(mBroadcastDispatcher).registerReceiver(
+ any(BroadcastReceiver.class),
+ any(IntentFilter.class),
+ eq(null),
+ any(UserHandle.class));
+ }
+
static class TestableStatusBar extends StatusBar {
public TestableStatusBar(StatusBarKeyguardViewManager man,
KeyguardIndicationController key,
@@ -801,7 +819,8 @@ public class StatusBarTest extends SysuiTestCase {
NavigationBarController navBarController,
AutoHideController autoHideController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
- StatusBarWindowView statusBarWindow) {
+ StatusBarWindowView statusBarWindow,
+ BroadcastDispatcher broadcastDispatcher) {
mStatusBarKeyguardViewManager = man;
mKeyguardIndicationController = key;
mStackScroller = stack;
@@ -835,6 +854,7 @@ public class StatusBarTest extends SysuiTestCase {
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mStatusBarWindow = statusBarWindow;
mDozeServiceHost.mWakeLockScreenPerformsAuth = false;
+ mBroadcastDispatcher = broadcastDispatcher;
}
private WakefulnessLifecycle createAwakeWakefulnessLifecycle() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
index a9a1392fb80b..589aa0353870 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
@@ -18,11 +18,9 @@ import static junit.framework.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import android.content.BroadcastReceiver;
-import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.testing.AndroidTestingRunner;
@@ -31,11 +29,14 @@ import android.testing.TestableLooper.RunWithLooper;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@@ -43,13 +44,16 @@ import org.mockito.ArgumentCaptor;
public class SystemUIDialogTest extends SysuiTestCase {
private SystemUIDialog mDialog;
-
- Context mContextSpy;
+ @Mock
+ private BroadcastDispatcher mBroadcastDispatcher;
@Before
public void setup() {
- mContextSpy = spy(mContext);
- mDialog = new SystemUIDialog(mContextSpy);
+ MockitoAnnotations.initMocks(this);
+
+ mDependency.injectTestDependency(BroadcastDispatcher.class, mBroadcastDispatcher);
+
+ mDialog = new SystemUIDialog(mContext);
}
@Test
@@ -60,12 +64,12 @@ public class SystemUIDialogTest extends SysuiTestCase {
ArgumentCaptor.forClass(IntentFilter.class);
mDialog.show();
- verify(mContextSpy).registerReceiverAsUser(broadcastReceiverCaptor.capture(), any(),
- intentFilterCaptor.capture(), any(), any());
+ verify(mBroadcastDispatcher).registerReceiver(broadcastReceiverCaptor.capture(),
+ intentFilterCaptor.capture(), eq(null), any());
assertTrue(intentFilterCaptor.getValue().hasAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
mDialog.dismiss();
- verify(mContextSpy).unregisterReceiver(eq(broadcastReceiverCaptor.getValue()));
+ verify(mBroadcastDispatcher).unregisterReceiver(eq(broadcastReceiverCaptor.getValue()));
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index f4d0854b2c9f..2e945f2481d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -16,41 +16,64 @@
package com.android.systemui.volume;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.IntentFilter;
import android.media.AudioManager;
import android.media.session.MediaSession;
+import android.os.Handler;
+import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.statusbar.phone.StatusBar;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+@RunWith(AndroidTestingRunner.class)
@SmallTest
public class VolumeDialogControllerImplTest extends SysuiTestCase {
TestableVolumeDialogControllerImpl mVolumeController;
VolumeDialogControllerImpl.C mCallback;
StatusBar mStatusBar;
+ @Mock
+ private BroadcastDispatcher mBroadcastDispatcher;
@Before
public void setup() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
mCallback = mock(VolumeDialogControllerImpl.C.class);
mStatusBar = mock(StatusBar.class);
- mVolumeController = new TestableVolumeDialogControllerImpl(mContext, mCallback, mStatusBar);
+ mVolumeController = new TestableVolumeDialogControllerImpl(mContext, mCallback, mStatusBar,
+ mBroadcastDispatcher);
mVolumeController.setEnableDialogs(true, true);
}
@Test
+ public void testRegisteredWithDispatcher() {
+ verify(mBroadcastDispatcher).registerReceiver(
+ any(BroadcastReceiver.class),
+ any(IntentFilter.class),
+ any(Handler.class)); // VolumeDialogControllerImpl does not call with user
+ }
+
+ @Test
public void testVolumeChangeW_deviceNotInteractiveAOD() {
when(mStatusBar.isDeviceInteractive()).thenReturn(false);
when(mStatusBar.getWakefulnessState()).thenReturn(WakefulnessLifecycle.WAKEFULNESS_AWAKE);
@@ -81,7 +104,7 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase {
public void testVolumeChangeW_nullStatusBar() {
VolumeDialogControllerImpl.C callback = mock(VolumeDialogControllerImpl.C.class);
TestableVolumeDialogControllerImpl nullStatusBarTestableDialog = new
- TestableVolumeDialogControllerImpl(mContext, callback, null);
+ TestableVolumeDialogControllerImpl(mContext, callback, null, mBroadcastDispatcher);
nullStatusBarTestableDialog.setEnableDialogs(true, true);
nullStatusBarTestableDialog.onVolumeChangedW(0, AudioManager.FLAG_SHOW_UI);
verify(callback, times(1)).onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED);
@@ -100,8 +123,9 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase {
}
static class TestableVolumeDialogControllerImpl extends VolumeDialogControllerImpl {
- public TestableVolumeDialogControllerImpl(Context context, C callback, StatusBar s) {
- super(context);
+ TestableVolumeDialogControllerImpl(Context context, C callback, StatusBar s,
+ BroadcastDispatcher broadcastDispatcher) {
+ super(context, broadcastDispatcher);
mCallbacks = callback;
mStatusBar = s;
}
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index c412ebdc9033..5947c3599ca8 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -1193,7 +1193,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub {
public void notifyCarrierNetworkChange(boolean active) {
// only CarrierService with carrier privilege rule should have the permission
int[] subIds = Arrays.stream(SubscriptionManager.from(mContext)
- .getActiveSubscriptionIdList())
+ .getActiveSubscriptionIdList(false))
.filter(i -> TelephonyPermissions.checkCarrierPrivilegeForSubId(i)).toArray();
if (ArrayUtils.isEmpty(subIds)) {
loge("notifyCarrierNetworkChange without carrier privilege");
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c46738df1f52..7b69bea6014b 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -8311,6 +8311,8 @@ public class ActivityManagerService extends IActivityManager.Stub
triggerShellBugreport.setAction(INTENT_BUGREPORT_REQUESTED);
triggerShellBugreport.setPackage(SHELL_APP_PACKAGE);
triggerShellBugreport.putExtra(EXTRA_BUGREPORT_TYPE, bugreportType);
+ triggerShellBugreport.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ triggerShellBugreport.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
if (shareTitle != null) {
triggerShellBugreport.putExtra(EXTRA_TITLE, shareTitle);
}
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index 33d8dec8b043..8e09d0e5958e 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -41,7 +41,8 @@ public class PlatformCompat extends IPlatformCompat.Stub {
public PlatformCompat(Context context) {
mContext = context;
- mChangeReporter = new ChangeReporter();
+ mChangeReporter = new ChangeReporter(
+ StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__SOURCE__SYSTEM_SERVER);
}
@Override
@@ -96,10 +97,6 @@ public class PlatformCompat extends IPlatformCompat.Stub {
private void reportChange(long changeId, ApplicationInfo appInfo, int state) {
int uid = appInfo.uid;
- //TODO(b/138374585): Implement rate limiting for the logs.
- Slog.d(TAG, ChangeReporter.createLogString(uid, changeId, state));
- mChangeReporter.reportChange(uid, changeId,
- state, /* source */
- StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__SOURCE__SYSTEM_SERVER);
+ mChangeReporter.reportChange(uid, changeId, state);
}
}
diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
index e8a577938b4c..88a7077ac37a 100644
--- a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
+++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
@@ -64,7 +64,13 @@ public class DisplayWhiteBalanceController implements
AmbientFilter mColorTemperatureFilter;
private DisplayWhiteBalanceThrottler mThrottler;
+ // In low brightness conditions the ALS readings are more noisy and produce
+ // high errors. This default is introduced to provide a fixed display color
+ // temperature when sensor readings become unreliable.
private final float mLowLightAmbientColorTemperature;
+ // In high brightness conditions certain color temperatures can cause peak display
+ // brightness to drop. This fixed color temperature can be used to compensate for
+ // this effect.
private final float mHighLightAmbientColorTemperature;
private float mAmbientColorTemperature;
@@ -85,12 +91,14 @@ public class DisplayWhiteBalanceController implements
// A piecewise linear relationship between ambient and display color temperatures.
private Spline.LinearSpline mAmbientToDisplayColorTemperatureSpline;
- // In very low or very high brightness conditions ambient EQ should to set to a default
- // instead of using mAmbientToDisplayColorTemperatureSpline. However, setting ambient EQ
- // based on thresholds can cause the display to rapidly change color temperature. To solve
- // this, mLowLightAmbientBrightnessToBiasSpline and mHighLightAmbientBrightnessToBiasSpline
- // are used to smoothly interpolate from ambient color temperature to the defaults.
- // A piecewise linear relationship between low light brightness and low light bias.
+ // In very low or very high brightness conditions Display White Balance should
+ // be to set to a default instead of using mAmbientToDisplayColorTemperatureSpline.
+ // However, setting Display White Balance based on thresholds can cause the
+ // display to rapidly change color temperature. To solve this,
+ // mLowLightAmbientBrightnessToBiasSpline and
+ // mHighLightAmbientBrightnessToBiasSpline are used to smoothly interpolate from
+ // ambient color temperature to the defaults. A piecewise linear relationship
+ // between low light brightness and low light bias.
private Spline.LinearSpline mLowLightAmbientBrightnessToBiasSpline;
// A piecewise linear relationship between high light brightness and high light bias.
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 248351ca3d2f..0aee8507d5af 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -456,6 +456,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
return;
}
mDestroyed = true;
+ mPlaybackState = null;
mHandler.post(MessageHandler.MSG_DESTROYED);
}
}
diff --git a/services/core/java/com/android/server/tv/TvRemoteProviderProxy.java b/services/core/java/com/android/server/tv/TvRemoteProviderProxy.java
index f8ffb7c1c0e2..b7bc77dc97ee 100644
--- a/services/core/java/com/android/server/tv/TvRemoteProviderProxy.java
+++ b/services/core/java/com/android/server/tv/TvRemoteProviderProxy.java
@@ -21,8 +21,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.media.tv.ITvRemoteProvider;
-import android.media.tv.ITvRemoteServiceInput;
-import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -30,44 +28,33 @@ import android.util.Log;
import android.util.Slog;
import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
/**
* Maintains a connection to a tv remote provider service.
*/
final class TvRemoteProviderProxy implements ServiceConnection {
- private static final String TAG = "TvRemoteProvProxy"; // max. 23 chars
+ private static final String TAG = "TvRemoteProviderProxy";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
- private static final boolean DEBUG_KEY = false;
// This should match TvRemoteProvider.ACTION_TV_REMOTE_PROVIDER
protected static final String SERVICE_INTERFACE =
"com.android.media.tv.remoteprovider.TvRemoteProvider";
private final Context mContext;
+ private final Object mLock;
private final ComponentName mComponentName;
private final int mUserId;
private final int mUid;
- /**
- * State guarded by mLock.
- * This is the first lock in sequence for an incoming call.
- * The second lock is always {@link TvRemoteService#mLock}
- *
- * There are currently no methods that break this sequence.
- */
- private final Object mLock = new Object();
-
- private ProviderMethods mProviderMethods;
- // Connection state
+ // State changes happen only in the main thread, hence no lock is needed
private boolean mRunning;
private boolean mBound;
- private Connection mActiveConnection;
+ private boolean mConnected;
- TvRemoteProviderProxy(Context context, ProviderMethods provider,
+ TvRemoteProviderProxy(Context context, Object lock,
ComponentName componentName, int userId, int uid) {
mContext = context;
- mProviderMethods = provider;
+ mLock = lock;
mComponentName = componentName;
mUserId = userId;
mUid = uid;
@@ -78,7 +65,7 @@ final class TvRemoteProviderProxy implements ServiceConnection {
pw.println(prefix + " mUserId=" + mUserId);
pw.println(prefix + " mRunning=" + mRunning);
pw.println(prefix + " mBound=" + mBound);
- pw.println(prefix + " mActiveConnection=" + mActiveConnection);
+ pw.println(prefix + " mConnected=" + mConnected);
}
public boolean hasComponentName(String packageName, String className) {
@@ -109,11 +96,9 @@ final class TvRemoteProviderProxy implements ServiceConnection {
}
public void rebindIfDisconnected() {
- synchronized (mLock) {
- if (mActiveConnection == null && mRunning) {
- unbind();
- bind();
- }
+ if (mRunning && !mConnected) {
+ unbind();
+ bind();
}
}
@@ -129,7 +114,7 @@ final class TvRemoteProviderProxy implements ServiceConnection {
mBound = mContext.bindServiceAsUser(service, this,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
new UserHandle(mUserId));
- if (!mBound && DEBUG) {
+ if (DEBUG && !mBound) {
Slog.d(TAG, this + ": Bind failed");
}
} catch (SecurityException ex) {
@@ -147,7 +132,6 @@ final class TvRemoteProviderProxy implements ServiceConnection {
}
mBound = false;
- disconnect();
mContext.unbindService(this);
}
}
@@ -158,392 +142,27 @@ final class TvRemoteProviderProxy implements ServiceConnection {
Slog.d(TAG, this + ": onServiceConnected()");
}
- if (mBound) {
- disconnect();
+ mConnected = true;
- ITvRemoteProvider provider = ITvRemoteProvider.Stub.asInterface(service);
- if (provider != null) {
- Connection connection = new Connection(provider);
- if (connection.register()) {
- synchronized (mLock) {
- mActiveConnection = connection;
- }
- if (DEBUG) {
- Slog.d(TAG, this + ": Connected successfully.");
- }
- } else {
- if (DEBUG) {
- Slog.d(TAG, this + ": Registration failed");
- }
- }
- } else {
- Slog.e(TAG, this + ": Service returned invalid remote-control provider binder");
- }
+ final ITvRemoteProvider provider = ITvRemoteProvider.Stub.asInterface(service);
+ if (provider == null) {
+ Slog.e(TAG, this + ": Invalid binder");
+ return;
}
- }
- @Override
- public void onServiceDisconnected(ComponentName name) {
- if (DEBUG) Slog.d(TAG, this + ": Service disconnected");
- disconnect();
- }
-
- private void disconnect() {
- synchronized (mLock) {
- if (mActiveConnection != null) {
- mActiveConnection.dispose();
- mActiveConnection = null;
- }
+ try {
+ provider.setRemoteServiceInputSink(new TvRemoteServiceInput(mLock, provider));
+ } catch (RemoteException e) {
+ Slog.e(TAG, this + ": Failed remote call to setRemoteServiceInputSink");
}
}
- interface ProviderMethods {
- // InputBridge
- boolean openInputBridge(TvRemoteProviderProxy provider, IBinder token, String name,
- int width, int height, int maxPointers);
-
- void closeInputBridge(TvRemoteProviderProxy provider, IBinder token);
-
- void clearInputBridge(TvRemoteProviderProxy provider, IBinder token);
-
- void sendKeyDown(TvRemoteProviderProxy provider, IBinder token, int keyCode);
-
- void sendKeyUp(TvRemoteProviderProxy provider, IBinder token, int keyCode);
-
- void sendPointerDown(TvRemoteProviderProxy provider, IBinder token, int pointerId, int x,
- int y);
-
- void sendPointerUp(TvRemoteProviderProxy provider, IBinder token, int pointerId);
-
- void sendPointerSync(TvRemoteProviderProxy provider, IBinder token);
- }
-
- private final class Connection {
- private final ITvRemoteProvider mTvRemoteProvider;
- private final RemoteServiceInputProvider mServiceInputProvider;
-
- public Connection(ITvRemoteProvider provider) {
- mTvRemoteProvider = provider;
- mServiceInputProvider = new RemoteServiceInputProvider(this);
- }
-
- public boolean register() {
- if (DEBUG) Slog.d(TAG, "Connection::register()");
- try {
- mTvRemoteProvider.setRemoteServiceInputSink(mServiceInputProvider);
- return true;
- } catch (RemoteException ex) {
- dispose();
- return false;
- }
- }
-
- public void dispose() {
- if (DEBUG) Slog.d(TAG, "Connection::dispose()");
- mServiceInputProvider.dispose();
- }
-
-
- public void onInputBridgeConnected(IBinder token) {
- if (DEBUG) Slog.d(TAG, this + ": onInputBridgeConnected");
- try {
- mTvRemoteProvider.onInputBridgeConnected(token);
- } catch (RemoteException ex) {
- Slog.e(TAG, "Failed to deliver onInputBridgeConnected. ", ex);
- }
- }
-
- void openInputBridge(final IBinder token, final String name, final int width,
- final int height, final int maxPointers) {
- synchronized (mLock) {
- if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
- if (DEBUG) {
- Slog.d(TAG, this + ": openInputBridge," +
- " token=" + token + ", name=" + name);
- }
- final long idToken = Binder.clearCallingIdentity();
- try {
- if (mProviderMethods.openInputBridge(TvRemoteProviderProxy.this, token,
- name, width, height, maxPointers)) {
- onInputBridgeConnected(token);
- }
- } finally {
- Binder.restoreCallingIdentity(idToken);
- }
- } else {
- if (DEBUG) {
- Slog.w(TAG,
- "openInputBridge, Invalid connection or incorrect uid: " + Binder
- .getCallingUid());
- }
- }
- }
- }
-
- void closeInputBridge(final IBinder token) {
- synchronized (mLock) {
- if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
- if (DEBUG) {
- Slog.d(TAG, this + ": closeInputBridge," +
- " token=" + token);
- }
- final long idToken = Binder.clearCallingIdentity();
- try {
- mProviderMethods.closeInputBridge(TvRemoteProviderProxy.this, token);
- } finally {
- Binder.restoreCallingIdentity(idToken);
- }
- } else {
- if (DEBUG) {
- Slog.w(TAG,
- "closeInputBridge, Invalid connection or incorrect uid: " +
- Binder.getCallingUid());
- }
- }
- }
- }
-
- void clearInputBridge(final IBinder token) {
- synchronized (mLock) {
- if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
- if (DEBUG) {
- Slog.d(TAG, this + ": clearInputBridge," +
- " token=" + token);
- }
- final long idToken = Binder.clearCallingIdentity();
- try {
- mProviderMethods.clearInputBridge(TvRemoteProviderProxy.this, token);
- } finally {
- Binder.restoreCallingIdentity(idToken);
- }
- } else {
- if (DEBUG) {
- Slog.w(TAG,
- "clearInputBridge, Invalid connection or incorrect uid: " +
- Binder.getCallingUid());
- }
- }
- }
- }
-
- void sendTimestamp(final IBinder token, final long timestamp) {
- if (DEBUG) {
- Slog.e(TAG, "sendTimestamp is deprecated, please remove all usages of this API.");
- }
- }
-
- void sendKeyDown(final IBinder token, final int keyCode) {
- synchronized (mLock) {
- if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
- if (DEBUG_KEY) {
- Slog.d(TAG, this + ": sendKeyDown," +
- " token=" + token + ", keyCode=" + keyCode);
- }
- final long idToken = Binder.clearCallingIdentity();
- try {
- mProviderMethods.sendKeyDown(TvRemoteProviderProxy.this, token, keyCode);
- } finally {
- Binder.restoreCallingIdentity(idToken);
- }
- } else {
- if (DEBUG) {
- Slog.w(TAG,
- "sendKeyDown, Invalid connection or incorrect uid: " + Binder
- .getCallingUid());
- }
- }
- }
- }
-
- void sendKeyUp(final IBinder token, final int keyCode) {
- synchronized (mLock) {
- if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
- if (DEBUG_KEY) {
- Slog.d(TAG, this + ": sendKeyUp," +
- " token=" + token + ", keyCode=" + keyCode);
- }
- final long idToken = Binder.clearCallingIdentity();
- try {
- mProviderMethods.sendKeyUp(TvRemoteProviderProxy.this, token, keyCode);
- } finally {
- Binder.restoreCallingIdentity(idToken);
- }
- } else {
- if (DEBUG) {
- Slog.w(TAG,
- "sendKeyUp, Invalid connection or incorrect uid: " + Binder
- .getCallingUid());
- }
- }
- }
- }
-
- void sendPointerDown(final IBinder token, final int pointerId, final int x, final int y) {
- synchronized (mLock) {
- if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
- if (DEBUG_KEY) {
- Slog.d(TAG, this + ": sendPointerDown," +
- " token=" + token + ", pointerId=" + pointerId);
- }
- final long idToken = Binder.clearCallingIdentity();
- try {
- mProviderMethods.sendPointerDown(TvRemoteProviderProxy.this, token,
- pointerId, x, y);
- } finally {
- Binder.restoreCallingIdentity(idToken);
- }
- } else {
- if (DEBUG) {
- Slog.w(TAG,
- "sendPointerDown, Invalid connection or incorrect uid: " + Binder
- .getCallingUid());
- }
- }
- }
- }
-
- void sendPointerUp(final IBinder token, final int pointerId) {
- synchronized (mLock) {
- if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
- if (DEBUG_KEY) {
- Slog.d(TAG, this + ": sendPointerUp," +
- " token=" + token + ", pointerId=" + pointerId);
- }
- final long idToken = Binder.clearCallingIdentity();
- try {
- mProviderMethods.sendPointerUp(TvRemoteProviderProxy.this, token,
- pointerId);
- } finally {
- Binder.restoreCallingIdentity(idToken);
- }
- } else {
- if (DEBUG) {
- Slog.w(TAG,
- "sendPointerUp, Invalid connection or incorrect uid: " + Binder
- .getCallingUid());
- }
- }
- }
- }
-
- void sendPointerSync(final IBinder token) {
- synchronized (mLock) {
- if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
- if (DEBUG_KEY) {
- Slog.d(TAG, this + ": sendPointerSync," +
- " token=" + token);
- }
- final long idToken = Binder.clearCallingIdentity();
- try {
- if (mProviderMethods != null) {
- mProviderMethods.sendPointerSync(TvRemoteProviderProxy.this, token);
- }
- } finally {
- Binder.restoreCallingIdentity(idToken);
- }
- } else {
- if (DEBUG) {
- Slog.w(TAG,
- "sendPointerSync, Invalid connection or incorrect uid: " + Binder
- .getCallingUid());
- }
- }
- }
- }
- }
-
- /**
- * Receives events from the connected provider.
- * <p>
- * This inner class is static and only retains a weak reference to the connection
- * to prevent the client from being leaked in case the service is holding an
- * active reference to the client's callback.
- * </p>
- */
- private static final class RemoteServiceInputProvider extends ITvRemoteServiceInput.Stub {
- private final WeakReference<Connection> mConnectionRef;
-
- public RemoteServiceInputProvider(Connection connection) {
- mConnectionRef = new WeakReference<Connection>(connection);
- }
-
- public void dispose() {
- // Terminate the connection.
- mConnectionRef.clear();
- }
-
- @Override
- public void openInputBridge(IBinder token, String name, int width,
- int height, int maxPointers) throws RemoteException {
- Connection connection = mConnectionRef.get();
- if (connection != null) {
- connection.openInputBridge(token, name, width, height, maxPointers);
- }
- }
-
- @Override
- public void closeInputBridge(IBinder token) throws RemoteException {
- Connection connection = mConnectionRef.get();
- if (connection != null) {
- connection.closeInputBridge(token);
- }
- }
-
- @Override
- public void clearInputBridge(IBinder token) throws RemoteException {
- Connection connection = mConnectionRef.get();
- if (connection != null) {
- connection.clearInputBridge(token);
- }
- }
-
- @Override
- public void sendTimestamp(IBinder token, long timestamp) throws RemoteException {
- Connection connection = mConnectionRef.get();
- if (connection != null) {
- connection.sendTimestamp(token, timestamp);
- }
- }
-
- @Override
- public void sendKeyDown(IBinder token, int keyCode) throws RemoteException {
- Connection connection = mConnectionRef.get();
- if (connection != null) {
- connection.sendKeyDown(token, keyCode);
- }
- }
-
- @Override
- public void sendKeyUp(IBinder token, int keyCode) throws RemoteException {
- Connection connection = mConnectionRef.get();
- if (connection != null) {
- connection.sendKeyUp(token, keyCode);
- }
- }
-
- @Override
- public void sendPointerDown(IBinder token, int pointerId, int x, int y)
- throws RemoteException {
- Connection connection = mConnectionRef.get();
- if (connection != null) {
- connection.sendPointerDown(token, pointerId, x, y);
- }
- }
-
- @Override
- public void sendPointerUp(IBinder token, int pointerId) throws RemoteException {
- Connection connection = mConnectionRef.get();
- if (connection != null) {
- connection.sendPointerUp(token, pointerId);
- }
- }
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mConnected = false;
- @Override
- public void sendPointerSync(IBinder token) throws RemoteException {
- Connection connection = mConnectionRef.get();
- if (connection != null) {
- connection.sendPointerSync(token);
- }
+ if (DEBUG) {
+ Slog.d(TAG, this + ": onServiceDisconnected()");
}
}
}
diff --git a/services/core/java/com/android/server/tv/TvRemoteProviderWatcher.java b/services/core/java/com/android/server/tv/TvRemoteProviderWatcher.java
index 0d29edd02663..cddcabe80f33 100644
--- a/services/core/java/com/android/server/tv/TvRemoteProviderWatcher.java
+++ b/services/core/java/com/android/server/tv/TvRemoteProviderWatcher.java
@@ -41,27 +41,27 @@ import java.util.Collections;
*/
final class TvRemoteProviderWatcher {
- private static final String TAG = "TvRemoteProvWatcher"; // max. 23 chars
+ private static final String TAG = "TvRemoteProviderWatcher";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
private final Context mContext;
- private final TvRemoteProviderProxy.ProviderMethods mProvider;
private final Handler mHandler;
private final PackageManager mPackageManager;
private final ArrayList<TvRemoteProviderProxy> mProviderProxies = new ArrayList<>();
private final int mUserId;
private final String mUnbundledServicePackage;
+ private final Object mLock;
private boolean mRunning;
- TvRemoteProviderWatcher(Context context, TvRemoteProviderProxy.ProviderMethods provider) {
+ TvRemoteProviderWatcher(Context context, Object lock) {
mContext = context;
- mProvider = provider;
mHandler = new Handler(true);
mUserId = UserHandle.myUserId();
mPackageManager = context.getPackageManager();
mUnbundledServicePackage = context.getString(
com.android.internal.R.string.config_tvRemoteServicePackage);
+ mLock = lock;
}
public void start() {
@@ -116,7 +116,7 @@ final class TvRemoteProviderWatcher {
int sourceIndex = findProvider(serviceInfo.packageName, serviceInfo.name);
if (sourceIndex < 0) {
TvRemoteProviderProxy providerProxy =
- new TvRemoteProviderProxy(mContext, mProvider,
+ new TvRemoteProviderProxy(mContext, mLock,
new ComponentName(serviceInfo.packageName, serviceInfo.name),
mUserId, serviceInfo.applicationInfo.uid);
providerProxy.start();
diff --git a/services/core/java/com/android/server/tv/TvRemoteService.java b/services/core/java/com/android/server/tv/TvRemoteService.java
index bee6fb34a899..58946456b940 100644
--- a/services/core/java/com/android/server/tv/TvRemoteService.java
+++ b/services/core/java/com/android/server/tv/TvRemoteService.java
@@ -17,17 +17,11 @@
package com.android.server.tv;
import android.content.Context;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.ArrayMap;
import android.util.Slog;
import com.android.server.SystemService;
import com.android.server.Watchdog;
-import java.io.IOException;
-import java.util.Map;
-
/**
* TvRemoteService represents a system service that allows a connected
* remote control (emote) service to inject white-listed input events
@@ -38,27 +32,17 @@ import java.util.Map;
public class TvRemoteService extends SystemService implements Watchdog.Monitor {
private static final String TAG = "TvRemoteService";
private static final boolean DEBUG = false;
- private static final boolean DEBUG_KEYS = false;
-
- private final TvRemoteProviderWatcher mWatcher;
- private Map<IBinder, UinputBridge> mBridgeMap = new ArrayMap();
/**
- * State guarded by mLock.
- * This is the second lock in sequence for an incoming call.
- * The first lock is always {@link TvRemoteProviderProxy#mLock}
- *
- * There are currently no methods that break this sequence.
- * Special note:
- * Outgoing call informInputBridgeConnected(), which is called from
- * openInputBridgeInternalLocked() uses a handler thereby relinquishing held locks.
+ * All actions on input bridges are serialized using mLock.
+ * This is necessary because {@link UInputBridge} is not thread-safe.
*/
private final Object mLock = new Object();
+ private final TvRemoteProviderWatcher mWatcher;
public TvRemoteService(Context context) {
super(context);
- mWatcher = new TvRemoteProviderWatcher(context,
- new UserProvider(TvRemoteService.this));
+ mWatcher = new TvRemoteProviderWatcher(context, mLock);
Watchdog.getInstance().addMonitor(this);
}
@@ -81,214 +65,4 @@ public class TvRemoteService extends SystemService implements Watchdog.Monitor {
mWatcher.start(); // Also schedules the start of all providers.
}
}
-
- private boolean openInputBridgeInternalLocked(final IBinder token,
- String name, int width, int height,
- int maxPointers) {
- if (DEBUG) {
- Slog.d(TAG, "openInputBridgeInternalLocked(), token: " + token + ", name: " + name +
- ", width: " + width + ", height: " + height + ", maxPointers: " + maxPointers);
- }
-
- try {
- //Create a new bridge, if one does not exist already
- if (mBridgeMap.containsKey(token)) {
- if (DEBUG) Slog.d(TAG, "RemoteBridge already exists");
- return true;
- }
-
- UinputBridge inputBridge = new UinputBridge(token, name, width, height, maxPointers);
- mBridgeMap.put(token, inputBridge);
-
- try {
- token.linkToDeath(new IBinder.DeathRecipient() {
- @Override
- public void binderDied() {
- synchronized (mLock) {
- closeInputBridgeInternalLocked(token);
- }
- }
- }, 0);
- } catch (RemoteException e) {
- if (DEBUG) Slog.d(TAG, "Token is already dead");
- closeInputBridgeInternalLocked(token);
- return false;
- }
- } catch (IOException ioe) {
- Slog.e(TAG, "Cannot create device for " + name);
- return false;
- }
- return true;
- }
-
- private void closeInputBridgeInternalLocked(IBinder token) {
- if (DEBUG) {
- Slog.d(TAG, "closeInputBridgeInternalLocked(), token: " + token);
- }
-
- // Close an existing RemoteBridge
- UinputBridge inputBridge = mBridgeMap.get(token);
- if (inputBridge != null) {
- inputBridge.close(token);
- }
-
- mBridgeMap.remove(token);
- }
-
- private void clearInputBridgeInternalLocked(IBinder token) {
- if (DEBUG) {
- Slog.d(TAG, "clearInputBridgeInternalLocked(), token: " + token);
- }
-
- UinputBridge inputBridge = mBridgeMap.get(token);
- if (inputBridge != null) {
- inputBridge.clear(token);
- }
- }
-
- private void sendKeyDownInternalLocked(IBinder token, int keyCode) {
- if (DEBUG_KEYS) {
- Slog.d(TAG, "sendKeyDownInternalLocked(), token: " + token + ", keyCode: " + keyCode);
- }
-
- UinputBridge inputBridge = mBridgeMap.get(token);
- if (inputBridge != null) {
- inputBridge.sendKeyDown(token, keyCode);
- }
- }
-
- private void sendKeyUpInternalLocked(IBinder token, int keyCode) {
- if (DEBUG_KEYS) {
- Slog.d(TAG, "sendKeyUpInternalLocked(), token: " + token + ", keyCode: " + keyCode);
- }
-
- UinputBridge inputBridge = mBridgeMap.get(token);
- if (inputBridge != null) {
- inputBridge.sendKeyUp(token, keyCode);
- }
- }
-
- private void sendPointerDownInternalLocked(IBinder token, int pointerId, int x, int y) {
- if (DEBUG_KEYS) {
- Slog.d(TAG, "sendPointerDownInternalLocked(), token: " + token + ", pointerId: " +
- pointerId + ", x: " + x + ", y: " + y);
- }
-
- UinputBridge inputBridge = mBridgeMap.get(token);
- if (inputBridge != null) {
- inputBridge.sendPointerDown(token, pointerId, x, y);
- }
- }
-
- private void sendPointerUpInternalLocked(IBinder token, int pointerId) {
- if (DEBUG_KEYS) {
- Slog.d(TAG, "sendPointerUpInternalLocked(), token: " + token + ", pointerId: " +
- pointerId);
- }
-
- UinputBridge inputBridge = mBridgeMap.get(token);
- if (inputBridge != null) {
- inputBridge.sendPointerUp(token, pointerId);
- }
- }
-
- private void sendPointerSyncInternalLocked(IBinder token) {
- if (DEBUG_KEYS) {
- Slog.d(TAG, "sendPointerSyncInternalLocked(), token: " + token);
- }
-
- UinputBridge inputBridge = mBridgeMap.get(token);
- if (inputBridge != null) {
- inputBridge.sendPointerSync(token);
- }
- }
-
- private final class UserProvider implements TvRemoteProviderProxy.ProviderMethods {
-
- private final TvRemoteService mService;
-
- public UserProvider(TvRemoteService service) {
- mService = service;
- }
-
- @Override
- public boolean openInputBridge(TvRemoteProviderProxy provider, IBinder token, String name,
- int width, int height, int maxPointers) {
- if (DEBUG) {
- Slog.d(TAG, "openInputBridge(), token: " + token +
- ", name: " + name + ", width: " + width +
- ", height: " + height + ", maxPointers: " + maxPointers);
- }
-
- synchronized (mLock) {
- return mService.openInputBridgeInternalLocked(token, name, width,
- height, maxPointers);
- }
- }
-
- @Override
- public void closeInputBridge(TvRemoteProviderProxy provider, IBinder token) {
- if (DEBUG) Slog.d(TAG, "closeInputBridge(), token: " + token);
- synchronized (mLock) {
- mService.closeInputBridgeInternalLocked(token);
- }
- }
-
- @Override
- public void clearInputBridge(TvRemoteProviderProxy provider, IBinder token) {
- if (DEBUG) Slog.d(TAG, "clearInputBridge(), token: " + token);
- synchronized (mLock) {
- mService.clearInputBridgeInternalLocked(token);
- }
- }
-
- @Override
- public void sendKeyDown(TvRemoteProviderProxy provider, IBinder token, int keyCode) {
- if (DEBUG_KEYS) {
- Slog.d(TAG, "sendKeyDown(), token: " + token + ", keyCode: " + keyCode);
- }
- synchronized (mLock) {
- mService.sendKeyDownInternalLocked(token, keyCode);
- }
- }
-
- @Override
- public void sendKeyUp(TvRemoteProviderProxy provider, IBinder token, int keyCode) {
- if (DEBUG_KEYS) {
- Slog.d(TAG, "sendKeyUp(), token: " + token + ", keyCode: " + keyCode);
- }
- synchronized (mLock) {
- mService.sendKeyUpInternalLocked(token, keyCode);
- }
- }
-
- @Override
- public void sendPointerDown(TvRemoteProviderProxy provider, IBinder token, int pointerId,
- int x, int y) {
- if (DEBUG_KEYS) {
- Slog.d(TAG, "sendPointerDown(), token: " + token + ", pointerId: " + pointerId);
- }
- synchronized (mLock) {
- mService.sendPointerDownInternalLocked(token, pointerId, x, y);
- }
- }
-
- @Override
- public void sendPointerUp(TvRemoteProviderProxy provider, IBinder token, int pointerId) {
- if (DEBUG_KEYS) {
- Slog.d(TAG, "sendPointerUp(), token: " + token + ", pointerId: " + pointerId);
- }
- synchronized (mLock) {
- mService.sendPointerUpInternalLocked(token, pointerId);
- }
- }
-
- @Override
- public void sendPointerSync(TvRemoteProviderProxy provider, IBinder token) {
- if (DEBUG_KEYS) Slog.d(TAG, "sendPointerSync(), token: " + token);
- synchronized (mLock) {
- mService.sendPointerSyncInternalLocked(token);
- }
- }
- }
}
diff --git a/services/core/java/com/android/server/tv/TvRemoteServiceInput.java b/services/core/java/com/android/server/tv/TvRemoteServiceInput.java
new file mode 100644
index 000000000000..8fe6da5e8dbe
--- /dev/null
+++ b/services/core/java/com/android/server/tv/TvRemoteServiceInput.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.tv;
+
+import android.media.tv.ITvRemoteProvider;
+import android.media.tv.ITvRemoteServiceInput;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Slog;
+
+import java.io.IOException;
+import java.util.Map;
+
+final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
+ private static final String TAG = "TvRemoteServiceInput";
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_KEYS = false;
+
+ private final Map<IBinder, UinputBridge> mBridgeMap;
+ private final Object mLock;
+ private final ITvRemoteProvider mProvider;
+
+ TvRemoteServiceInput(Object lock, ITvRemoteProvider provider) {
+ mBridgeMap = new ArrayMap();
+ mLock = lock;
+ mProvider = provider;
+ }
+
+ @Override
+ public void openInputBridge(IBinder token, String name, int width,
+ int height, int maxPointers) {
+ if (DEBUG) {
+ Slog.d(TAG, "openInputBridge(), token: " + token
+ + ", name: " + name + ", width: " + width
+ + ", height: " + height + ", maxPointers: " + maxPointers);
+ }
+
+ synchronized (mLock) {
+ if (mBridgeMap.containsKey(token)) {
+ if (DEBUG) {
+ Slog.d(TAG, "InputBridge already exists");
+ }
+ } else {
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ mBridgeMap.put(token,
+ new UinputBridge(token, name, width, height, maxPointers));
+ token.linkToDeath(new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ closeInputBridge(token);
+ }
+ }, 0);
+ } catch (IOException e) {
+ Slog.e(TAG, "Cannot create device for " + name);
+ return;
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Token is already dead");
+ closeInputBridge(token);
+ return;
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ }
+ }
+
+ try {
+ mProvider.onInputBridgeConnected(token);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed remote call to onInputBridgeConnected");
+ }
+ }
+
+ @Override
+ public void closeInputBridge(IBinder token) {
+ if (DEBUG) {
+ Slog.d(TAG, "closeInputBridge(), token: " + token);
+ }
+
+ synchronized (mLock) {
+ UinputBridge inputBridge = mBridgeMap.remove(token);
+ if (inputBridge == null) {
+ return;
+ }
+
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ inputBridge.close(token);
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ }
+ }
+
+ @Override
+ public void clearInputBridge(IBinder token) {
+ if (DEBUG) {
+ Slog.d(TAG, "clearInputBridge, token: " + token);
+ }
+
+ synchronized (mLock) {
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge == null) {
+ return;
+ }
+
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ inputBridge.clear(token);
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ }
+ }
+
+ @Override
+ public void sendTimestamp(IBinder token, long timestamp) {
+ if (DEBUG) {
+ Slog.e(TAG, "sendTimestamp is deprecated, please remove all usages of this API.");
+ }
+ }
+
+ @Override
+ public void sendKeyDown(IBinder token, int keyCode) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendKeyDown(), token: " + token + ", keyCode: " + keyCode);
+ }
+
+ synchronized (mLock) {
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge == null) {
+ return;
+ }
+
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ inputBridge.sendKeyDown(token, keyCode);
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ }
+ }
+
+ @Override
+ public void sendKeyUp(IBinder token, int keyCode) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendKeyUp(), token: " + token + ", keyCode: " + keyCode);
+ }
+
+ synchronized (mLock) {
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge == null) {
+ return;
+ }
+
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ inputBridge.sendKeyUp(token, keyCode);
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ }
+ }
+
+ @Override
+ public void sendPointerDown(IBinder token, int pointerId, int x, int y) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendPointerDown(), token: " + token + ", pointerId: "
+ + pointerId + ", x: " + x + ", y: " + y);
+ }
+
+ synchronized (mLock) {
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge == null) {
+ return;
+ }
+
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ inputBridge.sendPointerDown(token, pointerId, x, y);
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ }
+ }
+
+ @Override
+ public void sendPointerUp(IBinder token, int pointerId) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendPointerUp(), token: " + token + ", pointerId: " + pointerId);
+ }
+
+ synchronized (mLock) {
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge == null) {
+ return;
+ }
+
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ inputBridge.sendPointerUp(token, pointerId);
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ }
+ }
+
+ @Override
+ public void sendPointerSync(IBinder token) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendPointerSync(), token: " + token);
+ }
+
+ synchronized (mLock) {
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge == null) {
+ return;
+ }
+
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ inputBridge.sendPointerSync(token);
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ }
+ }
+}
diff --git a/services/core/jni/com_android_server_VibratorService.cpp b/services/core/jni/com_android_server_VibratorService.cpp
index d5fbd2b316e7..7aa9c7cce876 100644
--- a/services/core/jni/com_android_server_VibratorService.cpp
+++ b/services/core/jni/com_android_server_VibratorService.cpp
@@ -56,37 +56,57 @@ inline Return<R> NullptrStatus() {
return Return<R>{Status::fromExceptionCode(Status::EX_NULL_POINTER)};
}
-// Helper used to transparently deal with the vibrator HAL becoming unavailable.
-template<class R, class I, class... Args0, class... Args1>
-Return<R> halCall(Return<R> (I::* fn)(Args0...), Args1&&... args1) {
- // Assume that if getService returns a nullptr, HAL is not available on the
- // device.
- static sp<I> sHal = I::getService();
- static bool sAvailable = sHal != nullptr;
+template <typename I>
+class HalWrapper {
+ public:
+ static std::unique_ptr<HalWrapper> Create() {
+ // Assume that if getService returns a nullptr, HAL is not available on the
+ // device.
+ auto hal = I::getService();
+ return hal ? std::unique_ptr<HalWrapper>(new HalWrapper(std::move(hal))) : nullptr;
+ }
- if (!sAvailable) {
- return NullptrStatus<R>();
+ // Helper used to transparently deal with the vibrator HAL becoming unavailable.
+ template<class R, class... Args0, class... Args1>
+ Return<R> call(Return<R> (I::* fn)(Args0...), Args1&&... args1) {
+ // Return<R> doesn't have a default constructor, so make a Return<R> with
+ // STATUS::EX_NONE.
+ using ::android::hardware::Status;
+ Return<R> ret{Status::fromExceptionCode(Status::EX_NONE)};
+
+ // Note that ret is guaranteed to be changed after this loop.
+ for (int i = 0; i < NUM_TRIES; ++i) {
+ ret = (mHal == nullptr) ? NullptrStatus<R>()
+ : (*mHal.*fn)(std::forward<Args1>(args1)...);
+
+ if (ret.isOk()) {
+ break;
+ }
+
+ ALOGE("Failed to issue command to vibrator HAL. Retrying.");
+ // Restoring connection to the HAL.
+ mHal = I::tryGetService();
+ }
+ return ret;
}
- // Return<R> doesn't have a default constructor, so make a Return<R> with
- // STATUS::EX_NONE.
- using ::android::hardware::Status;
- Return<R> ret{Status::fromExceptionCode(Status::EX_NONE)};
+ private:
+ HalWrapper(sp<I> &&hal) : mHal(std::move(hal)) {}
- // Note that ret is guaranteed to be changed after this loop.
- for (int i = 0; i < NUM_TRIES; ++i) {
- ret = (sHal == nullptr) ? NullptrStatus<R>()
- : (*sHal.*fn)(std::forward<Args1>(args1)...);
+ private:
+ sp<I> mHal;
+};
- if (ret.isOk()) {
- break;
- }
+template <typename I>
+static auto getHal() {
+ static auto sHalWrapper = HalWrapper<I>::Create();
+ return sHalWrapper.get();
+}
- ALOGE("Failed to issue command to vibrator HAL. Retrying.");
- // Restoring connection to the HAL.
- sHal = I::tryGetService();
- }
- return ret;
+template<class R, class I, class... Args0, class... Args1>
+Return<R> halCall(Return<R> (I::* fn)(Args0...), Args1&&... args1) {
+ auto hal = getHal<I>();
+ return hal ? hal->call(fn, std::forward<Args1>(args1)...) : NullptrStatus<R>();
}
template<class R>
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 3734650adc78..479dd1e5d84e 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -4133,6 +4133,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
if (metrics.quality != quality) {
metrics.quality = quality;
+ resetInactivePasswordRequirementsIfRPlus(userId, ap);
updatePasswordValidityCheckpointLocked(userId, parent);
updatePasswordQualityCacheForUserGroup(userId);
saveSettingsLocked(userId);
@@ -4151,6 +4152,27 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
/**
+ * For admins targeting R+ reset various password constraints to default values when quality is
+ * set to a value that makes those constraints that have no effect.
+ */
+ private void resetInactivePasswordRequirementsIfRPlus(int userId, ActiveAdmin admin) {
+ if (getTargetSdk(admin.info.getPackageName(), userId) > Build.VERSION_CODES.Q) {
+ final PasswordMetrics metrics = admin.minimumPasswordMetrics;
+ if (metrics.quality < PASSWORD_QUALITY_NUMERIC) {
+ metrics.length = ActiveAdmin.DEF_MINIMUM_PASSWORD_LENGTH;
+ }
+ if (metrics.quality < PASSWORD_QUALITY_COMPLEX) {
+ metrics.letters = ActiveAdmin.DEF_MINIMUM_PASSWORD_LETTERS;
+ metrics.upperCase = ActiveAdmin.DEF_MINIMUM_PASSWORD_UPPER_CASE;
+ metrics.lowerCase = ActiveAdmin.DEF_MINIMUM_PASSWORD_LOWER_CASE;
+ metrics.numeric = ActiveAdmin.DEF_MINIMUM_PASSWORD_NUMERIC;
+ metrics.symbols = ActiveAdmin.DEF_MINIMUM_PASSWORD_SYMBOLS;
+ metrics.nonLetter = ActiveAdmin.DEF_MINIMUM_PASSWORD_NON_LETTER;
+ }
+ }
+ }
+
+ /**
* Updates a flag that tells us whether the user's password currently satisfies the
* requirements set by all of the user's active admins. The flag is updated both in memory
* and persisted to disk by calling {@link #saveSettingsLocked}, for the value of the flag
@@ -4281,6 +4303,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
+ ensureMinimumQuality(userId, ap, PASSWORD_QUALITY_NUMERIC, "setPasswordMinimumLength");
if (metrics.length != length) {
metrics.length = length;
updatePasswordValidityCheckpointLocked(userId, parent);
@@ -4295,10 +4318,19 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
.write();
}
+ private void ensureMinimumQuality(
+ int userId, ActiveAdmin admin, int minimumQuality, String operation) {
+ if (admin.minimumPasswordMetrics.quality < minimumQuality
+ && getTargetSdk(admin.info.getPackageName(), userId) > Build.VERSION_CODES.Q) {
+ throw new IllegalStateException(String.format(
+ "password quality should be at least %d for %s", minimumQuality, operation));
+ }
+ }
+
@Override
public int getPasswordMinimumLength(ComponentName who, int userHandle, boolean parent) {
return getStrictestPasswordRequirement(who, userHandle, parent,
- admin -> admin.minimumPasswordMetrics.length, PASSWORD_QUALITY_UNSPECIFIED);
+ admin -> admin.minimumPasswordMetrics.length, PASSWORD_QUALITY_NUMERIC);
}
@Override
@@ -4525,6 +4557,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
synchronized (getLockObject()) {
final ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
+ ensureMinimumQuality(
+ userId, ap, PASSWORD_QUALITY_COMPLEX, "setPasswordMinimumUpperCase");
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
if (metrics.upperCase != length) {
metrics.upperCase = length;
@@ -4553,6 +4587,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
+ ensureMinimumQuality(
+ userId, ap, PASSWORD_QUALITY_COMPLEX, "setPasswordMinimumLowerCase");
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
if (metrics.lowerCase != length) {
metrics.lowerCase = length;
@@ -4584,6 +4620,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
+ ensureMinimumQuality(userId, ap, PASSWORD_QUALITY_COMPLEX, "setPasswordMinimumLetters");
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
if (metrics.letters != length) {
metrics.letters = length;
@@ -4615,6 +4652,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
+ ensureMinimumQuality(userId, ap, PASSWORD_QUALITY_COMPLEX, "setPasswordMinimumNumeric");
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
if (metrics.numeric != length) {
metrics.numeric = length;
@@ -4646,6 +4684,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
+ ensureMinimumQuality(userId, ap, PASSWORD_QUALITY_COMPLEX, "setPasswordMinimumSymbols");
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
if (metrics.symbols != length) {
ap.minimumPasswordMetrics.symbols = length;
@@ -4677,6 +4716,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
+ ensureMinimumQuality(
+ userId, ap, PASSWORD_QUALITY_COMPLEX, "setPasswordMinimumNonLetter");
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
if (metrics.nonLetter != length) {
ap.minimumPasswordMetrics.nonLetter = length;
@@ -8011,6 +8052,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
"clearDeviceOwner can only be called by the device owner");
}
enforceUserUnlocked(deviceOwnerUserId);
+ DevicePolicyData policy = getUserData(deviceOwnerUserId);
+ if (policy.mPasswordTokenHandle != 0) {
+ mLockPatternUtils.removeEscrowToken(policy.mPasswordTokenHandle, deviceOwnerUserId);
+ }
final ActiveAdmin admin = getDeviceOwnerAdminLocked();
long ident = mInjector.binderClearCallingIdentity();
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index d90091017116..a25e40f8cc13 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -1212,6 +1212,45 @@ public class DevicePolicyManagerTest extends DpmTestBase {
assertTrue(dpm.isDeviceManaged());
}
+ /**
+ * Test for: {@link DevicePolicyManager#clearDeviceOwnerApp(String)}
+ *
+ * Validates that when the device owner is removed, the reset password token is cleared
+ */
+ public void testClearDeviceOwner_clearResetPasswordToken() throws Exception {
+ mContext.callerPermissions.add(android.Manifest.permission.MANAGE_DEVICE_ADMINS);
+ mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS);
+ mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
+
+ // Install admin1 on system user
+ setUpPackageManagerForAdmin(admin1, DpmMockContext.CALLER_SYSTEM_USER_UID);
+
+ // Set admin1 to active admin and device owner
+ dpm.setActiveAdmin(admin1, /* replace =*/ false);
+ dpm.setDeviceOwner(admin1, null, UserHandle.USER_SYSTEM);
+
+ // Add reset password token
+ final long handle = 12000;
+ final byte[] token = new byte[32];
+ when(getServices().lockPatternUtils.addEscrowToken(eq(token), eq(UserHandle.USER_SYSTEM),
+ nullable(EscrowTokenStateChangeCallback.class)))
+ .thenReturn(handle);
+ assertTrue(dpm.setResetPasswordToken(admin1, token));
+
+ // Assert reset password token is active
+ when(getServices().lockPatternUtils.isEscrowTokenActive(eq(handle),
+ eq(UserHandle.USER_SYSTEM)))
+ .thenReturn(true);
+ assertTrue(dpm.isResetPasswordTokenActive(admin1));
+
+ // Remove the device owner
+ dpm.clearDeviceOwnerApp(admin1.getPackageName());
+
+ // Verify password reset password token was removed
+ verify(getServices().lockPatternUtils).removeEscrowToken(eq(handle),
+ eq(UserHandle.USER_SYSTEM));
+ }
+
public void testSetProfileOwner() throws Exception {
setAsProfileOwner(admin1);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java b/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java
index 8eecff532ad9..acbbc461e4dd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -47,7 +48,7 @@ public class ProtoLogIntegrationTest {
ProtoLogGroup.testProtoLog();
verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.ERROR), eq(
ProtoLogGroup.TEST_GROUP),
- eq(485522692), eq(0b0010101001010111),
+ anyInt(), eq(0b0010101001010111),
eq(ProtoLogGroup.TEST_GROUP.isLogToLogcat()
? "Test completed successfully: %b %d %o %x %e %g %f %% %s"
: null),
diff --git a/tests/FlickerTests/AndroidManifest.xml b/tests/FlickerTests/AndroidManifest.xml
index 9b73abfd6908..91fb7c12b392 100644
--- a/tests/FlickerTests/AndroidManifest.xml
+++ b/tests/FlickerTests/AndroidManifest.xml
@@ -21,6 +21,8 @@
<!-- Read and write traces from external storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <!-- Write secure settings -->
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<!-- Capture screen contents -->
<uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" />
<!-- Enable / Disable tracing !-->
diff --git a/wifi/java/android/net/wifi/WifiScanner.java b/wifi/java/android/net/wifi/WifiScanner.java
index 075531ce158e..68948cbbe7a9 100644
--- a/wifi/java/android/net/wifi/WifiScanner.java
+++ b/wifi/java/android/net/wifi/WifiScanner.java
@@ -17,6 +17,7 @@
package android.net.wifi;
import android.Manifest;
+import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
@@ -58,13 +59,23 @@ public class WifiScanner {
/** 5 GHz band excluding DFS channels */
public static final int WIFI_BAND_5_GHZ = 2; /* 5 GHz band without DFS channels */
/** DFS channels from 5 GHz band only */
- public static final int WIFI_BAND_5_GHZ_DFS_ONLY = 4; /* 5 GHz band with DFS channels */
+ public static final int WIFI_BAND_5_GHZ_DFS_ONLY = 4; /* 5 GHz band DFS channels */
+ /**
+ * 2.4Ghz band + DFS channels from 5 GHz band only
+ * @hide
+ */
+ public static final int WIFI_BAND_24_GHZ_WITH_5GHZ_DFS = 5;
/** 5 GHz band including DFS channels */
public static final int WIFI_BAND_5_GHZ_WITH_DFS = 6; /* 5 GHz band with DFS channels */
/** Both 2.4 GHz band and 5 GHz band; no DFS channels */
public static final int WIFI_BAND_BOTH = 3; /* both bands without DFS channels */
/** Both 2.4 GHz band and 5 GHz band; with DFS channels */
public static final int WIFI_BAND_BOTH_WITH_DFS = 7; /* both bands with DFS channels */
+ /**
+ * Max band value
+ * @hide
+ */
+ public static final int WIFI_BAND_MAX = 8;
/** Minimum supported scanning period */
public static final int MIN_SCAN_PERIOD_MS = 1000; /* minimum supported period */
@@ -375,19 +386,27 @@ public class WifiScanner {
*/
private int mBandScanned;
/** all scan results discovered in this scan, sorted by timestamp in ascending order */
- private ScanResult mResults[];
+ private final List<ScanResult> mResults;
- ScanData() {}
+ ScanData() {
+ mResults = new ArrayList<>();
+ }
public ScanData(int id, int flags, ScanResult[] results) {
mId = id;
mFlags = flags;
- mResults = results;
+ mResults = new ArrayList<>(Arrays.asList(results));
}
/** {@hide} */
public ScanData(int id, int flags, int bucketsScanned, int bandScanned,
ScanResult[] results) {
+ this(id, flags, bucketsScanned, bandScanned, new ArrayList<>(Arrays.asList(results)));
+ }
+
+ /** {@hide} */
+ public ScanData(int id, int flags, int bucketsScanned, int bandScanned,
+ List<ScanResult> results) {
mId = id;
mFlags = flags;
mBucketsScanned = bucketsScanned;
@@ -400,11 +419,9 @@ public class WifiScanner {
mFlags = s.mFlags;
mBucketsScanned = s.mBucketsScanned;
mBandScanned = s.mBandScanned;
- mResults = new ScanResult[s.mResults.length];
- for (int i = 0; i < s.mResults.length; i++) {
- ScanResult result = s.mResults[i];
- ScanResult newResult = new ScanResult(result);
- mResults[i] = newResult;
+ mResults = new ArrayList<>();
+ for (ScanResult scanResult : s.mResults) {
+ mResults.add(new ScanResult(scanResult));
}
}
@@ -427,7 +444,14 @@ public class WifiScanner {
}
public ScanResult[] getResults() {
- return mResults;
+ return mResults.toArray(new ScanResult[0]);
+ }
+
+ /** {@hide} */
+ public void addResults(@NonNull ScanResult[] newResults) {
+ for (ScanResult result : newResults) {
+ mResults.add(new ScanResult(result));
+ }
}
/** Implement the Parcelable interface {@hide} */
@@ -437,19 +461,11 @@ public class WifiScanner {
/** Implement the Parcelable interface {@hide} */
public void writeToParcel(Parcel dest, int flags) {
- if (mResults != null) {
- dest.writeInt(mId);
- dest.writeInt(mFlags);
- dest.writeInt(mBucketsScanned);
- dest.writeInt(mBandScanned);
- dest.writeInt(mResults.length);
- for (int i = 0; i < mResults.length; i++) {
- ScanResult result = mResults[i];
- result.writeToParcel(dest, flags);
- }
- } else {
- dest.writeInt(0);
- }
+ dest.writeInt(mId);
+ dest.writeInt(mFlags);
+ dest.writeInt(mBucketsScanned);
+ dest.writeInt(mBandScanned);
+ dest.writeParcelableList(mResults, 0);
}
/** Implement the Parcelable interface {@hide} */
@@ -460,11 +476,8 @@ public class WifiScanner {
int flags = in.readInt();
int bucketsScanned = in.readInt();
int bandScanned = in.readInt();
- int n = in.readInt();
- ScanResult results[] = new ScanResult[n];
- for (int i = 0; i < n; i++) {
- results[i] = ScanResult.CREATOR.createFromParcel(in);
- }
+ List<ScanResult> results = new ArrayList<>();
+ in.readParcelableList(results, ScanResult.class.getClassLoader());
return new ScanData(id, flags, bucketsScanned, bandScanned, results);
}
diff --git a/wifi/tests/src/android/net/wifi/WifiScannerTest.java b/wifi/tests/src/android/net/wifi/WifiScannerTest.java
index dd05b47fbd4f..ea136d62b202 100644
--- a/wifi/tests/src/android/net/wifi/WifiScannerTest.java
+++ b/wifi/tests/src/android/net/wifi/WifiScannerTest.java
@@ -22,7 +22,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -445,4 +444,37 @@ public class WifiScannerTest {
assertEquals(WifiScanner.CMD_STOP_PNO_SCAN, message.what);
}
+
+ @Test
+ public void testScanDataAddResults() throws Exception {
+ ScanResult scanResult1 = new ScanResult();
+ scanResult1.SSID = TEST_SSID_1;
+ ScanData scanData = new ScanData(0, 0, new ScanResult[]{scanResult1});
+
+ ScanResult scanResult2 = new ScanResult();
+ scanResult2.SSID = TEST_SSID_2;
+ scanData.addResults(new ScanResult[]{scanResult2});
+
+ ScanResult[] consolidatedScanResults = scanData.getResults();
+ assertEquals(2, consolidatedScanResults.length);
+ assertEquals(TEST_SSID_1, consolidatedScanResults[0].SSID);
+ assertEquals(TEST_SSID_2, consolidatedScanResults[1].SSID);
+ }
+
+ @Test
+ public void testScanDataParcel() throws Exception {
+ ScanResult scanResult1 = new ScanResult();
+ scanResult1.SSID = TEST_SSID_1;
+ ScanData scanData = new ScanData(5, 4, new ScanResult[]{scanResult1});
+
+ Parcel parcel = Parcel.obtain();
+ scanData.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0); // Rewind data position back to the beginning for read.
+ ScanData readScanData = ScanData.CREATOR.createFromParcel(parcel);
+
+ assertEquals(scanData.getId(), readScanData.getId());
+ assertEquals(scanData.getFlags(), readScanData.getFlags());
+ assertEquals(scanData.getResults().length, readScanData.getResults().length);
+ assertEquals(scanData.getResults()[0].SSID, readScanData.getResults()[0].SSID);
+ }
}