diff options
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); + } } |